From 7aed1b4b8f6b7699a407251f1b5a675cf30def73 Mon Sep 17 00:00:00 2001 From: Hiroki Uemura Date: Mon, 12 Oct 2026 17:23:28 +0900 Subject: [PATCH 1/4] feat(text): add Text component with typography role variants Text applies one of the ten typography roles with the variant prop. Heading roles render h2, body roles render p, and label and code roles render span. The render prop (Base UI) sets the element, such as the heading level. Color and modifiers stay in className. Add a compile test that a consumer leading-none, font-medium, or tracking class overrides a prefixed role. Co-Authored-By: Claude Opus 5.5 --- .changeset/text-role-variants.md | 12 ++ .../src__components__text__text.test.tsx.snap | 9 ++ packages/core/src/components/text/index.ts | 1 + .../core/src/components/text/text.test.tsx | 136 ++++++++++++++++++ packages/core/src/components/text/text.tsx | 83 +++++++++++ packages/core/src/index.ts | 1 + .../core/tests/typography-compile.test.ts | 23 +++ 7 files changed, 265 insertions(+) create mode 100644 .changeset/text-role-variants.md create mode 100644 packages/core/__snapshots__/src__components__text__text.test.tsx.snap create mode 100644 packages/core/src/components/text/index.ts create mode 100644 packages/core/src/components/text/text.test.tsx create mode 100644 packages/core/src/components/text/text.tsx 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/packages/core/__snapshots__/src__components__text__text.test.tsx.snap b/packages/core/__snapshots__/src__components__text__text.test.tsx.snap new file mode 100644 index 000000000..a9b4e38ea --- /dev/null +++ b/packages/core/__snapshots__/src__components__text__text.test.tsx.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Text > snapshots > code role 1`] = `"PO-0001"`; + +exports[`Text > snapshots > default role 1`] = `"

Orders sent this month.

"`; + +exports[`Text > snapshots > heading role 1`] = `"

Purchase orders

"`; + +exports[`Text > snapshots > with render prop 1`] = `"

Line items

"`; diff --git a/packages/core/src/components/text/index.ts b/packages/core/src/components/text/index.ts new file mode 100644 index 000000000..e20cd3f8a --- /dev/null +++ b/packages/core/src/components/text/index.ts @@ -0,0 +1 @@ +export * from "./text"; diff --git a/packages/core/src/components/text/text.test.tsx b/packages/core/src/components/text/text.test.tsx new file mode 100644 index 000000000..4d54a9303 --- /dev/null +++ b/packages/core/src/components/text/text.test.tsx @@ -0,0 +1,136 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import * as React from "react"; +import { afterEach, describe, expect, it } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import { Text, type TextProps } from "./text"; + +afterEach(() => { + cleanup(); +}); + +type TextVariant = NonNullable; + +const defaultTheme = readFileSync(join(__dirname, "../../assets/themes/default.css"), "utf8"); +const themeRoles = [ + ...new Set([...defaultTheme.matchAll(/^\s*--app-shell-type-([a-z-]+)-size:/gm)].map((m) => m[1])), +]; + +describe("Text", () => { + // ========================================================================== + // Snapshots — verify full DOM structure for representative roles + // ========================================================================== + + describe("snapshots", () => { + it("default role", () => { + const { container } = render(Orders sent this month.); + expect(container.innerHTML).toMatchSnapshot(); + }); + + it("heading role", () => { + const { container } = render(Purchase orders); + expect(container.innerHTML).toMatchSnapshot(); + }); + + it("code role", () => { + const { container } = render(PO-0001); + expect(container.innerHTML).toMatchSnapshot(); + }); + + it("with render prop", () => { + const { container } = render( + // oxlint-disable-next-line jsx-a11y/heading-has-content -- Text passes the children to the rendered h3 + }> + Line items + , + ); + expect(container.innerHTML).toMatchSnapshot(); + }); + }); + + it("has a variant for every role in the default palette", () => { + for (const role of themeRoles) { + const { container } = render(x); + expect(container.firstElementChild?.className, role).toContain(`astw:text-${role}`); + cleanup(); + } + }); + + it.each([ + ["heading-lg", "H2"], + ["heading-md", "H2"], + ["heading-sm", "H2"], + ["body-md", "P"], + ["body-sm", "P"], + ["body-md-relaxed", "P"], + ["body-sm-relaxed", "P"], + ["label-md", "SPAN"], + ["label-sm", "SPAN"], + ["code-sm", "SPAN"], + ] as const)("renders %s as %s by default", (variant, tagName) => { + const { container } = render(x); + expect(container.firstElementChild?.tagName).toBe(tagName); + }); + + it("renders body-md as a paragraph when no variant is given", () => { + const { container } = render(x); + const element = container.firstElementChild; + expect(element?.tagName).toBe("P"); + expect(element?.className).toContain("astw:text-body-md"); + }); + + it("changes the element but keeps the role with the render prop", () => { + render( + // oxlint-disable-next-line jsx-a11y/heading-has-content -- Text passes the children to the rendered h4 + }> + Shipping + , + ); + const heading = screen.getByRole("heading", { level: 4 }); + expect(heading.textContent).toBe("Shipping"); + expect(heading.className).toContain("astw:text-heading-sm"); + }); + + it("adds font-mono to code-sm", () => { + const { container } = render(PO-0001); + expect(container.firstElementChild?.className).toContain("astw:font-mono"); + }); + + it("keeps the role next to color and modifier classes", () => { + const { container } = render( + + Total + , + ); + const classes = container.firstElementChild?.className.split(" "); + expect(classes).toEqual( + expect.arrayContaining([ + "astw:text-heading-md", + "text-muted-foreground", + "leading-none", + "tabular-nums", + ]), + ); + }); + + it("sets data-slot and passes other props through", () => { + render( + + x + , + ); + const element = document.getElementById("total"); + expect(element?.getAttribute("data-slot")).toBe("text"); + expect(element?.getAttribute("aria-live")).toBe("polite"); + }); + + it("forwards the ref to the rendered element", () => { + const ref = React.createRef(); + render( + + x + , + ); + expect(ref.current?.tagName).toBe("H2"); + }); +}); diff --git a/packages/core/src/components/text/text.tsx b/packages/core/src/components/text/text.tsx new file mode 100644 index 000000000..1960a2c38 --- /dev/null +++ b/packages/core/src/components/text/text.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { useRender } from "@base-ui/react/use-render"; + +import { cn } from "@/lib/utils"; + +const textVariants = cva("", { + variants: { + /** Typography role. Sets font size, line height, font weight, and letter spacing together. */ + variant: { + "heading-lg": "astw:text-heading-lg", + "heading-md": "astw:text-heading-md", + "heading-sm": "astw:text-heading-sm", + "body-md": "astw:text-body-md", + "body-sm": "astw:text-body-sm", + "label-md": "astw:text-label-md", + "label-sm": "astw:text-label-sm", + "code-sm": "astw:font-mono astw:text-code-sm", + "body-md-relaxed": "astw:text-body-md-relaxed", + "body-sm-relaxed": "astw:text-body-sm-relaxed", + }, + }, + defaultVariants: { + variant: "body-md", + }, +}); + +type TextVariant = NonNullable["variant"]>; + +/** + * The element each role renders when no `render` prop is given. All heading + * roles render `h2`: the heading level comes from the document structure, not + * from the visual size, so pick the level with `render`. + */ +const defaultTagNames = { + "heading-lg": "h2", + "heading-md": "h2", + "heading-sm": "h2", + "body-md": "p", + "body-sm": "p", + "label-md": "span", + "label-sm": "span", + "code-sm": "span", + "body-md-relaxed": "p", + "body-sm-relaxed": "p", +} as const satisfies Record; + +type TextProps = React.HTMLAttributes & + VariantProps & { + /** Ref to the rendered element. */ + ref?: React.Ref; + /** Custom element to render as (Base UI render prop). Use it to set the heading level, for example `render={

}`. */ + render?: React.ReactElement; + }; + +/** + * Text with a typography role. The role sets the size, line height, weight, + * and letter spacing. It does not set the color or choose the heading level. + * + * @example + * ```tsx + * import { Text } from "@tailor-platform/app-shell"; + * + * }>Line items + * Orders sent this month. + * 1,240 + * ``` + */ +function Text({ className, variant, render, children, ref, ...props }: TextProps) { + return useRender({ + defaultTagName: defaultTagNames[variant ?? "body-md"], + render, + ref, + props: { + "data-slot": "text", + className: cn(textVariants({ variant, className })), + children, + ...props, + }, + }); +} + +export { Text, textVariants, type TextProps }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9c9783153..89d930049 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -190,6 +190,7 @@ export { export { Layout, type LayoutProps } from "./components/layout"; export { Grid, type GridProps, type GridItemProps } from "./components/grid"; export { Button, buttonVariants, type ButtonProps } from "./components/button"; +export { Text, textVariants, type TextProps } from "./components/text"; export { Avatar, avatarVariants, type AvatarProps } from "./components/avatar"; export { Spinner, type SpinnerProps } from "./components/spinner"; export { Input, type InputProps } from "./components/input"; diff --git a/packages/core/tests/typography-compile.test.ts b/packages/core/tests/typography-compile.test.ts index 75fc503f3..0aa887d4f 100644 --- a/packages/core/tests/typography-compile.test.ts +++ b/packages/core/tests/typography-compile.test.ts @@ -81,6 +81,29 @@ describe("typography role utilities", () => { expect(declarationsOf(css, ".text-label-md").get("line-height")).toMatch(/^var\(--tw-leading,/); }); + it("lets a consumer modifier override a role from AppShell's prefixed build", async () => { + // `` puts AppShell's `astw:text-*` and the + // consumer's unprefixed modifier on one element. The prefixed sheet loads later and + // wins the cascade, so the modifier works only because both builds share the + // unprefixed `--tw-*` variables. + const library = await compile('@import "tailwindcss" prefix(astw) source(none);', [ + "astw:text-heading-md", + ]); + const consumer = await compile('@import "tailwindcss" source(none);', [ + "leading-none", + "font-medium", + "tracking-wide", + ]); + + const role = declarationsOf(library, ".astw\\:text-heading-md"); + expect(role.get("line-height")).toMatch(/^var\(--tw-leading,/); + expect(role.get("font-weight")).toMatch(/^var\(--tw-font-weight,/); + expect(role.get("letter-spacing")).toMatch(/^var\(--tw-tracking,/); + expect(declarationsOf(consumer, ".leading-none").get("--tw-leading")).toBe("1"); + expect(declarationsOf(consumer, ".font-medium").has("--tw-font-weight")).toBe(true); + expect(declarationsOf(consumer, ".tracking-wide").has("--tw-tracking")).toBe(true); + }); + it("emits every role defined in the default palette", async () => { const roles = [ ...new Set( From 850a05ad193478bf76e8c0b4a6a13a22bcf55c04 Mon Sep 17 00:00:00 2001 From: Hiroki Uemura Date: Mon, 12 Oct 2026 17:23:32 +0900 Subject: [PATCH 2/4] docs(text): document Text and the typography convention for components Add the Text docs page with examples, mention Text in the Typography section of the styling guide, add typography rules to the component design reference, and record the Text decisions in decisions/typography-roles.md. Co-Authored-By: Claude Opus 5.5 --- .agents/references/component-design.md | 21 ++ decisions/typography-roles.md | 13 +- docs-manifest.json | 26 ++- docs-src/components/text.docs.examples.tsx | 51 +++++ docs-src/components/text.docs.outline.md | 123 +++++++++++ .../concepts/styling-theming.docs.outline.md | 9 +- docs/components/text.md | 193 ++++++++++++++++++ docs/concepts/styling-theming.md | 9 +- 8 files changed, 438 insertions(+), 7 deletions(-) create mode 100644 docs-src/components/text.docs.examples.tsx create mode 100644 docs-src/components/text.docs.outline.md create mode 100644 docs/components/text.md diff --git a/.agents/references/component-design.md b/.agents/references/component-design.md index a2ba6c9de..2c31b69a4 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. Do not hard-wire `

` to a size; let the consumer choose the level where the component can sit at different depths. + +```tsx +// Good +
{title}
+{updatedAt} +``` + +```tsx +// Bad: a raw size + weight combination, and a size tied to a heading level +

{title}

+``` + ### Container queries Prefer Tailwind container-query utilities with arbitrary values. diff --git a/decisions/typography-roles.md b/decisions/typography-roles.md index 400702edd..d32a5909f 100644 --- a/decisions/typography-roles.md +++ b/decisions/typography-roles.md @@ -84,5 +84,16 @@ 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. +- **`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 163a15912..934405909 100644 --- a/docs-manifest.json +++ b/docs-manifest.json @@ -1287,9 +1287,9 @@ "symbols": [], "hashes": { "typeSurface": null, - "outline": "e63d837190e71ff1", + "outline": "59aedd105f98f661", "snapshot": null, - "outputMd": "112b43c697d30369", + "outputMd": "47a1c9e851eaae86", "examples": "4d19f4f688e0f0ac" } }, @@ -1327,6 +1327,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", "textVariants"], + "hashes": { + "typeSurface": "8775dcc47909645d", + "outline": "a5834d13dfd3b79d", + "snapshot": "f5d9429ab497863a", + "outputMd": "4f157b7c8b0e188c", + "examples": "6137a257a21e5748" + } + }, "textarea": { "slug": "textarea", "kind": "code-backed", @@ -1659,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": "dc6b3afc049b4148", - "packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "112b43c697d30369", + "packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "47a1c9e851eaae86", "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", @@ -1698,6 +1715,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": "4f157b7c8b0e188c", "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", @@ -1739,6 +1757,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": "12527965a47cf42f" + "packages/core/skills/app-shell-patterns/SKILL.md": "a29a1573faccfb73" } } diff --git a/docs-src/components/text.docs.examples.tsx b/docs-src/components/text.docs.examples.tsx new file mode 100644 index 000000000..d4df25635 --- /dev/null +++ b/docs-src/components/text.docs.examples.tsx @@ -0,0 +1,51 @@ +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. + + } className="leading-none"> + Card title + + + 1,240.00 + + + Status + +
+ ); +} diff --git a/docs-src/components/text.docs.outline.md b/docs-src/components/text.docs.outline.md new file mode 100644 index 000000000..442e25fe2 --- /dev/null +++ b/docs-src/components/text.docs.outline.md @@ -0,0 +1,123 @@ +--- +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. + +## 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 + +``` + +If your app enables the `jsx-a11y/heading-has-content` lint rule, it reports `render={

}` as an empty heading, because it does not see that `Text` passes its children to that element. Disable the rule for that line: + +```tsx +// oxlint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children +}> + Line items + +``` + +To use a heading role for text that is not a heading, render a `div` or a `span`: + +```tsx +}> + 1,240 orders + +``` + +## Color and Modifiers + +Set the color with a color token in `className`. Four modifiers may also be combined with a role: `leading-none` for single-line text, `tabular-nums`, `font-medium` or `font-semibold` for emphasis, and `uppercase`. + + + +```tsx + + Orders sent this month. + +} className="leading-none"> + Card title + + + 1,240.00 + +``` + +Do not combine a role with other `leading-*`, `tracking-*`, or `text-*` size values. Pick a different `variant` instead. + +## Using Roles Without Text + +Each role is also a plain utility, for example `text-heading-md`. Use the utility when you style your own element and do not need `Text`: + +```tsx +Amount +``` + +## TypeScript + +```typescript +import { type TextProps } from "@tailor-platform/app-shell"; +``` + +## Accessibility + +- A heading role renders a real heading (`h2`) by default, so screen readers list it with the other headings. Set the level with `render` so the levels follow the page structure. +- Do not choose a heading level for its size. `Text` keeps the size and the level separate. +- `body-*` roles render `p`. Inside another `p`, render a `span` instead, because a `p` cannot contain a `p`. + +## Palettes + +The `cream` and `bloom` palettes set the line height and letter spacing of `h1` to `h6` elements. Under these palettes, a heading role on a heading element uses the palette values (line height 1.2, letter spacing `-0.03em`), including when you add `leading-none`. The same role on a `div` or `span` uses the role values. See the [Typography section of the styling guide](../concepts/styling-theming.md#typography). + +## Related Components + +- [Card](./card.md) - Card titles use the `heading-md` role +- [Layout](./layout.md) - The page title uses the `heading-lg` role diff --git a/docs-src/concepts/styling-theming.docs.outline.md b/docs-src/concepts/styling-theming.docs.outline.md index c2d5d721e..8bd807a97 100644 --- a/docs-src/concepts/styling-theming.docs.outline.md +++ b/docs-src/concepts/styling-theming.docs.outline.md @@ -286,12 +286,19 @@ Pair a role with a color token from the Color section above. These pairings are Updated 2h ago ``` +The [`Text`](../components/text.md) component applies the same roles with a `variant` prop. It renders a default element for each role (`h2` for headings, `p` for body text, `span` for the others), and its `render` prop sets the element, such as the heading level: + +```tsx +}>Line items +Description copy +``` + On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's line height and letter spacing, including `leading-none`; see "Change a role" below. AppShell's own components still use the stock utilities (`text-sm`, `font-medium`, and so on) today. They will move to the roles in later changes, so the two sets can appear side by side for now. **Rules** - **A role carries no color.** Set the color with a token such as `text-muted-foreground`. -- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size. +- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size. With `Text`, set the element with `render`. - **A role sets weight and letter spacing on its own element.** It does not inherit them from a parent. Put an emphasis or tracking class on the element that has the role, not on its parent. - **Four modifiers may be combined with a role.** All are stock Tailwind utilities: - Compact line height: `leading-none`. Use it only for single-line text, such as a card title. Write it after the role when you merge with `cn()`. diff --git a/docs/components/text.md b/docs/components/text.md new file mode 100644 index 000000000..05450d5a1 --- /dev/null +++ b/docs/components/text.md @@ -0,0 +1,193 @@ +--- +title: Text +description: Text with a typography role, rendered as an element you choose +--- + + + +# 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 + + + +```tsx +function BasicUsage() { + return ( +
+ Purchase orders + Orders sent to suppliers this month. +
+ ); +} +``` + +## Props + +_Generated from the type surface — first-party props only._ + +### TextProps + +| Prop | Type | Default | Description | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | +| `ref?` | `React.Ref` | - | Ref to the rendered element. | +| `render?` | `React.ReactElement` | - | Custom element to render as (Base UI render prop). Use it to set the heading level, for example `render={

}`. | +| `variant?` | `"heading-lg" \| "heading-md" \| "heading-sm" \| "body-md" \| "body-sm" \| "label-md" \| "label-sm" \| "code-sm" \| "body-md-relaxed" \| "body-sm-relaxed" \| null` | `"body-md"` | Typography role. Sets font size, line height, font weight, and letter spacing together. | + +## Exports + +- `Text` — function +- `TextProps` — type +- `textVariants` — const + +All standard HTML attributes are also accepted. + +## 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 | + + + +```tsx +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 +
+ ); +} +``` + +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 + +``` + +If your app enables the `jsx-a11y/heading-has-content` lint rule, it reports `render={

}` as an empty heading, because it does not see that `Text` passes its children to that element. Disable the rule for that line: + +```tsx +// oxlint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children +}> + Line items + +``` + +To use a heading role for text that is not a heading, render a `div` or a `span`: + +```tsx +}> + 1,240 orders + +``` + +## Color and Modifiers + +Set the color with a color token in `className`. Four modifiers may also be combined with a role: `leading-none` for single-line text, `tabular-nums`, `font-medium` or `font-semibold` for emphasis, and `uppercase`. + + + +```tsx +function ColorAndModifiers() { + return ( +
+ + Orders sent this month. + + } className="leading-none"> + Card title + + + 1,240.00 + + + Status + +
+ ); +} +``` + +```tsx + + Orders sent this month. + +} className="leading-none"> + Card title + + + 1,240.00 + +``` + +Do not combine a role with other `leading-*`, `tracking-*`, or `text-*` size values. Pick a different `variant` instead. + +## Using Roles Without Text + +Each role is also a plain utility, for example `text-heading-md`. Use the utility when you style your own element and do not need `Text`: + +```tsx +Amount +``` + +## TypeScript + +```typescript +import { type TextProps } from "@tailor-platform/app-shell"; +``` + +## Accessibility + +- A heading role renders a real heading (`h2`) by default, so screen readers list it with the other headings. Set the level with `render` so the levels follow the page structure. +- Do not choose a heading level for its size. `Text` keeps the size and the level separate. +- `body-*` roles render `p`. Inside another `p`, render a `span` instead, because a `p` cannot contain a `p`. + +## Palettes + +The `cream` and `bloom` palettes set the line height and letter spacing of `h1` to `h6` elements. Under these palettes, a heading role on a heading element uses the palette values (line height 1.2, letter spacing `-0.03em`), including when you add `leading-none`. The same role on a `div` or `span` uses the role values. See the [Typography section of the styling guide](../concepts/styling-theming.md#typography). + +## Related Components + +- [Card](./card.md) - Card titles use the `heading-md` role +- [Layout](./layout.md) - The page title uses the `heading-lg` role diff --git a/docs/concepts/styling-theming.md b/docs/concepts/styling-theming.md index 61a21f8dc..046d6b204 100644 --- a/docs/concepts/styling-theming.md +++ b/docs/concepts/styling-theming.md @@ -401,12 +401,19 @@ function TypographyRoles() { Updated 2h ago ``` +The [`Text`](../components/text.md) component applies the same roles with a `variant` prop. It renders a default element for each role (`h2` for headings, `p` for body text, `span` for the others), and its `render` prop sets the element, such as the heading level: + +```tsx +}>Line items +Description copy +``` + On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's line height and letter spacing, including `leading-none`; see "Change a role" below. AppShell's own components still use the stock utilities (`text-sm`, `font-medium`, and so on) today. They will move to the roles in later changes, so the two sets can appear side by side for now. **Rules** - **A role carries no color.** Set the color with a token such as `text-muted-foreground`. -- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size. +- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size. With `Text`, set the element with `render`. - **A role sets weight and letter spacing on its own element.** It does not inherit them from a parent. Put an emphasis or tracking class on the element that has the role, not on its parent. - **Four modifiers may be combined with a role.** All are stock Tailwind utilities: - Compact line height: `leading-none`. Use it only for single-line text, such as a card title. Write it after the role when you merge with `cn()`. From 8b05d8b35df2d518bdef6da7028abbd97a4546b0 Mon Sep 17 00:00:00 2001 From: Hiroki Uemura Date: Mon, 12 Oct 2026 17:51:08 +0900 Subject: [PATCH 3/4] fix(text): resolve a null variant and correct the docs from review Treat variant={null} as body-md instead of rendering a p with no role. Use eslint-disable comments, which ESLint and oxlint both read. Keep the card title as h3 in the component design example, state that core components do not use the roles yet, update the ADR header, show where element-specific attributes go, and drop the ad-hoc overline example. Co-Authored-By: Claude Opus 5.5 --- .agents/references/component-design.md | 6 +++--- decisions/typography-roles.md | 4 ++-- docs-manifest.json | 8 +++---- docs-src/components/text.docs.examples.tsx | 3 --- docs-src/components/text.docs.outline.md | 18 +++++++++++----- docs/components/text.md | 21 ++++++++++++------- .../core/src/components/text/text.test.tsx | 11 ++++++++-- packages/core/src/components/text/text.tsx | 6 ++++-- 8 files changed, 48 insertions(+), 29 deletions(-) diff --git a/.agents/references/component-design.md b/.agents/references/component-design.md index 2c31b69a4..a5bc868e1 100644 --- a/.agents/references/component-design.md +++ b/.agents/references/component-design.md @@ -435,16 +435,16 @@ Text that a component renders takes its size, line height, weight, and letter sp - 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. Do not hard-wire `

` to a size; let the consumer choose the level where the component can sit at different depths. +- 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}
+

{title}

{updatedAt} ``` ```tsx -// Bad: a raw size + weight combination, and a size tied to a heading level +// Bad: a raw size + weight combination and a line height outside the roles

{title}

``` diff --git a/decisions/typography-roles.md b/decisions/typography-roles.md index d32a5909f..c95356463 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 diff --git a/docs-manifest.json b/docs-manifest.json index 934405909..7d0936c99 100644 --- a/docs-manifest.json +++ b/docs-manifest.json @@ -1338,10 +1338,10 @@ "symbols": ["Text", "TextProps", "textVariants"], "hashes": { "typeSurface": "8775dcc47909645d", - "outline": "a5834d13dfd3b79d", + "outline": "1a42fbcfd81a2df3", "snapshot": "f5d9429ab497863a", - "outputMd": "4f157b7c8b0e188c", - "examples": "6137a257a21e5748" + "outputMd": "efd4a7a0e4049d76", + "examples": "c09e64bcc10c13c7" } }, "textarea": { @@ -1715,7 +1715,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": "4f157b7c8b0e188c", + "packages/core/skills/app-shell-patterns/references/components/text.md": "efd4a7a0e4049d76", "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", diff --git a/docs-src/components/text.docs.examples.tsx b/docs-src/components/text.docs.examples.tsx index d4df25635..371db20a0 100644 --- a/docs-src/components/text.docs.examples.tsx +++ b/docs-src/components/text.docs.examples.tsx @@ -43,9 +43,6 @@ export function ColorAndModifiers() { 1,240.00 - - Status -

); } diff --git a/docs-src/components/text.docs.outline.md b/docs-src/components/text.docs.outline.md index 442e25fe2..984a1cbe0 100644 --- a/docs-src/components/text.docs.outline.md +++ b/docs-src/components/text.docs.outline.md @@ -23,7 +23,13 @@ import { Text } from "@tailor-platform/app-shell"; -All standard HTML attributes are also accepted. +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 + +``` ## Roles @@ -56,10 +62,10 @@ All three heading roles render `h2` by default. The heading level comes from the
``` -If your app enables the `jsx-a11y/heading-has-content` lint rule, it reports `render={

}` as an empty heading, because it does not see that `Text` passes its children to that element. Disable the rule for that line: +If your app enables the `jsx-a11y/heading-has-content` lint rule, it reports `render={

}` as an empty heading, because it does not see that `Text` passes its children to that element. Disable the rule for that line. ESLint and oxlint both read this comment: ```tsx -// oxlint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children +// eslint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children }> Line items @@ -119,5 +125,7 @@ The `cream` and `bloom` palettes set the line height and letter spacing of `h1` ## Related Components -- [Card](./card.md) - Card titles use the `heading-md` role -- [Layout](./layout.md) - The page title uses the `heading-lg` role +- [Card](./card.md) - The table above lists `heading-md` for card titles +- [Layout](./layout.md) - The table above lists `heading-lg` for the page title + +AppShell's own components, including these two, still use the stock utilities. They will move to the roles in a later change. diff --git a/docs/components/text.md b/docs/components/text.md index 05450d5a1..a324f2ef8 100644 --- a/docs/components/text.md +++ b/docs/components/text.md @@ -48,7 +48,13 @@ _Generated from the type surface — first-party props only._ - `TextProps` — type - `textVariants` — const -All standard HTML attributes are also accepted. +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 + +``` ## Roles @@ -105,10 +111,10 @@ All three heading roles render `h2` by default. The heading level comes from the ``` -If your app enables the `jsx-a11y/heading-has-content` lint rule, it reports `render={

}` as an empty heading, because it does not see that `Text` passes its children to that element. Disable the rule for that line: +If your app enables the `jsx-a11y/heading-has-content` lint rule, it reports `render={

}` as an empty heading, because it does not see that `Text` passes its children to that element. Disable the rule for that line. ESLint and oxlint both read this comment: ```tsx -// oxlint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children +// eslint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children }> Line items @@ -141,9 +147,6 @@ function ColorAndModifiers() { 1,240.00 - - Status - ); } @@ -189,5 +192,7 @@ The `cream` and `bloom` palettes set the line height and letter spacing of `h1` ## Related Components -- [Card](./card.md) - Card titles use the `heading-md` role -- [Layout](./layout.md) - The page title uses the `heading-lg` role +- [Card](./card.md) - The table above lists `heading-md` for card titles +- [Layout](./layout.md) - The table above lists `heading-lg` for the page title + +AppShell's own components, including these two, still use the stock utilities. They will move to the roles in a later change. diff --git a/packages/core/src/components/text/text.test.tsx b/packages/core/src/components/text/text.test.tsx index 4d54a9303..9b569df1a 100644 --- a/packages/core/src/components/text/text.test.tsx +++ b/packages/core/src/components/text/text.test.tsx @@ -39,7 +39,7 @@ describe("Text", () => { it("with render prop", () => { const { container } = render( - // oxlint-disable-next-line jsx-a11y/heading-has-content -- Text passes the children to the rendered h3 + // eslint-disable-next-line jsx-a11y/heading-has-content -- Text passes the children to the rendered h3 }> Line items , @@ -79,9 +79,16 @@ describe("Text", () => { expect(element?.className).toContain("astw:text-body-md"); }); + it("treats a null variant as the default role", () => { + const { container } = render(x); + const element = container.firstElementChild; + expect(element?.tagName).toBe("P"); + expect(element?.className).toContain("astw:text-body-md"); + }); + it("changes the element but keeps the role with the render prop", () => { render( - // oxlint-disable-next-line jsx-a11y/heading-has-content -- Text passes the children to the rendered h4 + // eslint-disable-next-line jsx-a11y/heading-has-content -- Text passes the children to the rendered h4 }> Shipping , diff --git a/packages/core/src/components/text/text.tsx b/packages/core/src/components/text/text.tsx index 1960a2c38..664e86894 100644 --- a/packages/core/src/components/text/text.tsx +++ b/packages/core/src/components/text/text.tsx @@ -67,13 +67,15 @@ type TextProps = React.HTMLAttributes & * ``` */ function Text({ className, variant, render, children, ref, ...props }: TextProps) { + // cva returns no class for an explicit `null`, so resolve the default here. + const role = variant ?? "body-md"; return useRender({ - defaultTagName: defaultTagNames[variant ?? "body-md"], + defaultTagName: defaultTagNames[role], render, ref, props: { "data-slot": "text", - className: cn(textVariants({ variant, className })), + className: cn(textVariants({ variant: role, className })), children, ...props, }, From f35252b3d78ece4df4f7f12e13b2b061c6a56be8 Mon Sep 17 00:00:00 2001 From: Hiroki Uemura Date: Mon, 12 Oct 2026 18:03:45 +0900 Subject: [PATCH 4/4] refactor(text): drop the textVariants export and align card title guidance A role can already be applied with the text- utility or with Text, so textVariants stays internal. Generalize the docs lint note to the jsx-a11y rules that check element content, and keep card titles as headings in the Text docs and the styling guide pairing table. Co-Authored-By: Claude Opus 5.5 --- decisions/typography-roles.md | 1 + docs-manifest.json | 18 +++++++------- docs-src/components/text.docs.examples.tsx | 3 --- docs-src/components/text.docs.outline.md | 20 +++++++++------- .../concepts/styling-theming.docs.outline.md | 2 +- docs/components/text.md | 24 +++++++++---------- docs/concepts/styling-theming.md | 2 +- packages/core/src/components/text/text.tsx | 4 ++-- packages/core/src/index.ts | 2 +- 9 files changed, 39 insertions(+), 37 deletions(-) diff --git a/decisions/typography-roles.md b/decisions/typography-roles.md index c95356463..23ef6a41e 100644 --- a/decisions/typography-roles.md +++ b/decisions/typography-roles.md @@ -95,5 +95,6 @@ Limit: this does not let a role replace a size that core sets on its own element - **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 7d0936c99..636ac5614 100644 --- a/docs-manifest.json +++ b/docs-manifest.json @@ -1287,9 +1287,9 @@ "symbols": [], "hashes": { "typeSurface": null, - "outline": "59aedd105f98f661", + "outline": "8ad61f58de459be2", "snapshot": null, - "outputMd": "47a1c9e851eaae86", + "outputMd": "c40075c1f34fb315", "examples": "4d19f4f688e0f0ac" } }, @@ -1335,13 +1335,13 @@ "examples": "docs-src/components/text.docs.examples.tsx", "sources": ["packages/core/src/components/text/**"], "claims": [], - "symbols": ["Text", "TextProps", "textVariants"], + "symbols": ["Text", "TextProps"], "hashes": { - "typeSurface": "8775dcc47909645d", - "outline": "1a42fbcfd81a2df3", + "typeSurface": "ed54a16ca677dbe3", + "outline": "b354c6be5f81ce2a", "snapshot": "f5d9429ab497863a", - "outputMd": "efd4a7a0e4049d76", - "examples": "c09e64bcc10c13c7" + "outputMd": "6436f68b6b404f0b", + "examples": "4a832708dc79474b" } }, "textarea": { @@ -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": "dc6b3afc049b4148", - "packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "47a1c9e851eaae86", + "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", @@ -1715,7 +1715,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": "efd4a7a0e4049d76", + "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", diff --git a/docs-src/components/text.docs.examples.tsx b/docs-src/components/text.docs.examples.tsx index 371db20a0..fbf44b839 100644 --- a/docs-src/components/text.docs.examples.tsx +++ b/docs-src/components/text.docs.examples.tsx @@ -37,9 +37,6 @@ export function ColorAndModifiers() { Orders sent this month. - } className="leading-none"> - Card title - 1,240.00 diff --git a/docs-src/components/text.docs.outline.md b/docs-src/components/text.docs.outline.md index 984a1cbe0..2d599b6e6 100644 --- a/docs-src/components/text.docs.outline.md +++ b/docs-src/components/text.docs.outline.md @@ -31,6 +31,8 @@ All standard HTML attributes are also accepted. Put an attribute that belongs to ``` +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`: @@ -62,20 +64,22 @@ All three heading roles render `h2` by default. The heading level comes from the ``` -If your app enables the `jsx-a11y/heading-has-content` lint rule, it reports `render={

}` as an empty heading, because it does not see that `Text` passes its children to that element. Disable the rule for that line. ESLint and oxlint both read this comment: +To use a heading role for text that is not a heading, render a `div` or a `span`: ```tsx -// eslint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children -}> - Line items +}> + 1,240 orders ``` -To use a heading role for text that is not a heading, render a `div` or a `span`: +## 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 `