@tailor-platform/app-shell 1.17.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/app-shell-core.css +1 -1
  2. package/dist/app-shell.d.ts +503 -137
  3. package/dist/app-shell.js +8847 -7378
  4. package/dist/{appshell-CahhZy7v.js → appshell-DQTz-QwK.js} +489 -379
  5. package/dist/style-entry.d.ts +1 -656
  6. package/dist/testing.d.ts +118 -113
  7. package/dist/testing.js +1 -1
  8. package/dist/theme.bridge.css +127 -40
  9. package/dist/theme.css +4 -3
  10. package/dist/themes/_template.css +5 -6
  11. package/dist/themes/bloom.css +0 -2
  12. package/dist/themes/cream.css +0 -2
  13. package/dist/themes/default.css +112 -77
  14. package/dist/vite-plugin.d.ts +118 -113
  15. package/package.json +8 -5
  16. package/skills/app-shell-patterns/SKILL.md +51 -32
  17. package/skills/app-shell-patterns/references/api/aggregations.md +131 -0
  18. package/skills/app-shell-patterns/references/api/define-i18n-labels.md +123 -91
  19. package/skills/app-shell-patterns/references/api/define-module.md +2 -4
  20. package/skills/app-shell-patterns/references/components/action-panel.md +55 -23
  21. package/skills/app-shell-patterns/references/components/badge.md +7 -3
  22. package/skills/app-shell-patterns/references/components/data-table.md +724 -36
  23. package/skills/app-shell-patterns/references/components/form.md +19 -9
  24. package/skills/app-shell-patterns/references/components/item-list.md +191 -0
  25. package/skills/app-shell-patterns/references/components/select.md +10 -0
  26. package/skills/app-shell-patterns/references/components/sidebar-group.md +6 -6
  27. package/skills/app-shell-patterns/references/components/text.md +235 -0
  28. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +16 -14
  29. package/skills/app-shell-patterns/references/concepts/styling-theming.md +46 -22
  30. package/skills/app-shell-patterns/references/migrations.md +23 -2
  31. package/skills/app-shell-patterns/references/pages/document-detail.md +36 -25
  32. package/skills/app-shell-patterns/references/patterns/form-modal.md +4 -1
  33. package/skills/app-shell-patterns/references/patterns/i18n-labels.md +139 -0
  34. package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +3 -2
  35. package/dist/semantic-contrast.test.ts +0 -154
  36. package/dist/theme-bridge.test.ts +0 -159
@@ -100,14 +100,14 @@ A compound component that groups all parts of a form field and manages its valid
100
100
 
101
101
  ### Sub-components
102
102
 
103
- | Sub-component | Description |
104
- | ------------------- | --------------------------------------------------------------------------------------------- |
105
- | `Field.Root` | Context boundary for a single field. Manages validation state. |
106
- | `Field.Label` | Accessible label; `htmlFor` is resolved automatically via context. |
107
- | `Field.Control` | Styled `<input>`. Can be omitted when using a Base UI-backed AppShell component as the input. |
108
- | `Field.Description` | Supplementary help text; automatically linked via `aria-describedby`. |
109
- | `Field.Error` | Validation error message. Use `match` to target specific validity states. |
110
- | `Field.Validity` | Render-prop access to the field's `ValidityState` for fully custom validation UI. |
103
+ | Sub-component | Description |
104
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------- |
105
+ | `Field.Root` | Context boundary for a single field. Manages validation state. |
106
+ | `Field.Label` | Accessible label; `htmlFor` is resolved automatically via context. |
107
+ | `Field.Control` | Styled text `<input>`. `Select`, `Combobox`, and the other self-wired controls go directly under `Field.Root` instead. |
108
+ | `Field.Description` | Supplementary help text; automatically linked via `aria-describedby`. |
109
+ | `Field.Error` | Validation error message. Use `match` to target specific validity states. |
110
+ | `Field.Validity` | Render-prop access to the field's `ValidityState` for fully custom validation UI. |
111
111
 
112
112
  ### Field.Root Props
113
113
 
@@ -134,7 +134,17 @@ A compound component that groups all parts of a form field and manages its valid
134
134
 
135
135
  ### Using Another AppShell Component as the Control
136
136
 
137
- `Field.Control` can be omitted when using a Base UI-backed AppShell component (e.g. `Select`, `Combobox`). The component registers itself with the `Field` context automatically, inheriting label association and validation state.
137
+ `Field.Control` is a text input. It is the control for plain text-like fields, and you can pass it a native element such as `render={<textarea />}`. The controls in the last row of the table below go **directly under `Field.Root`**, as a sibling of `Field.Label`. Each one registers itself with the `Field` context, so it gets label association, `aria-describedby`, `disabled`, and validation state without `Field.Control`.
138
+
139
+ | Control | Where it goes |
140
+ | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
141
+ | Plain text, email, number, password, … input | `<Field.Control type="…" />` |
142
+ | Native element (`<textarea>`, `<input type="…">`) | `<Field.Control render={<textarea />} />` |
143
+ | `Select`, `Combobox`, `Autocomplete`, `Checkbox`, `Textarea`, `DateField`, `DatePicker`, `DateRangePicker` (and their `.Async`) | Directly under `Field.Root` — not `Field.Control` |
144
+
145
+ `Input` is the exception. It is a plain `<input>` that does not connect to `Field.Root`, so inside a field use `<Field.Control />` in its place.
146
+
147
+ Do not pass the self-wired controls to `Field.Control`'s `render` prop. They draw their own frame, so `<Field.Control render={<Select />} />` renders a second border around the select. `Field.Control` also passes them `<input>` props they don't take. `Field.Control` logs a console warning when it is given one of these controls directly as an element. A wrapper component or the function form of `render` is not detected.
138
148
 
139
149
  ```tsx
140
150
  const [country, setCountry] = React.useState<string | null>(null);
@@ -0,0 +1,191 @@
1
+ ---
2
+ title: ItemList
3
+ description: Compact icon-and-label lists for consistent navigation inside and outside the Sidebar
4
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/components/item-list.docs.outline.md — do not edit by hand. -->
7
+
8
+ # ItemList
9
+
10
+ `ItemList` presents compact icon-and-label rows for navigation and actions. Use it for in-content navigation, tables of contents, or other lists that should share the Sidebar's list presentation without depending on its layout or providers.
11
+
12
+ It does not require `AppShell`, a router, or a Sidebar provider. Resource metadata, translations, permissions, and active-route matching remain the caller's responsibility.
13
+
14
+ ## Import
15
+
16
+ ```tsx
17
+ import { ItemList } from "@tailor-platform/app-shell";
18
+ ```
19
+
20
+ ## Basic Usage
21
+
22
+ <example-preview name="basic-usage"></example-preview>
23
+
24
+ ```tsx
25
+ function BasicUsage() {
26
+ const [view, setView] = useState("Overview");
27
+ return (
28
+ <>
29
+ <ItemList.Root aria-label="Workspace">
30
+ <ItemList.Item
31
+ icon={<Home />}
32
+ isActive={view === "Overview"}
33
+ onClick={() => setView("Overview")}
34
+ >
35
+ Overview
36
+ </ItemList.Item>
37
+ <ItemList.Separator />
38
+ <ItemList.Section title="Business">
39
+ <ItemList.Item icon={<ShoppingCart />} onClick={() => setView("Orders")}>
40
+ Orders
41
+ </ItemList.Item>
42
+ <ItemList.Group title="Products" icon={<Package />}>
43
+ <ItemList.Item onClick={() => setView("All products")}>All products</ItemList.Item>
44
+ <ItemList.Item onClick={() => setView("Categories")}>Categories</ItemList.Item>
45
+ </ItemList.Group>
46
+ </ItemList.Section>
47
+ </ItemList.Root>
48
+ <output>Current view: {view}</output>
49
+ </>
50
+ );
51
+ }
52
+ ```
53
+
54
+ ## Sub-components
55
+
56
+ | Sub-component | Description |
57
+ | -------------------- | -------------------------------------------------------------------------------- |
58
+ | `ItemList.Root` | A semantic `ul`; accepts standard list props |
59
+ | `ItemList.Item` | A compact icon-and-label row containing a button or supplied interactive element |
60
+ | `ItemList.Group` | A collapsible, indented child list; open by default |
61
+ | `ItemList.Section` | A labeled, non-collapsible list whose items retain their normal indentation |
62
+ | `ItemList.Separator` | A decorative divider that remains a valid list child |
63
+
64
+ ## Items, Links, and Actions
65
+
66
+ `ItemList.Item` is for links and actions, not static rows. It requires `onClick`, `render`, or both; omitting both is a type error, including for a disabled Item. With `onClick`, it renders a `button` with `type="button"` by default. Supply an anchor or application link through `render` for navigation.
67
+
68
+ `render` replaces the interactive element inside the row, not the containing `li`. `children` supplies its label, and `icon` supplies an optional decorative leading icon. Standard button props, classes, attributes, and refs apply to the inner operation. A disabled Item is skipped by Tab and cancels clicks, including navigation through a supplied link.
69
+
70
+ <example-preview name="links-and-actions"></example-preview>
71
+
72
+ ```tsx
73
+ function LinksAndActions() {
74
+ const [checks, setChecks] = useState(0);
75
+ const handleCheckNotifications = () => {
76
+ setChecks((count) => count + 1);
77
+ };
78
+ return (
79
+ <>
80
+ <ItemList.Root aria-label="Quick access">
81
+ <ItemList.Item icon={<Home />} render={<a href="https://example.com/help" />}>
82
+ Help center
83
+ </ItemList.Item>
84
+ <ItemList.Item icon={<Bell />} onClick={handleCheckNotifications}>
85
+ Check notifications
86
+ </ItemList.Item>
87
+ </ItemList.Root>
88
+ <output>Notification checks: {checks}</output>
89
+ </>
90
+ );
91
+ }
92
+ ```
93
+
94
+ For application navigation, use `Link` or `NavLink` from `@tailor-platform/app-shell` inside an AppShell-managed page. Do not import routing APIs directly from `react-router` or create a separate router around the list. The routes in this example belong to the documentation app; use your application's route paths in your own pages.
95
+
96
+ <example-preview name="app-shell-routing"></example-preview>
97
+
98
+ ```tsx
99
+ function AppShellRouting() {
100
+ return (
101
+ <ItemList.Root aria-label="Documentation pages">
102
+ <ItemList.Item render={<Link to="/components/button" />}>Button documentation</ItemList.Item>
103
+ <ItemList.Item render={<NavLink to="/components/item-list" end />}>
104
+ ItemList documentation
105
+ </ItemList.Item>
106
+ </ItemList.Root>
107
+ );
108
+ }
109
+ ```
110
+
111
+ `NavLink` owns route matching and supplies `aria-current="page"` when matched; ItemList styles that attribute with the same emphasis as `isActive`. Use `end` when only the exact destination should be current. With a plain link, the caller supplies `aria-current="page"` when appropriate.
112
+
113
+ `isActive` remains a boolean for explicit visual emphasis, including local actions. It does not imply `aria-current`, selection state, a `listbox` role, or `aria-selected`. ItemList does not perform route matching.
114
+
115
+ ### Item Props
116
+
117
+ | Prop | Type | Default | Description |
118
+ | ----------- | -------------------------------------- | ------- | ------------------------------------------------------------------ |
119
+ | `icon` | `ReactNode` | — | Optional decorative leading icon |
120
+ | `children` | `ReactNode` | — | Compact row label |
121
+ | `render` | `ReactElement` | — | Replaces the inner button; required unless `onClick` is supplied |
122
+ | `onClick` | `MouseEventHandler<HTMLButtonElement>` | — | Action handler; required unless `render` is supplied |
123
+ | `isActive` | `boolean` | `false` | Explicit visual emphasis without route matching or selection state |
124
+ | `disabled` | `boolean` | — | Disables clicks and removes the operation from the tab order |
125
+ | `className` | `string` | — | Classes applied to the inner operation |
126
+
127
+ Other standard button props, such as `title` and ARIA attributes, are forwarded to the inner operation. Put link-specific props such as `href`, `target`, and `rel` on the element passed to `render`.
128
+
129
+ Rows without an `icon` do not reserve icon space. To align a mixed list, explicitly pass `icon={<span />}` on a row that needs an empty icon slot. Lists with no icons keep their compact indentation.
130
+
131
+ ## Groups and Heading Hierarchies
132
+
133
+ `expandOnClick` defaults to `true`: clicking a group's heading toggles its child list, whether or not `render` is supplied. `render` only replaces the heading's main operation; it does not determine the expansion behavior. `defaultOpen` defaults to `true`; set it to `false` to start collapsed. Groups can be nested.
134
+
135
+ Set `expandOnClick={false}` to separate the heading's main operation from expansion. This requires `onClick` or `render` so the heading does not become an inert button. A separate button, named by the group title, expands and collapses the group. Use this for linked headings so navigation does not toggle the list.
136
+
137
+ `onClick` applies only to the heading's main operation. Clicking a child Item or the separate expand button does not invoke it. `isActive` and `aria-current="page"` control emphasis on the main heading operation, not the separate expand button.
138
+
139
+ For a table of contents, use a linked Group for a parent heading and Items for its child headings. Keep the list data aligned with the actual heading hierarchy and anchor IDs in the document.
140
+
141
+ <example-preview name="linked-group"></example-preview>
142
+
143
+ ```tsx
144
+ function LinkedGroup() {
145
+ return (
146
+ <ItemList.Root aria-label="Contents">
147
+ <ItemList.Group
148
+ title="Installing"
149
+ render={<a href="#installing" aria-label="Installing" />}
150
+ expandOnClick={false}
151
+ >
152
+ <ItemList.Item render={<a href="#configuration" aria-label="Configuration" />}>
153
+ Configuration
154
+ </ItemList.Item>
155
+ </ItemList.Group>
156
+ </ItemList.Root>
157
+ );
158
+ }
159
+ ```
160
+
161
+ ### Group Props
162
+
163
+ | Prop | Type | Default | Description |
164
+ | --------------- | -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------- |
165
+ | `title` | `ReactNode` | — | Accessible group label, including text and optional decorations |
166
+ | `icon` | `ReactNode` | — | Optional decorative leading icon |
167
+ | `render` | `ReactElement` | — | Replaces the main heading operation without changing expansion behavior |
168
+ | `onClick` | `MouseEventHandler<HTMLButtonElement>` | — | Handler for the main heading operation only |
169
+ | `expandOnClick` | `boolean` | `true` | Toggle from the heading; `false` requires `onClick` or `render` and adds a separate expand button |
170
+ | `isActive` | `boolean` | `false` | Explicit visual emphasis for the heading operation |
171
+ | `defaultOpen` | `boolean` | `true` | Initial uncontrolled expanded state |
172
+ | `children` | `ReactNode` | — | Nested Items, Groups, Sections, or Separators |
173
+
174
+ Other standard `li` props, including `className`, apply to the containing group item, not its heading operation. `onClick` is the exception described above.
175
+
176
+ A Group's rendered label names its heading operation, separate expand button, and child list. Keep meaningful text in a rich `title`; ReactNode content is not converted into a string. Meaningful details such as counts can be read with the label, while purely decorative elements should use `aria-hidden="true"`. An icon-only title needs screen-reader text. Do not put links or buttons inside the title, because the heading itself is interactive.
177
+
178
+ ## Sections and Separators
179
+
180
+ Use `Group` for hierarchy and `Section` for flat classification. A Section accepts a resolved string `title` and `children`, keeps child rows at their normal indentation, and does not add an interactive heading. Other standard `li` props apply to its outer item.
181
+
182
+ Neither Group nor Section requires wrapping its children in another `ItemList.Root`. Use `ItemList.Separator` between logical runs of items; it is decorative and renders a list item rather than an invalid direct `div` child of `ul`.
183
+
184
+ ## Styling and Accessibility
185
+
186
+ - Keep labels concise; they truncate in narrow containers.
187
+ - Use `aria-label` or `aria-labelledby` on Root when a list needs an accessible name.
188
+ - Native links and buttons retain their keyboard interactions. Group buttons announce their expanded state and support Enter and Space.
189
+ - Resolve translations before passing labels; ItemList does not translate them. Group titles can contain ReactNode content, while Section titles remain strings.
190
+ - ItemList is an ordinary list, not a menu or selection widget. It does not provide arrow-key selection, search, reordering, or Sidebar icon-rail behavior.
191
+ - Existing Sidebar components retain route metadata resolution, rail tooltips and flyouts, and drawer dismissal while sharing the underlying list presentation.
@@ -171,6 +171,16 @@ const { Root, Trigger, Value, Content, Item, Group, GroupLabel, Separator } = Se
171
171
 
172
172
  ## Form submission
173
173
 
174
+ Inside a form, place `Select` directly under `Field.Root`, not inside `Field.Control`. `Select` draws
175
+ its own trigger and registers with the field itself.
176
+
177
+ ```tsx
178
+ <Field.Root name="country">
179
+ <Field.Label>Country</Field.Label>
180
+ <Select items={["Japan", "United States"]} placeholder="Select a country" />
181
+ </Field.Root>
182
+ ```
183
+
174
184
  Inside a `Field.Root`, the field's `name` identifies the value and these props are unnecessary —
175
185
  see [Form](./form.md). They matter for **native** submission: a plain `<form>`, `new FormData(form)`,
176
186
  or a server action.
@@ -124,13 +124,13 @@ Titles support i18n with `defineI18nLabels`:
124
124
  import { defineI18nLabels } from "@tailor-platform/app-shell";
125
125
 
126
126
  const labels = defineI18nLabels({
127
- products: {
128
- en: "Products",
129
- ja: "商品",
127
+ en: {
128
+ products: "Products",
129
+ settings: "Settings",
130
130
  },
131
- settings: {
132
- en: "Settings",
133
- ja: "設定",
131
+ ja: {
132
+ products: "商品",
133
+ settings: "設定",
134
134
  },
135
135
  });
136
136
 
@@ -0,0 +1,235 @@
1
+ ---
2
+ title: Text
3
+ description: Text with a typography role, rendered as an element you choose
4
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/components/text.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Text
9
+
10
+ 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. To choose the element, use an element component such as `Text.h1` or `Text.p`, or use the `render` prop (Base UI render pattern) to compose with another component.
11
+
12
+ ## Import
13
+
14
+ ```tsx
15
+ import { Text } from "@tailor-platform/app-shell";
16
+ ```
17
+
18
+ ## Basic Usage
19
+
20
+ <example-preview name="basic-usage"></example-preview>
21
+
22
+ ```tsx
23
+ function BasicUsage() {
24
+ return (
25
+ <div className="flex flex-col gap-1">
26
+ <Text.h2>Purchase orders</Text.h2>
27
+ <Text.p className="text-muted-foreground">Orders sent to suppliers this month.</Text.p>
28
+ </div>
29
+ );
30
+ }
31
+ ```
32
+
33
+ ## Props
34
+
35
+ _Generated from the type surface — first-party props only._
36
+
37
+ ### TextProps
38
+
39
+ | Prop | Type | Default | Description |
40
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | `ref?` | `React.Ref<HTMLElement>` | - | Ref to the rendered element. |
42
+ | `render?` | `React.ReactElement` | - | Custom element to render as (Base UI render prop). Use it to compose with another component. For a plain HTML element, use `Text.h1` to `Text.h6`, `Text.p`, `Text.span`, or `Text.div`. |
43
+ | `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. |
44
+
45
+ ## Exports
46
+
47
+ - `Text` — const
48
+ - `TextProps` — type
49
+
50
+ ## Element Components
51
+
52
+ `Text.h1` to `Text.h6`, `Text.p`, `Text.span`, and `Text.div` always render their named HTML element. Each one applies a default role, so you do not repeat `variant` for ordinary text:
53
+
54
+ <example-preview name="element-components"></example-preview>
55
+
56
+ ```tsx
57
+ function ElementComponents() {
58
+ return (
59
+ <div className="flex flex-col gap-1">
60
+ <Text.h3>Shipping address</Text.h3>
61
+ <Text.p className="text-muted-foreground">Orders ship from the main warehouse.</Text.p>
62
+ <Text.span className="tabular-nums">3 packages</Text.span>
63
+ </div>
64
+ );
65
+ }
66
+ ```
67
+
68
+ | Component | Default `variant` |
69
+ | ------------------- | ----------------- |
70
+ | `Text.h1` | `heading-lg` |
71
+ | `Text.h2` | `heading-md` |
72
+ | `Text.h3`–`Text.h6` | `heading-sm` |
73
+ | `Text.p` | `body-md` |
74
+ | `Text.span` | `label-md` |
75
+ | `Text.div` | `body-md` |
76
+
77
+ The element sets the meaning and `variant` sets the appearance. The default is only a starting point, and any role works on any element:
78
+
79
+ ```tsx
80
+ <Text.h1 variant="heading-sm">A compact page title</Text.h1>
81
+ <Text.h3 variant="heading-lg">A visually prominent subsection</Text.h3>
82
+ <Text.span variant="body-sm">Updated recently</Text.span>
83
+ ```
84
+
85
+ The element components take `variant`, `className`, the element's own attributes, and a ref typed for the element (`HTMLHeadingElement` for `Text.h2`). They do not take `render`. For any other element, or to compose with another component, use `Text` with `render`.
86
+
87
+ ## Using `render`
88
+
89
+ All standard HTML attributes are also accepted by `Text`. Put an attribute that belongs to one element, such as `htmlFor` on a `label`, on the `render` element:
90
+
91
+ ```tsx
92
+ <Text variant="label-md" render={<label htmlFor="quantity" />}>
93
+ Quantity
94
+ </Text>
95
+ ```
96
+
97
+ Some lint rules report this example; see [Lint](#lint).
98
+
99
+ ## Roles
100
+
101
+ `variant` takes one of the ten roles. On `Text`, each role renders a default element when you do not pass `render`:
102
+
103
+ | `variant` | Default element | Use |
104
+ | ----------------- | --------------- | ----------------------------------------------- |
105
+ | `heading-lg` | `h2` | Page title (`Layout.Header`) |
106
+ | `heading-md` | `h2` | Section heading, card title |
107
+ | `heading-sm` | `h2` | Sub-section heading |
108
+ | `body-md` | `p` | Body copy, descriptions (default role) |
109
+ | `body-sm` | `p` | Caption, timestamp, metadata |
110
+ | `body-md-relaxed` | `p` | Long text, such as a chat message or a document |
111
+ | `body-sm-relaxed` | `p` | Long text at the small size |
112
+ | `label-md` | `span` | Field label, short UI text |
113
+ | `label-sm` | `span` | Small label, badge text |
114
+ | `code-sm` | `span` | ID and code. `Text` adds `font-mono` for you |
115
+
116
+ <example-preview name="roles"></example-preview>
117
+
118
+ ```tsx
119
+ function Roles() {
120
+ return (
121
+ <div className="flex flex-col gap-3">
122
+ {/* The headings render a div so that this specimen is not a document heading. */}
123
+ <Text.div variant="heading-lg">heading-lg: Purchase orders</Text.div>
124
+ <Text.div variant="heading-md">heading-md: Line items</Text.div>
125
+ <Text.div variant="heading-sm">heading-sm: Shipping address</Text.div>
126
+ <Text variant="body-md">body-md: Orders sent to suppliers this month.</Text>
127
+ <Text variant="body-sm">body-sm: Updated 2 hours ago</Text>
128
+ <Text variant="label-md">label-md: Total amount</Text>
129
+ <Text variant="label-sm">label-sm: Draft</Text>
130
+ <Text variant="code-sm">code-sm: PO-2026-0001</Text>
131
+ </div>
132
+ );
133
+ }
134
+ ```
135
+
136
+ The sizes and line heights are in the [Typography section of the styling guide](../concepts/styling-theming.md#typography).
137
+
138
+ ## Heading Level
139
+
140
+ On `Text`, 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 `Text.h1` to `Text.h6`:
141
+
142
+ ```tsx
143
+ <Text.h3 variant="heading-md">Line items</Text.h3>
144
+ ```
145
+
146
+ To use a heading role for text that is not a heading, use `Text.div` or `Text.span`:
147
+
148
+ ```tsx
149
+ <Text.div variant="heading-md">1,240 orders</Text.div>
150
+ ```
151
+
152
+ ## Lint
153
+
154
+ Some `jsx-a11y` rules check that an element has text inside it, for example `heading-has-content` for `<h3 />` and `label-has-associated-control` for `<label />`. These rules report the element in `render` as empty, because they do not see that `Text` passes its children to that element.
155
+
156
+ For headings, use `Text.h1` to `Text.h6`. The rules do not report them. For other elements, such as `label`, use `render`, and if your app enables the rule, disable it on that line. ESLint and oxlint both read this comment:
157
+
158
+ ```tsx
159
+ // eslint-disable-next-line jsx-a11y/label-has-associated-control -- Text renders the children
160
+ <Text variant="label-md" render={<label htmlFor="quantity" />}>
161
+ Quantity
162
+ </Text>
163
+ ```
164
+
165
+ ## Color and Modifiers
166
+
167
+ 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-semibold` for emphasis, and `uppercase`.
168
+
169
+ <example-preview name="color-and-modifiers"></example-preview>
170
+
171
+ ```tsx
172
+ function ColorAndModifiers() {
173
+ return (
174
+ <div className="flex flex-col gap-3">
175
+ <Text variant="body-md" className="text-muted-foreground">
176
+ Orders sent this month.
177
+ </Text>
178
+ <Text variant="label-md" className="tabular-nums">
179
+ 1,240.00
180
+ </Text>
181
+ </div>
182
+ );
183
+ }
184
+ ```
185
+
186
+ ```tsx
187
+ <Text variant="body-md" className="text-muted-foreground">
188
+ Orders sent this month.
189
+ </Text>
190
+ <Text.h3 variant="heading-md" className="leading-none">
191
+ Card title
192
+ </Text.h3>
193
+ <Text variant="label-md" className="tabular-nums">
194
+ 1,240.00
195
+ </Text>
196
+ ```
197
+
198
+ Do not combine a role with other `leading-*`, `tracking-*`, or `text-*` size values. Pick a different `variant` instead. A `text-*` size class such as `text-sm` has no effect on `Text`: AppShell's stylesheet loads after yours, so the role's size wins.
199
+
200
+ ## Using Roles Without Text
201
+
202
+ 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`:
203
+
204
+ ```tsx
205
+ <th className="text-label-md text-muted-foreground">Amount</th>
206
+ ```
207
+
208
+ ## TypeScript
209
+
210
+ ```typescript
211
+ import { type TextProps } from "@tailor-platform/app-shell";
212
+ ```
213
+
214
+ The element components have no exported props type. Read it from the component:
215
+
216
+ ```tsx
217
+ type SectionTitleProps = React.ComponentProps<typeof Text.h2>;
218
+ ```
219
+
220
+ ## Accessibility
221
+
222
+ - A heading role on `Text` renders a real heading (`h2`) by default, so screen readers list it with the other headings. Set the level with `Text.h1` to `Text.h6` so the levels follow the page structure.
223
+ - Do not choose a heading level for its size. `Text` keeps the size and the level separate.
224
+ - `body-*` roles render `p` on `Text`. Inside another `p`, use `Text.span` instead (`<Text.span variant="body-md">`): the browser closes the outer `p` before the inner one, so React warns and server rendering fails to hydrate. Inside inline elements such as `button`, `label` or `span`, a `p` is invalid HTML too, so render a `span` there as well.
225
+
226
+ ## Palettes
227
+
228
+ 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).
229
+
230
+ ## Related Components
231
+
232
+ - [Card](./card.md) - The table above lists `heading-md` for card titles
233
+ - [Layout](./layout.md) - The table above lists `heading-lg` for the page title
234
+
235
+ AppShell's own components, including these two, render their titles and descriptions with `Text`.
@@ -69,16 +69,18 @@ const App = () => (
69
69
  ### SidebarItem
70
70
 
71
71
  A navigation item that automatically resolves title and icon from resource definitions.
72
+ Active internal sidebar links expose `aria-current="page"` on the link itself, including
73
+ items with custom `render` content. Inactive and external links do not expose it.
72
74
 
73
75
  #### Props
74
76
 
75
- | Prop | Type | Required | Description |
76
- | ---------- | ----------------- | -------- | ------------------------------------------------------------------------------- |
77
- | `to` | `string` | Yes | Target URL. External URLs (http://...) are rendered as external links |
78
- | `title` | `LocalizedString` | No | Override title (i18n supported). When omitted, auto-resolved from resource meta |
79
- | `icon` | `ReactNode` | No | Override icon. When omitted, auto-resolved from resource meta |
80
- | `external` | `boolean` | No | Opens link in new tab with external link icon |
81
- | `render` | `function` | No | Custom render function for full UI control |
77
+ | Prop | Type | Required | Description |
78
+ | ---------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
79
+ | `to` | `string` | Yes | Target URL. External URLs (http://...) are rendered as external links |
80
+ | `title` | `LocalizedString` | No | Override title (i18n supported). When omitted, auto-resolved from resource meta |
81
+ | `icon` | `ReactNode` | No | Override icon. When omitted, auto-resolved from resource meta; with no icon at all, the icon rail shows the title's first letter |
82
+ | `external` | `boolean` | No | Opens link in new tab with external link icon |
83
+ | `render` | `function` | No | Custom render function for full UI control |
82
84
 
83
85
  #### Examples
84
86
 
@@ -124,13 +126,13 @@ A collapsible group for organizing navigation items.
124
126
 
125
127
  #### Props
126
128
 
127
- | Prop | Type | Required | Description |
128
- | ------------- | ----------------- | -------- | ---------------------------------------------- |
129
- | `title` | `LocalizedString` | Yes | Group title (supports i18n) |
130
- | `icon` | `ReactNode` | No | Group icon |
131
- | `to` | `string` | No | When specified, title becomes a clickable link |
132
- | `defaultOpen` | `boolean` | No | Initial expanded state. Default: `true` |
133
- | `children` | `ReactNode` | Yes | Child items (SidebarItem, SidebarGroup, etc.) |
129
+ | Prop | Type | Required | Description |
130
+ | ------------- | ----------------- | -------- | ---------------------------------------------------------------------- |
131
+ | `title` | `LocalizedString` | Yes | Group title (supports i18n) |
132
+ | `icon` | `ReactNode` | No | Group icon. When omitted, the icon rail shows the title's first letter |
133
+ | `to` | `string` | No | When specified, title becomes a clickable link |
134
+ | `defaultOpen` | `boolean` | No | Initial expanded state. Default: `true` |
135
+ | `children` | `ReactNode` | Yes | Child items (SidebarItem, SidebarGroup, etc.) |
134
136
 
135
137
  #### Examples
136
138