diff --git a/.agents/references/component-design.md b/.agents/references/component-design.md index a2ba6c9de..a5bc868e1 100644 --- a/.agents/references/component-design.md +++ b/.agents/references/component-design.md @@ -427,6 +427,27 @@ Review whether component styling remains self-contained and consistent:
``` +### Typography roles + +Text that a component renders takes its size, line height, weight, and letter spacing from a typography role, not from a raw size + weight combination. The roles are listed in the Typography section of `docs/concepts/styling-theming.md`. + +- Use the role utility (`astw:text-heading-md`, `astw:text-label-md`, ...) on the component's own element, or render `Text` when the element itself is the text. +- Do not write new raw combinations such as `astw:text-lg astw:font-semibold` or `astw:text-sm astw:font-medium`. Existing ones are being moved to roles. +- Combine a role only with the four modifiers: `astw:leading-none` (single-line text), `astw:tabular-nums`, `astw:font-medium` or `astw:font-semibold`, and `astw:uppercase`. Do not add other `leading-*`, `tracking-*`, or `text-[Npx]` values. 12px is the minimum size. +- A role carries no color. Set the color with a token such as `astw:text-muted-foreground`. +- A heading's level comes from the document structure, not from its size. Keep a title that is a heading in a heading element, and pick the role for its look. Do not change the element to get a different size, and do not turn a heading into a `div` to get a different line height. Under the `cream` and `bloom` palettes, `h1` to `h6` ignore `leading-none`; see the Typography section. + +```tsx +// Good +

{title}

+{updatedAt} +``` + +```tsx +// Bad: a raw size + weight combination and a line height outside the roles +

{title}

+``` + ### Container queries Prefer Tailwind container-query utilities with arbitrary values. diff --git a/.changeset/text-role-variants.md b/.changeset/text-role-variants.md new file mode 100644 index 000000000..a19223bed --- /dev/null +++ b/.changeset/text-role-variants.md @@ -0,0 +1,12 @@ +--- +"@tailor-platform/app-shell": minor +--- + +Add the `Text` component. It applies a typography role with the `variant` prop and does not set a color. Heading roles render `h2`, body roles render `p`, and label and code roles render `span`. Use the `render` prop to choose a different element, such as the heading level. + +```tsx +import { Text } from "@tailor-platform/app-shell"; + +}>Line items +Orders sent this month. +``` diff --git a/decisions/typography-roles.md b/decisions/typography-roles.md index 400702edd..23ef6a41e 100644 --- a/decisions/typography-roles.md +++ b/decisions/typography-roles.md @@ -1,7 +1,7 @@ # Decision: typography role tokens -> Status: **Decided for the first change — add 10 typography role tokens as CSS variables with Tailwind `text-*` utilities. No component uses them yet.** -> Scope: role names and values, how they reach Tailwind, and the rules for combining them. This does not cover applying roles to AppShell components, a `Text` component, or sizes below 12px. +> Status: **Decided for the first two changes — add 10 typography role tokens as CSS variables with Tailwind `text-*` utilities, then a `Text` component on top of them. AppShell's own components do not use the roles yet.** +> Scope: role names and values, how they reach Tailwind, the rules for combining them, and the `Text` component (see "Second change" at the end). This does not cover applying roles to AppShell components or sizes below 12px. ## Context @@ -84,5 +84,17 @@ Limit: this does not let a role replace a size that core sets on its own element - **No visual change.** No component uses the roles yet. The default, cream, and bloom palettes render as before. - **cream and bloom `h1` to `h6` rule.** Both palettes set `line-height: 1.2` and `letter-spacing: -0.03em` on `html :where(h1, h2, h3, h4, h5, h6)` outside any `@layer`. That rule wins over a role utility on those elements, so a heading role on an `h1` to `h6` gets the palette values for line height and letter spacing. It also wins over any `leading-*` or `tracking-*` class on those elements, including the `leading-none` in the card title pairing: an `h3` with `text-heading-md leading-none` has a line height of 1.2 under cream and bloom, and 1.0 under the default palette. The same classes on a `div` or `span` render as documented. This PR keeps the rule and documents the effect. Open question for the maintainers: what does this rule protect, and should it stay once heading roles exist? - **Keyboard hint.** The earlier pairing table listed "keyboard hint" with `font-mono text-xs`. The only keyboard hint in the code (the sidebar ⌘K hint) uses `text-xs font-normal`. The new table has no keyboard hint row. -- **Follow-up changes** (not in this PR): apply the roles to core components; `overline`; migrate 10px and 11px text to 12px; a `Text` component; numeric DataTable columns with `tabular-nums` and right alignment as the default; a MetricCard value role and a display role above 24px. +- **Follow-up changes** (not in this PR): apply the roles to core components; `overline`; migrate 10px and 11px text to 12px; a `Text` component (added in a second change, see below); numeric DataTable columns with `tabular-nums` and right alignment as the default; a MetricCard value role and a display role above 24px. - **Docs example.** The live example in the docs uses `div` and `p` elements. The docs app runs the bloom palette, and its `h1` to `h6` rule would otherwise change the heading rows. + +## Second change: the `Text` component + +> Status: **Decided.** Applying the roles to core components is a third change. + +- **API.** `Text` takes the role as `variant`. The prop is not called `role`, because `role` is an ARIA attribute. The element is set with Base UI's `render` prop, as in `Button`, the sidebar primitives, and the breadcrumb. AppShell has no `as` or `asChild` prop. +- **Default elements.** The three heading roles render `h2`. The body roles, including the relaxed variants, render `p`. `label-md`, `label-sm`, and `code-sm` render `span`. All headings share one default so that no heading level is implied by size. `h2` is the default because a page has one `h1` (`Layout.Header`), and `Dialog.Title` from Base UI also renders `h2`. A default is hard to change later, because every call site without `render` would get a different element. +- **Lint.** The `jsx-a11y/heading-has-content` rule reports `render={

}` as an empty heading, and the function form `render={(props) =>

}` too. Apps that enable the rule disable it on that line. A `level` prop would avoid this, but it would add a second way to choose the element next to `render`. The documentation shows the disable comment instead. +- **No color or modifier props.** Color and the four modifiers stay in `className`, as for the utilities. A consumer `leading-none`, `font-medium`, or `tracking-*` overrides the role even though AppShell's prefixed sheet loads later, because the role reads the shared `--tw-leading`, `--tw-font-weight`, and `--tw-tracking` variables first. `tests/typography-compile.test.ts` checks this. +- **No `textVariants` export.** A role can already be applied with the `text-` utility or with `Text`. A third way would add public surface with no use case that these two do not cover. Unlike `Button` and `Badge`, `Text` does not export its cva function. It can be exported later without a breaking change. +- **`code-sm` includes `font-mono`.** The utility cannot set the font family, but the component can. +- **cream and bloom.** The `h1` to `h6` rule is unchanged. The default `h2` takes the palette's line height and letter spacing under these palettes, the same as existing headings in AppShell components today. diff --git a/docs-manifest.json b/docs-manifest.json index 749f4e4b3..ea7bfc404 100644 --- a/docs-manifest.json +++ b/docs-manifest.json @@ -1304,9 +1304,9 @@ "symbols": [], "hashes": { "typeSurface": null, - "outline": "e63d837190e71ff1", + "outline": "8ad61f58de459be2", "snapshot": null, - "outputMd": "112b43c697d30369", + "outputMd": "c40075c1f34fb315", "examples": "4d19f4f688e0f0ac" } }, @@ -1344,6 +1344,23 @@ "examples": "b07f258088e81cac" } }, + "text": { + "slug": "text", + "kind": "code-backed", + "outline": "docs-src/components/text.docs.outline.md", + "output": "docs/components/text.md", + "examples": "docs-src/components/text.docs.examples.tsx", + "sources": ["packages/core/src/components/text/**"], + "claims": [], + "symbols": ["Text", "TextProps"], + "hashes": { + "typeSurface": "ed54a16ca677dbe3", + "outline": "b354c6be5f81ce2a", + "snapshot": "f5d9429ab497863a", + "outputMd": "6436f68b6b404f0b", + "examples": "4a832708dc79474b" + } + }, "textarea": { "slug": "textarea", "kind": "code-backed", @@ -1676,7 +1693,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": "c40075c1f34fb315", "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", @@ -1716,6 +1733,7 @@ "packages/core/skills/app-shell-patterns/references/components/spinner.md": "e05d94ba9c3d17da", "packages/core/skills/app-shell-patterns/references/components/table.md": "f85ec64ceb7f4acf", "packages/core/skills/app-shell-patterns/references/components/tabs.md": "30bb32e57fc78bd9", + "packages/core/skills/app-shell-patterns/references/components/text.md": "6436f68b6b404f0b", "packages/core/skills/app-shell-patterns/references/components/textarea.md": "c940bc01cd573334", "packages/core/skills/app-shell-patterns/references/components/timeline.md": "699249c9452485ab", "packages/core/skills/app-shell-patterns/references/components/toolbar.md": "91d70b140c7bb747", @@ -1757,6 +1775,6 @@ "packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md": "916baa3cd2b9cbf1", "packages/core/skills/app-shell-patterns/references/pages/document-detail.md": "717ef4e7ae835aff", "packages/core/skills/app-shell-patterns/references/migrations.md": "013c3fef2303b096", - "packages/core/skills/app-shell-patterns/SKILL.md": "526decbfd2db6555" + "packages/core/skills/app-shell-patterns/SKILL.md": "f9d8e8f67c1b895e" } } diff --git a/docs-src/components/text.docs.examples.tsx b/docs-src/components/text.docs.examples.tsx new file mode 100644 index 000000000..fbf44b839 --- /dev/null +++ b/docs-src/components/text.docs.examples.tsx @@ -0,0 +1,45 @@ +import { Text } from "@tailor-platform/app-shell"; + +export function BasicUsage() { + return ( +
+ Purchase orders + Orders sent to suppliers this month. +
+ ); +} + +export function Roles() { + return ( +
+ {/* The headings render a div so that this specimen is not a document heading. */} + }> + heading-lg: Purchase orders + + }> + heading-md: Line items + + }> + heading-sm: Shipping address + + body-md: Orders sent to suppliers this month. + body-sm: Updated 2 hours ago + label-md: Total amount + label-sm: Draft + code-sm: PO-2026-0001 +
+ ); +} + +export function ColorAndModifiers() { + return ( +
+ + Orders sent this month. + + + 1,240.00 + +
+ ); +} diff --git a/docs-src/components/text.docs.outline.md b/docs-src/components/text.docs.outline.md new file mode 100644 index 000000000..2d599b6e6 --- /dev/null +++ b/docs-src/components/text.docs.outline.md @@ -0,0 +1,135 @@ +--- +kind: code-backed +group: text +title: Text +description: Text with a typography role, rendered as an element you choose +sources: + - packages/core/src/components/text/** +--- + +# Text + +The `Text` component applies a [typography role](../concepts/styling-theming.md#typography) to its content. A role sets font size, line height, font weight, and letter spacing together. The role does not set the color, and it does not choose the HTML element. Use the `render` prop to choose the element (Base UI render pattern). + +## Import + +```tsx +import { Text } from "@tailor-platform/app-shell"; +``` + +## Basic Usage + + + + + +All standard HTML attributes are also accepted. Put an attribute that belongs to one element, such as `htmlFor` on a `label`, on the `render` element: + +```tsx +}> + Quantity + +``` + +Some lint rules report this example; see [Lint](#lint). + +## Roles + +`variant` takes one of the ten roles. Each role renders a default element when you do not pass `render`: + +| `variant` | Default element | Use | +| ----------------- | --------------- | ----------------------------------------------- | +| `heading-lg` | `h2` | Page title (`Layout.Header`) | +| `heading-md` | `h2` | Section heading, card title | +| `heading-sm` | `h2` | Sub-section heading | +| `body-md` | `p` | Body copy, descriptions (default role) | +| `body-sm` | `p` | Caption, timestamp, metadata | +| `body-md-relaxed` | `p` | Long text, such as a chat message or a document | +| `body-sm-relaxed` | `p` | Long text at the small size | +| `label-md` | `span` | Field label, short UI text, figures in a table | +| `label-sm` | `span` | Small label, badge text | +| `code-sm` | `span` | ID and code. `Text` adds `font-mono` for you | + + + +The sizes and line heights are in the [Typography section of the styling guide](../concepts/styling-theming.md#typography). + +## Heading Level + +All three heading roles render `h2` by default. The heading level comes from the document structure, not from the visual size, so set it with `render`: + +```tsx +}> + Line items + +``` + +To use a heading role for text that is not a heading, render a `div` or a `span`: + +```tsx +}> + 1,240 orders + +``` + +## Lint + +Some `jsx-a11y` rules check that an element has text inside it, for example `heading-has-content` for `

` and `label-has-associated-control` for `