@tailor-platform/app-shell 1.18.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.
@@ -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.
@@ -7,7 +7,7 @@ description: Text with a typography role, rendered as an element you choose
7
7
 
8
8
  # Text
9
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. Use the `render` prop to choose the element (Base UI render pattern).
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
11
 
12
12
  ## Import
13
13
 
@@ -23,8 +23,8 @@ import { Text } from "@tailor-platform/app-shell";
23
23
  function BasicUsage() {
24
24
  return (
25
25
  <div className="flex flex-col gap-1">
26
- <Text variant="heading-md">Purchase orders</Text>
27
- <Text className="text-muted-foreground">Orders sent to suppliers this month.</Text>
26
+ <Text.h2>Purchase orders</Text.h2>
27
+ <Text.p className="text-muted-foreground">Orders sent to suppliers this month.</Text.p>
28
28
  </div>
29
29
  );
30
30
  }
@@ -36,18 +36,57 @@ _Generated from the type surface — first-party props only._
36
36
 
37
37
  ### TextProps
38
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 set the heading level, for example `render={<h3 />}`. |
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. |
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
44
 
45
45
  ## Exports
46
46
 
47
- - `Text` — function
47
+ - `Text` — const
48
48
  - `TextProps` — type
49
49
 
50
- 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:
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:
51
90
 
52
91
  ```tsx
53
92
  <Text variant="label-md" render={<label htmlFor="quantity" />}>
@@ -59,7 +98,7 @@ Some lint rules report this example; see [Lint](#lint).
59
98
 
60
99
  ## Roles
61
100
 
62
- `variant` takes one of the ten roles. Each role renders a default element when you do not pass `render`:
101
+ `variant` takes one of the ten roles. On `Text`, each role renders a default element when you do not pass `render`:
63
102
 
64
103
  | `variant` | Default element | Use |
65
104
  | ----------------- | --------------- | ----------------------------------------------- |
@@ -70,7 +109,7 @@ Some lint rules report this example; see [Lint](#lint).
70
109
  | `body-sm` | `p` | Caption, timestamp, metadata |
71
110
  | `body-md-relaxed` | `p` | Long text, such as a chat message or a document |
72
111
  | `body-sm-relaxed` | `p` | Long text at the small size |
73
- | `label-md` | `span` | Field label, short UI text, figures in a table |
112
+ | `label-md` | `span` | Field label, short UI text |
74
113
  | `label-sm` | `span` | Small label, badge text |
75
114
  | `code-sm` | `span` | ID and code. `Text` adds `font-mono` for you |
76
115
 
@@ -81,15 +120,9 @@ function Roles() {
81
120
  return (
82
121
  <div className="flex flex-col gap-3">
83
122
  {/* The headings render a div so that this specimen is not a document heading. */}
84
- <Text variant="heading-lg" render={<div />}>
85
- heading-lg: Purchase orders
86
- </Text>
87
- <Text variant="heading-md" render={<div />}>
88
- heading-md: Line items
89
- </Text>
90
- <Text variant="heading-sm" render={<div />}>
91
- heading-sm: Shipping address
92
- </Text>
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>
93
126
  <Text variant="body-md">body-md: Orders sent to suppliers this month.</Text>
94
127
  <Text variant="body-sm">body-sm: Updated 2 hours ago</Text>
95
128
  <Text variant="label-md">label-md: Total amount</Text>
@@ -104,30 +137,28 @@ The sizes and line heights are in the [Typography section of the styling guide](
104
137
 
105
138
  ## Heading Level
106
139
 
107
- 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`:
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`:
108
141
 
109
142
  ```tsx
110
- <Text variant="heading-md" render={<h3 />}>
111
- Line items
112
- </Text>
143
+ <Text.h3 variant="heading-md">Line items</Text.h3>
113
144
  ```
114
145
 
115
- To use a heading role for text that is not a heading, render a `div` or a `span`:
146
+ To use a heading role for text that is not a heading, use `Text.div` or `Text.span`:
116
147
 
117
148
  ```tsx
118
- <Text variant="heading-md" render={<div />}>
119
- 1,240 orders
120
- </Text>
149
+ <Text.div variant="heading-md">1,240 orders</Text.div>
121
150
  ```
122
151
 
123
152
  ## Lint
124
153
 
125
- 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. If your app enables them, disable the rule on that line. ESLint and oxlint both read this comment:
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:
126
157
 
127
158
  ```tsx
128
- // eslint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children
129
- <Text variant="heading-md" render={<h3 />}>
130
- Line items
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
131
162
  </Text>
132
163
  ```
133
164
 
@@ -156,9 +187,9 @@ function ColorAndModifiers() {
156
187
  <Text variant="body-md" className="text-muted-foreground">
157
188
  Orders sent this month.
158
189
  </Text>
159
- <Text variant="heading-md" render={<h3 />} className="leading-none">
190
+ <Text.h3 variant="heading-md" className="leading-none">
160
191
  Card title
161
- </Text>
192
+ </Text.h3>
162
193
  <Text variant="label-md" className="tabular-nums">
163
194
  1,240.00
164
195
  </Text>
@@ -180,11 +211,17 @@ Each role is also a plain utility, for example `text-heading-md`. Use the utilit
180
211
  import { type TextProps } from "@tailor-platform/app-shell";
181
212
  ```
182
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
+
183
220
  ## Accessibility
184
221
 
185
- - 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.
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.
186
223
  - Do not choose a heading level for its size. `Text` keeps the size and the level separate.
187
- - `body-*` roles render `p`. Inside another `p`, render a `span` instead (`<Text render={<span />}>`): 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.
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.
188
225
 
189
226
  ## Palettes
190
227
 
@@ -195,4 +232,4 @@ The `cream` and `bloom` palettes set the line height and letter spacing of `h1`
195
232
  - [Card](./card.md) - The table above lists `heading-md` for card titles
196
233
  - [Layout](./layout.md) - The table above lists `heading-lg` for the page title
197
234
 
198
- AppShell's own components, including these two, still use the stock utilities. They will move to the roles in a later change.
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
 
@@ -217,7 +217,14 @@ Each intent has eight roles. The intents are `info`, `success`, `warning`, `dang
217
217
  <span className="rounded-md bg-success-surface px-2 py-0.5 text-success-text">Paid</span>
218
218
  ```
219
219
 
220
- In the default palette, `text` on `surface` and `contrast` on `solid` and `solid-hover` are at least 4.5:1 for the four hue intents, in light and dark mode, on `--card` and `--background`. A unit test checks this from the shipped CSS. `text` on `surface-hover` is held at 4.2:1 by the same test; it is below 4.5:1 for `danger` and `success` in light mode. The test does not cover the `cream` and `bloom` palettes or the `neutral` intent.
220
+ The hue roles have the same values in every shipped palette. `cream` and `bloom` change brand and surface colors, not status colors. The `neutral` roles follow the palette, because they read the system tokens.
221
+
222
+ **Contrast.** AppShell's thresholds are 4.5:1 for text (WCAG 1.4.3) and 3:1 for non-text (WCAG 1.4.11). In every shipped palette (`default`, `cream`, `bloom`), in light and dark mode, on `--card` and `--background`, `text` on `surface` and `contrast` on `solid` and `solid-hover` are at least 4.5:1 for the four hue intents, and `danger` `solid` (the invalid-input border) is at least 3:1. A browser test checks this in Chromium against the built stylesheet. Known exceptions:
223
+
224
+ - `text` on `surface-hover` is below 4.5:1 for `danger` and `success` in light mode. The test holds it at 4.2:1 or more.
225
+ - In `cream` and `bloom`, the main content area is transparent over the shell gradient. At the top of the `bloom` light gradient, `text` on `surface` is 4.49:1 for `success` and 4.48:1 for `danger`. Put status content on a card when it needs to pass.
226
+ - `warning` `solid` and `indicator` are 2.15:1 against a light card. Use them with a label, not as the only sign of a state.
227
+ - The `neutral` intent reads the system tokens and is not checked.
221
228
 
222
229
  Most `surface` and `border` values are translucent tints, so an opacity modifier compounds rather than replaces — `bg-info-surface/50` halves the tint's alpha instead of setting it to 50%. Some light-mode values are opaque instead of translucent: the `warning` surface, surface-hover and border, the `info` border and the `danger` surface-hover. They do not blend with a tinted parent. All dark-mode values are translucent.
223
230
 
@@ -227,7 +234,7 @@ Most `surface` and `border` values are translucent tints, so an opacity modifier
227
234
 
228
235
  `--sidebar`, `--sidebar-foreground`, `--sidebar-border`, `--sidebar-primary(-foreground)`, `--sidebar-accent(-foreground)`, `--sidebar-ring` → `bg-sidebar`, `text-sidebar-foreground`, and so on. These let a palette tint the shell independently of page content.
229
236
 
230
- `--chart-1` … `--chart-5` → `bg-chart-1`, `text-chart-1`, `fill-chart-1`. Use them in order for categorical series.
237
+ `--chart-1` … `--chart-5` → `bg-chart-1`, `text-chart-1`, `fill-chart-1`. Use them in order for categorical series. They tell series apart and carry no status, so they are not semantic roles and have literal values.
231
238
 
232
239
  #### Alerts
233
240
 
@@ -283,7 +290,7 @@ There are 8 core roles and 2 relaxed body variants:
283
290
  | `heading-sm` | `text-heading-sm` | 14px / 20px | 600 | Sub-section heading |
284
291
  | `body-md` | `text-body-md` | 14px / 20px | 400 | Body copy, descriptions |
285
292
  | `body-sm` | `text-body-sm` | 12px / 16px | 400 | Caption, timestamp, metadata |
286
- | `label-md` | `text-label-md` | 14px / 20px | 500 | Field label, short UI text, figures in a table |
293
+ | `label-md` | `text-label-md` | 14px / 20px | 500 | Field label, short UI text |
287
294
  | `label-sm` | `text-label-sm` | 12px / 16px | 500 | Small label, badge text |
288
295
  | `code-sm` | `text-code-sm` | 12px / 18px | 400 | ID and code. Use with `font-mono` |
289
296
  | `body-md-relaxed` | `text-body-md-relaxed` | 14px / 24px | 400 | Long text, such as a chat message or a document |
@@ -301,7 +308,7 @@ Pair a role with a color token from the Color section above. These pairings are
301
308
  | Body copy | `text-body-md` |
302
309
  | Secondary copy, descriptions | `text-body-md text-muted-foreground` |
303
310
  | Caption, timestamp, metadata | `text-body-sm text-muted-foreground` |
304
- | Numeric value in a table or field | `text-label-md tabular-nums` |
311
+ | Numeric value in a table or field | `text-body-md tabular-nums` |
305
312
  | ID, code | `font-mono text-code-sm text-muted-foreground` |
306
313
 
307
314
  <example-preview name="typography-roles"></example-preview>
@@ -404,20 +411,21 @@ function TypographyRoles() {
404
411
  }
405
412
  ```
406
413
 
414
+ For headings, body copy, labels, and captions, use the [`Text`](../components/text.md) component. Do not style a bare `p`, `h1` to `h6`, or `span` with `text-sm`, `text-xs`, or `font-*`. A form field label is [`Field.Label`](../components/form.md), which already has the `label-md` role. `Text` 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). To set the element, such as the heading level, use an element component (`Text.h1` to `Text.h6`, `Text.p`, `Text.span`, `Text.div`):
415
+
407
416
  ```tsx
408
- <h2 className="text-heading-md">Section</h2>
409
- <p className="text-body-md text-muted-foreground">Description copy</p>
410
- <span className="text-body-sm text-muted-foreground">Updated 2h ago</span>
417
+ <Text.h3 variant="heading-md">Line items</Text.h3>
418
+ <Text variant="body-md" className="text-muted-foreground">Description copy</Text>
419
+ <Text variant="body-sm" className="text-muted-foreground">Updated 2h ago</Text>
411
420
  ```
412
421
 
413
- 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:
422
+ Use the role utility when you style an element that you render yourself, such as a table header cell or a link:
414
423
 
415
424
  ```tsx
416
- <Text variant="heading-md" render={<h3 />}>Line items</Text>
417
- <Text variant="body-md" className="text-muted-foreground">Description copy</Text>
425
+ <th className="text-label-md text-muted-foreground">Amount</th>
418
426
  ```
419
427
 
420
- 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.
428
+ 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 use the roles: content text renders with `Text`, and controls use the role utility.
421
429
 
422
430
  **Rules**
423
431
 
@@ -33,7 +33,7 @@ Each entry states which versions are affected, what breaks, how to detect it, an
33
33
  - **`text-destructive` resolves to the solid.** In dark mode it changes from `#f87171` to `#dc2626`, which is 3.71:1 on the dark card, below 4.5:1 for text. Components shipped with AppShell now use `text-danger-text` for error text, and invalid borders and focus rings read `--danger-solid`. In dark mode the invalid border changes from `#f87171` to `#dc2626`.
34
34
  - **Themes that override `--destructive` no longer reach the components.** `Button`, invalid `Input`, `Textarea`, `Checkbox`, `Field` and `DateField` borders and rings, error text and the `CsvImporter` error cells read `--danger-*`. An override of `--destructive` still changes `bg-destructive` and `text-destructive` on your own elements, so the two reds can drift apart. The `cream` and `bloom` palettes no longer set `--destructive`; their values were the same as the default.
35
35
 
36
- How to detect it: search the app for `destructive` in class names and in theme CSS, including Theme Generator output that sets `--destructive`. Compare `Button variant="destructive"`, an invalid `Input` and an error message in light and dark mode after upgrading.
36
+ How to detect it: search the app for `destructive` in class names and in theme CSS, including Theme Generator output that sets `--destructive`. In JS and TS files, the `no-deprecated-tokens` rule in `@tailor-platform/eslint-plugin-app-shell` reports these names and the role to use instead. The rule is available from version 0.2.0 of the plugin, and it does not read CSS files. Compare `Button variant="destructive"`, an invalid `Input` and an error message in light and dark mode after upgrading.
37
37
 
38
38
  What to change:
39
39
 
@@ -53,7 +53,7 @@ The roles are documented in [Styling & theming](https://github.com/tailor-platfo
53
53
  - **Component recolouring moves to the roles.** `Badge`, `Alert`, `CsvImporter` and `MetricCard` read the roles directly. A theme that overrides `--status-*` or `--alert-*` no longer changes these components; override `--{intent}-*` instead. `Badge` `error` / `subtle-error` and `Alert` `error` read `--danger-*`, so overriding `--destructive` no longer reaches them either. `Button variant="destructive"` and `text-destructive` still follow `--destructive`.
54
54
  - **`text-status-*` loses contrast in dark mode.** The `--status-*` values are now the step-9 fills, which are below 4.5:1 as text on the dark card.
55
55
 
56
- How to detect it: search the app for `status-`, `alert-` and theme overrides of `--status-`, `--alert-` and `--destructive`. Compare `Badge` and `Alert` in light and dark mode after upgrading.
56
+ How to detect it: search the app for `status-`, `alert-` and theme overrides of `--status-`, `--alert-` and `--destructive`. In JS and TS files, the `no-deprecated-tokens` rule in `@tailor-platform/eslint-plugin-app-shell` reports these names and the role to use instead. The rule is available from version 0.2.0 of the plugin, and it does not read CSS files. Compare `Badge` and `Alert` in light and dark mode after upgrading.
57
57
 
58
58
  What to change:
59
59