diff --git a/.changeset/quiet-roles-align.md b/.changeset/quiet-roles-align.md index d96d8707b..f161e1e60 100644 --- a/.changeset/quiet-roles-align.md +++ b/.changeset/quiet-roles-align.md @@ -9,4 +9,4 @@ Add typography role tokens. A role sets font size, line height, font weight, and

Orders sent to suppliers this month.

``` -Each role reads four CSS variables named `--app-shell-type--`, which you can override on `:root`. AppShell's `cn()` now keeps a role together with a text color class, for example in `className="text-label-md text-muted-foreground"` passed to an AppShell component. If your app has its own `tailwind-merge` setup, see the Typography section of the styling guide. +Each role reads four CSS variables named `--type--`, which you can override on `:root`. AppShell's `cn()` now keeps a role together with a text color class, for example in `className="text-label-md text-muted-foreground"` passed to an AppShell component. If your app has its own `tailwind-merge` setup, see the Typography section of the styling guide. diff --git a/decisions/typography-roles.md b/decisions/typography-roles.md index 400702edd..44d7edb14 100644 --- a/decisions/typography-roles.md +++ b/decisions/typography-roles.md @@ -30,7 +30,7 @@ Each role is a named set of four values: size, line height, weight, and letter s ### Two layers, as for colors -- `themes/default.css` holds the values as `--app-shell-type--` on `:root`. A palette or an app can override them. The `--app-shell-` prefix follows the existing `--app-shell-font-sans` override variable and keeps these `:root` variables apart from variables defined by the app or other libraries. +- `themes/default.css` holds the values as `--type--` on `:root`. A palette or an app can override them. They have no `--app-shell-` prefix, like the colour roles and `--semantic-shadow-*`. `--app-shell-font-sans` needs the prefix because `--font-*` is a Tailwind theme namespace; `--type-*` is not, and the bridge writes `--text-`, so the names do not clash with the app's Tailwind. - `theme.bridge.css` maps them to Tailwind with `--text-` and its `--line-height`, `--font-weight`, and `--letter-spacing` suffixes, inside the existing `@theme inline`. `@theme inline` writes literal values into the utility. If the values sat in the bridge, they could not be overridden on `:root`. So the bridge holds only `var()` references. diff --git a/docs-manifest.json b/docs-manifest.json index 749f4e4b3..d31c5e30d 100644 --- a/docs-manifest.json +++ b/docs-manifest.json @@ -1304,9 +1304,9 @@ "symbols": [], "hashes": { "typeSurface": null, - "outline": "e63d837190e71ff1", + "outline": "208932a187e21671", "snapshot": null, - "outputMd": "112b43c697d30369", + "outputMd": "5a624c507ee93e09", "examples": "4d19f4f688e0f0ac" } }, @@ -1676,7 +1676,7 @@ "packages/core/skills/app-shell-patterns/references/concepts/modules-and-resources.md": "bf15b5460f22d165", "packages/core/skills/app-shell-patterns/references/concepts/routing-navigation.md": "8034c7f885994b29", "packages/core/skills/app-shell-patterns/references/concepts/sidebar-navigation.md": "e342ef02dc2b9b86", - "packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "112b43c697d30369", + "packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "5a624c507ee93e09", "packages/core/skills/app-shell-patterns/references/components/action-panel.md": "71794d4b561e4d4b", "packages/core/skills/app-shell-patterns/references/components/activity-card.md": "4f386e49380fb0b0", "packages/core/skills/app-shell-patterns/references/components/ai-chat.md": "824d1aa36bb7f194", diff --git a/docs-src/concepts/styling-theming.docs.outline.md b/docs-src/concepts/styling-theming.docs.outline.md index c2d5d721e..1477d6712 100644 --- a/docs-src/concepts/styling-theming.docs.outline.md +++ b/docs-src/concepts/styling-theming.docs.outline.md @@ -305,12 +305,12 @@ On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's Always use `tabular-nums` for numbers that stack in a column — without it, digits jitter between rows. -**Change a role.** Each role is four CSS variables named `--app-shell-type--`. Override them on `:root` to change a role for the whole app. Typography has no dark variant, so one override applies to light and dark mode. +**Change a role.** Each role is four CSS variables named `--type--`. Override them on `:root` to change a role for the whole app. Typography has no dark variant, so one override applies to light and dark mode. ```css :root { - --app-shell-type-heading-lg-size: 1.75rem; - --app-shell-type-heading-lg-line-height: calc(36 / 28); + --type-heading-lg-size: 1.75rem; + --type-heading-lg-line-height: calc(36 / 28); } ``` diff --git a/docs/concepts/styling-theming.md b/docs/concepts/styling-theming.md index 61a21f8dc..0c6a39d59 100644 --- a/docs/concepts/styling-theming.md +++ b/docs/concepts/styling-theming.md @@ -420,12 +420,12 @@ On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's Always use `tabular-nums` for numbers that stack in a column — without it, digits jitter between rows. -**Change a role.** Each role is four CSS variables named `--app-shell-type--`. Override them on `:root` to change a role for the whole app. Typography has no dark variant, so one override applies to light and dark mode. +**Change a role.** Each role is four CSS variables named `--type--`. Override them on `:root` to change a role for the whole app. Typography has no dark variant, so one override applies to light and dark mode. ```css :root { - --app-shell-type-heading-lg-size: 1.75rem; - --app-shell-type-heading-lg-line-height: calc(36 / 28); + --type-heading-lg-size: 1.75rem; + --type-heading-lg-line-height: calc(36 / 28); } ``` diff --git a/packages/core/src/assets/theme-bridge.test.ts b/packages/core/src/assets/theme-bridge.test.ts index 86fb5ef0c..63991962e 100644 --- a/packages/core/src/assets/theme-bridge.test.ts +++ b/packages/core/src/assets/theme-bridge.test.ts @@ -71,19 +71,20 @@ describe("theme bridge", () => { describe("typography roles", () => { const slots = ["size", "line-height", "weight", "letter-spacing"] as const; - /** `--app-shell-type--` variables in the default palette, grouped by role. */ + /** `--type--` variables in the default palette, grouped by role. */ const defined = new Map>(); for (const m of defaultTheme.matchAll( - /^\s*--app-shell-type-([a-z-]+)-(size|line-height|weight|letter-spacing):\s*([^;]+);/gm, + /^\s*--type-([a-z-]+)-(size|line-height|weight|letter-spacing):\s*([^;]+);/gm, )) { const role = m[1]; defined.set(role, (defined.get(role) ?? new Map()).set(m[2], m[3].trim())); } - /** Bridge entries `--text-[--]: var(--app-shell-type-...)`, keyed by property name. */ + /** Bridge entries `--text-[--]: var(--type-...)`, keyed by property name. */ const bridgedText = new Map( - [...bridge.matchAll(/^\s*(--text-[a-z-]+):\s*var\((--app-shell-type-[a-z-]+)\);/gm)].map( - (m) => [m[1], m[2]], - ), + [...bridge.matchAll(/^\s*(--text-[a-z-]+):\s*var\((--type-[a-z-]+)\);/gm)].map((m) => [ + m[1], + m[2], + ]), ); it("pins the role count at 10", () => { @@ -107,7 +108,7 @@ describe("theme bridge", () => { const expected = new Map(); for (const role of defined.keys()) { for (const slot of slots) { - expected.set(`--text-${role}${suffix[slot]}`, `--app-shell-type-${role}-${slot}`); + expected.set(`--text-${role}${suffix[slot]}`, `--type-${role}-${slot}`); } } expect(bridgedText).toEqual(expected); diff --git a/packages/core/src/assets/theme.bridge.css b/packages/core/src/assets/theme.bridge.css index 31d9cc0b5..a1382bdef 100644 --- a/packages/core/src/assets/theme.bridge.css +++ b/packages/core/src/assets/theme.bridge.css @@ -45,46 +45,46 @@ * The `--text-*` namespace cannot set font-family. `text-code-sm` is used with * `font-mono`. */ - --text-heading-lg: var(--app-shell-type-heading-lg-size); - --text-heading-lg--line-height: var(--app-shell-type-heading-lg-line-height); - --text-heading-lg--font-weight: var(--app-shell-type-heading-lg-weight); - --text-heading-lg--letter-spacing: var(--app-shell-type-heading-lg-letter-spacing); - --text-heading-md: var(--app-shell-type-heading-md-size); - --text-heading-md--line-height: var(--app-shell-type-heading-md-line-height); - --text-heading-md--font-weight: var(--app-shell-type-heading-md-weight); - --text-heading-md--letter-spacing: var(--app-shell-type-heading-md-letter-spacing); - --text-heading-sm: var(--app-shell-type-heading-sm-size); - --text-heading-sm--line-height: var(--app-shell-type-heading-sm-line-height); - --text-heading-sm--font-weight: var(--app-shell-type-heading-sm-weight); - --text-heading-sm--letter-spacing: var(--app-shell-type-heading-sm-letter-spacing); - --text-body-md: var(--app-shell-type-body-md-size); - --text-body-md--line-height: var(--app-shell-type-body-md-line-height); - --text-body-md--font-weight: var(--app-shell-type-body-md-weight); - --text-body-md--letter-spacing: var(--app-shell-type-body-md-letter-spacing); - --text-body-sm: var(--app-shell-type-body-sm-size); - --text-body-sm--line-height: var(--app-shell-type-body-sm-line-height); - --text-body-sm--font-weight: var(--app-shell-type-body-sm-weight); - --text-body-sm--letter-spacing: var(--app-shell-type-body-sm-letter-spacing); - --text-label-md: var(--app-shell-type-label-md-size); - --text-label-md--line-height: var(--app-shell-type-label-md-line-height); - --text-label-md--font-weight: var(--app-shell-type-label-md-weight); - --text-label-md--letter-spacing: var(--app-shell-type-label-md-letter-spacing); - --text-label-sm: var(--app-shell-type-label-sm-size); - --text-label-sm--line-height: var(--app-shell-type-label-sm-line-height); - --text-label-sm--font-weight: var(--app-shell-type-label-sm-weight); - --text-label-sm--letter-spacing: var(--app-shell-type-label-sm-letter-spacing); - --text-code-sm: var(--app-shell-type-code-sm-size); - --text-code-sm--line-height: var(--app-shell-type-code-sm-line-height); - --text-code-sm--font-weight: var(--app-shell-type-code-sm-weight); - --text-code-sm--letter-spacing: var(--app-shell-type-code-sm-letter-spacing); - --text-body-md-relaxed: var(--app-shell-type-body-md-relaxed-size); - --text-body-md-relaxed--line-height: var(--app-shell-type-body-md-relaxed-line-height); - --text-body-md-relaxed--font-weight: var(--app-shell-type-body-md-relaxed-weight); - --text-body-md-relaxed--letter-spacing: var(--app-shell-type-body-md-relaxed-letter-spacing); - --text-body-sm-relaxed: var(--app-shell-type-body-sm-relaxed-size); - --text-body-sm-relaxed--line-height: var(--app-shell-type-body-sm-relaxed-line-height); - --text-body-sm-relaxed--font-weight: var(--app-shell-type-body-sm-relaxed-weight); - --text-body-sm-relaxed--letter-spacing: var(--app-shell-type-body-sm-relaxed-letter-spacing); + --text-heading-lg: var(--type-heading-lg-size); + --text-heading-lg--line-height: var(--type-heading-lg-line-height); + --text-heading-lg--font-weight: var(--type-heading-lg-weight); + --text-heading-lg--letter-spacing: var(--type-heading-lg-letter-spacing); + --text-heading-md: var(--type-heading-md-size); + --text-heading-md--line-height: var(--type-heading-md-line-height); + --text-heading-md--font-weight: var(--type-heading-md-weight); + --text-heading-md--letter-spacing: var(--type-heading-md-letter-spacing); + --text-heading-sm: var(--type-heading-sm-size); + --text-heading-sm--line-height: var(--type-heading-sm-line-height); + --text-heading-sm--font-weight: var(--type-heading-sm-weight); + --text-heading-sm--letter-spacing: var(--type-heading-sm-letter-spacing); + --text-body-md: var(--type-body-md-size); + --text-body-md--line-height: var(--type-body-md-line-height); + --text-body-md--font-weight: var(--type-body-md-weight); + --text-body-md--letter-spacing: var(--type-body-md-letter-spacing); + --text-body-sm: var(--type-body-sm-size); + --text-body-sm--line-height: var(--type-body-sm-line-height); + --text-body-sm--font-weight: var(--type-body-sm-weight); + --text-body-sm--letter-spacing: var(--type-body-sm-letter-spacing); + --text-label-md: var(--type-label-md-size); + --text-label-md--line-height: var(--type-label-md-line-height); + --text-label-md--font-weight: var(--type-label-md-weight); + --text-label-md--letter-spacing: var(--type-label-md-letter-spacing); + --text-label-sm: var(--type-label-sm-size); + --text-label-sm--line-height: var(--type-label-sm-line-height); + --text-label-sm--font-weight: var(--type-label-sm-weight); + --text-label-sm--letter-spacing: var(--type-label-sm-letter-spacing); + --text-code-sm: var(--type-code-sm-size); + --text-code-sm--line-height: var(--type-code-sm-line-height); + --text-code-sm--font-weight: var(--type-code-sm-weight); + --text-code-sm--letter-spacing: var(--type-code-sm-letter-spacing); + --text-body-md-relaxed: var(--type-body-md-relaxed-size); + --text-body-md-relaxed--line-height: var(--type-body-md-relaxed-line-height); + --text-body-md-relaxed--font-weight: var(--type-body-md-relaxed-weight); + --text-body-md-relaxed--letter-spacing: var(--type-body-md-relaxed-letter-spacing); + --text-body-sm-relaxed: var(--type-body-sm-relaxed-size); + --text-body-sm-relaxed--line-height: var(--type-body-sm-relaxed-line-height); + --text-body-sm-relaxed--font-weight: var(--type-body-sm-relaxed-weight); + --text-body-sm-relaxed--letter-spacing: var(--type-body-sm-relaxed-letter-spacing); --color-background: var(--background); --color-foreground: var(--foreground); diff --git a/packages/core/src/assets/theme.css b/packages/core/src/assets/theme.css index fac3798e7..a61d3b134 100644 --- a/packages/core/src/assets/theme.css +++ b/packages/core/src/assets/theme.css @@ -17,7 +17,7 @@ * * Inherited from default.css — do not duplicate in new palettes: * --ring, --sidebar-*, --primitive-* (colour scales), --status-*, --alert-*, - * --app-shell-type-* (typography roles). + * --type-* (typography roles). * The colour roles --{intent}-{role} inherit too; a palette overrides them * only to recolour the status components. Typography roles are inherited * the same way: do not copy the defaults, override only the variables diff --git a/packages/core/src/assets/themes/_template.css b/packages/core/src/assets/themes/_template.css index a4dd7e390..7874cca71 100644 --- a/packages/core/src/assets/themes/_template.css +++ b/packages/core/src/assets/themes/_template.css @@ -18,11 +18,11 @@ * * DO NOT SET (inherited from `:root` in default.css): * --ring, --sidebar-*, --primitive-* (colour scales), --status-*, --alert-*, - * --app-shell-type-* + * --type-* * The colour roles --{info|success|warning|danger|neutral}-{role} also inherit. * Override a role only to recolour the status components (Badge, Alert, ...) * for this palette; never set --status-* or --alert-*, they alias the roles. - * Typography roles (--app-shell-type-*) inherit too: do not copy the defaults, + * Typography roles (--type-*) inherit too: do not copy the defaults, * override only the variables whose values differ. * * PREVIEW: `/showcase/colors` in the Vite example app. diff --git a/packages/core/src/assets/themes/default.css b/packages/core/src/assets/themes/default.css index 0a76bc497..113f69c34 100644 --- a/packages/core/src/assets/themes/default.css +++ b/packages/core/src/assets/themes/default.css @@ -435,48 +435,48 @@ * * Four variables per role: size, line-height (unitless ratio), weight and * letter-spacing. theme.bridge.css maps them to `text-` utilities. - * Palettes inherit these; override `--app-shell-type-*` on `:root` to change + * Palettes inherit these; override `--type-*` on `:root` to change * a role. See decisions/typography-roles.md. */ :root { - --app-shell-type-heading-lg-size: 1.5rem; - --app-shell-type-heading-lg-line-height: calc(32 / 24); - --app-shell-type-heading-lg-weight: 700; - --app-shell-type-heading-lg-letter-spacing: -0.025em; - --app-shell-type-heading-md-size: 1.125rem; - --app-shell-type-heading-md-line-height: calc(28 / 18); - --app-shell-type-heading-md-weight: 600; - --app-shell-type-heading-md-letter-spacing: 0em; - --app-shell-type-heading-sm-size: 0.875rem; - --app-shell-type-heading-sm-line-height: calc(20 / 14); - --app-shell-type-heading-sm-weight: 600; - --app-shell-type-heading-sm-letter-spacing: 0em; - --app-shell-type-body-md-size: 0.875rem; - --app-shell-type-body-md-line-height: calc(20 / 14); - --app-shell-type-body-md-weight: 400; - --app-shell-type-body-md-letter-spacing: 0em; - --app-shell-type-body-sm-size: 0.75rem; - --app-shell-type-body-sm-line-height: calc(16 / 12); - --app-shell-type-body-sm-weight: 400; - --app-shell-type-body-sm-letter-spacing: 0em; - --app-shell-type-label-md-size: 0.875rem; - --app-shell-type-label-md-line-height: calc(20 / 14); - --app-shell-type-label-md-weight: 500; - --app-shell-type-label-md-letter-spacing: 0em; - --app-shell-type-label-sm-size: 0.75rem; - --app-shell-type-label-sm-line-height: calc(16 / 12); - --app-shell-type-label-sm-weight: 500; - --app-shell-type-label-sm-letter-spacing: 0em; - --app-shell-type-code-sm-size: 0.75rem; - --app-shell-type-code-sm-line-height: calc(18 / 12); - --app-shell-type-code-sm-weight: 400; - --app-shell-type-code-sm-letter-spacing: 0em; - --app-shell-type-body-md-relaxed-size: 0.875rem; - --app-shell-type-body-md-relaxed-line-height: calc(24 / 14); - --app-shell-type-body-md-relaxed-weight: 400; - --app-shell-type-body-md-relaxed-letter-spacing: 0em; - --app-shell-type-body-sm-relaxed-size: 0.75rem; - --app-shell-type-body-sm-relaxed-line-height: calc(20 / 12); - --app-shell-type-body-sm-relaxed-weight: 400; - --app-shell-type-body-sm-relaxed-letter-spacing: 0em; + --type-heading-lg-size: 1.5rem; + --type-heading-lg-line-height: calc(32 / 24); + --type-heading-lg-weight: 700; + --type-heading-lg-letter-spacing: -0.025em; + --type-heading-md-size: 1.125rem; + --type-heading-md-line-height: calc(28 / 18); + --type-heading-md-weight: 600; + --type-heading-md-letter-spacing: 0em; + --type-heading-sm-size: 0.875rem; + --type-heading-sm-line-height: calc(20 / 14); + --type-heading-sm-weight: 600; + --type-heading-sm-letter-spacing: 0em; + --type-body-md-size: 0.875rem; + --type-body-md-line-height: calc(20 / 14); + --type-body-md-weight: 400; + --type-body-md-letter-spacing: 0em; + --type-body-sm-size: 0.75rem; + --type-body-sm-line-height: calc(16 / 12); + --type-body-sm-weight: 400; + --type-body-sm-letter-spacing: 0em; + --type-label-md-size: 0.875rem; + --type-label-md-line-height: calc(20 / 14); + --type-label-md-weight: 500; + --type-label-md-letter-spacing: 0em; + --type-label-sm-size: 0.75rem; + --type-label-sm-line-height: calc(16 / 12); + --type-label-sm-weight: 500; + --type-label-sm-letter-spacing: 0em; + --type-code-sm-size: 0.75rem; + --type-code-sm-line-height: calc(18 / 12); + --type-code-sm-weight: 400; + --type-code-sm-letter-spacing: 0em; + --type-body-md-relaxed-size: 0.875rem; + --type-body-md-relaxed-line-height: calc(24 / 14); + --type-body-md-relaxed-weight: 400; + --type-body-md-relaxed-letter-spacing: 0em; + --type-body-sm-relaxed-size: 0.75rem; + --type-body-sm-relaxed-line-height: calc(20 / 12); + --type-body-sm-relaxed-weight: 400; + --type-body-sm-relaxed-letter-spacing: 0em; } diff --git a/packages/core/src/lib/utils.test.ts b/packages/core/src/lib/utils.test.ts index 6b5c8db76..4f039f1d5 100644 --- a/packages/core/src/lib/utils.test.ts +++ b/packages/core/src/lib/utils.test.ts @@ -9,7 +9,7 @@ const defaultTheme = readFileSync( "utf8", ); const roles = [ - ...new Set([...defaultTheme.matchAll(/^\s*--app-shell-type-([a-z-]+)-size:/gm)].map((m) => m[1])), + ...new Set([...defaultTheme.matchAll(/^\s*--type-([a-z-]+)-size:/gm)].map((m) => m[1])), ]; describe("cn", () => { diff --git a/packages/core/tests/typography-compile.test.ts b/packages/core/tests/typography-compile.test.ts index 75fc503f3..74e951175 100644 --- a/packages/core/tests/typography-compile.test.ts +++ b/packages/core/tests/typography-compile.test.ts @@ -55,19 +55,15 @@ describe("typography role utilities", () => { const css = await compile(header, [`${prefix}text-label-md`, `${prefix}text-heading-lg`]); const label = declarationsOf(css, `.${escaped}text-label-md`); - expect(label.get("font-size")).toBe("var(--app-shell-type-label-md-size)"); - expect(label.get("line-height")).toBe( - "var(--tw-leading, var(--app-shell-type-label-md-line-height))", - ); - expect(label.get("font-weight")).toBe( - "var(--tw-font-weight, var(--app-shell-type-label-md-weight))", - ); + expect(label.get("font-size")).toBe("var(--type-label-md-size)"); + expect(label.get("line-height")).toBe("var(--tw-leading, var(--type-label-md-line-height))"); + expect(label.get("font-weight")).toBe("var(--tw-font-weight, var(--type-label-md-weight))"); expect(label.get("letter-spacing")).toBe( - "var(--tw-tracking, var(--app-shell-type-label-md-letter-spacing))", + "var(--tw-tracking, var(--type-label-md-letter-spacing))", ); const heading = declarationsOf(css, `.${escaped}text-heading-lg`); - expect(heading.get("font-size")).toBe("var(--app-shell-type-heading-lg-size)"); + expect(heading.get("font-size")).toBe("var(--type-heading-lg-size)"); }, ); @@ -83,9 +79,7 @@ describe("typography role utilities", () => { it("emits every role defined in the default palette", async () => { const roles = [ - ...new Set( - [...defaultTheme.matchAll(/^\s*--app-shell-type-([a-z-]+)-size:/gm)].map((m) => m[1]), - ), + ...new Set([...defaultTheme.matchAll(/^\s*--type-([a-z-]+)-size:/gm)].map((m) => m[1])), ]; const css = await compile( '@import "tailwindcss" source(none);', @@ -93,15 +87,15 @@ describe("typography role utilities", () => { ); for (const role of roles) { const decls = declarationsOf(css, `.text-${role}`); - expect(decls.get("font-size"), role).toBe(`var(--app-shell-type-${role}-size)`); + expect(decls.get("font-size"), role).toBe(`var(--type-${role}-size)`); expect(decls.get("line-height"), role).toBe( - `var(--tw-leading, var(--app-shell-type-${role}-line-height))`, + `var(--tw-leading, var(--type-${role}-line-height))`, ); expect(decls.get("letter-spacing"), role).toBe( - `var(--tw-tracking, var(--app-shell-type-${role}-letter-spacing))`, + `var(--tw-tracking, var(--type-${role}-letter-spacing))`, ); expect(decls.get("font-weight"), role).toBe( - `var(--tw-font-weight, var(--app-shell-type-${role}-weight))`, + `var(--tw-font-weight, var(--type-${role}-weight))`, ); expect(decls.size, role).toBe(4); }