@tailor-platform/app-shell 1.18.0 → 1.20.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 (55) hide show
  1. package/dist/app-shell-core.css +2 -2
  2. package/dist/app-shell.d.ts +1108 -190
  3. package/dist/app-shell.js +13090 -10783
  4. package/dist/{appshell-Y1AYCuVJ.js → appshell-B4QkQvXF.js} +519 -392
  5. package/dist/fonts/inter.css +7 -7
  6. package/dist/fonts/noto-sans-jp.css +116 -116
  7. package/dist/style-entry.d.ts +1 -656
  8. package/dist/testing.d.ts +132 -97
  9. package/dist/testing.js +1 -1
  10. package/dist/vite-plugin.d.ts +132 -97
  11. package/package.json +15 -10
  12. package/skills/app-shell-patterns/SKILL.md +38 -27
  13. package/skills/app-shell-patterns/references/api/aggregations.md +131 -0
  14. package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +32 -2
  15. package/skills/app-shell-patterns/references/api/define-module.md +5 -5
  16. package/skills/app-shell-patterns/references/api/router/use-params.md +2 -2
  17. package/skills/app-shell-patterns/references/api/use-ai-chat.md +46 -8
  18. package/skills/app-shell-patterns/references/api/use-app-shell-config.md +4 -4
  19. package/skills/app-shell-patterns/references/api/use-app-shell-data.md +5 -5
  20. package/skills/app-shell-patterns/references/api/use-app-shell.md +2 -2
  21. package/skills/app-shell-patterns/references/api/use-auth.md +3 -3
  22. package/skills/app-shell-patterns/references/api/use-route-error.md +12 -12
  23. package/skills/app-shell-patterns/references/components/action-panel.md +57 -23
  24. package/skills/app-shell-patterns/references/components/activity-card.md +3 -3
  25. package/skills/app-shell-patterns/references/components/ai-chat.md +37 -3
  26. package/skills/app-shell-patterns/references/components/app-shell.md +6 -5
  27. package/skills/app-shell-patterns/references/components/badge.md +7 -3
  28. package/skills/app-shell-patterns/references/components/card.md +2 -2
  29. package/skills/app-shell-patterns/references/components/data-table.md +514 -77
  30. package/skills/app-shell-patterns/references/components/date-picker.md +2 -2
  31. package/skills/app-shell-patterns/references/components/default-sidebar.md +2 -2
  32. package/skills/app-shell-patterns/references/components/dialog.md +8 -0
  33. package/skills/app-shell-patterns/references/components/filter-rail.md +10 -3
  34. package/skills/app-shell-patterns/references/components/form.md +19 -9
  35. package/skills/app-shell-patterns/references/components/global-header-layout.md +31 -8
  36. package/skills/app-shell-patterns/references/components/item-list.md +191 -0
  37. package/skills/app-shell-patterns/references/components/layout.md +24 -17
  38. package/skills/app-shell-patterns/references/components/select.md +18 -0
  39. package/skills/app-shell-patterns/references/components/sidebar-group.md +9 -7
  40. package/skills/app-shell-patterns/references/components/sidebar-layout.md +74 -11
  41. package/skills/app-shell-patterns/references/components/sidebar-section.md +8 -8
  42. package/skills/app-shell-patterns/references/components/tabs.md +32 -2
  43. package/skills/app-shell-patterns/references/components/text.md +75 -38
  44. package/skills/app-shell-patterns/references/components/tree.md +342 -0
  45. package/skills/app-shell-patterns/references/components/with-guard.md +5 -5
  46. package/skills/app-shell-patterns/references/concepts/authentication.md +1 -1
  47. package/skills/app-shell-patterns/references/concepts/routing-navigation.md +3 -3
  48. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +16 -14
  49. package/skills/app-shell-patterns/references/concepts/styling-theming.md +28 -20
  50. package/skills/app-shell-patterns/references/migrations.md +22 -2
  51. package/skills/app-shell-patterns/references/pages/document-detail.md +32 -25
  52. package/skills/app-shell-patterns/references/patterns/form-modal.md +4 -1
  53. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +1 -1
  54. package/dist/semantic-contrast.test.ts +0 -227
  55. package/dist/theme-bridge.test.ts +0 -182
@@ -56,7 +56,7 @@ With a visible label + description:
56
56
  aria-labelledby="invoice-date-label"
57
57
  aria-describedby="invoice-date-help"
58
58
  />
59
- <p id="invoice-date-help">Format follows your locale</p>
59
+ <Text.p id="invoice-date-help">Format follows your locale</Text.p>
60
60
  ```
61
61
 
62
62
  Inside `Field.Root`:
@@ -124,7 +124,7 @@ Use standard HTML + ARIA when rendering the field standalone:
124
124
  value={value}
125
125
  onChange={setValue}
126
126
  />
127
- {error && <p id="delivery-date-error">{error}</p>}
127
+ {error && <Text.p id="delivery-date-error">{error}</Text.p>}
128
128
  ```
129
129
 
130
130
  Or let `Field.Root` wire the label, description, and error elements:
@@ -75,7 +75,7 @@ With `iconRail`, collapsing the sidebar keeps icons visible at every width:
75
75
 
76
76
  - on desktop, the sidebar collapses to a narrow persistent rail
77
77
  - on mobile, the rail stays visible and its toggle opens the full sidebar as a slide-in drawer
78
- - grouped items reveal child pages from the collapsed icon via a flyout popover
78
+ - grouped items reveal child pages from the collapsed icon via a flyout popover: it opens on hover with a mouse, and on tap on a touch device (tap again, tap outside, or press `Escape` to close it). A group with its own page links to it from the flyout heading
79
79
  - items without children show their label in a tooltip
80
80
 
81
81
  [`GlobalHeaderLayout`](./global-header-layout.md) bakes this mode in through `GlobalHeaderLayout.DefaultSidebar`.
@@ -94,7 +94,7 @@ When the app title and primary toggle live in a full-width top bar, hide the sid
94
94
  function UserProfile() {
95
95
  return (
96
96
  <div className="border-t p-4">
97
- <p className="text-sm font-medium">Ada Lovelace</p>
97
+ <Text.p variant="label-md">Ada Lovelace</Text.p>
98
98
  <button type="button" className="mt-2 text-sm text-muted-foreground">
99
99
  Logout
100
100
  </button>
@@ -68,6 +68,14 @@ function BasicUsage() {
68
68
 
69
69
  Accepts `className` and all standard HTML `<div>` props.
70
70
 
71
+ ## Tall Content
72
+
73
+ `Dialog.Content` is never taller than the viewport minus 2rem (16px above and below, the same margin as on the sides). When the content is taller, the whole dialog scrolls, including the title and the close button. `Escape` and a click on the backdrop still close it.
74
+
75
+ On open, focus moves to the first control in the dialog. In a dialog that scrolls, this happens only when that control is in view at the top. When it is below the fold (for example, a footer button after a long body), the dialog itself takes focus instead, so it opens at the top rather than scrolled to that control. `Tab` then moves to the control. Pass `initialFocus` to choose the element yourself.
76
+
77
+ To keep a header or footer in view while only the body scrolls, make `Dialog.Content` a flex column and give the body `min-h-0 flex-1 overflow-y-auto`. A `className` with its own `max-h-*` or `overflow-*` replaces the defaults.
78
+
71
79
  ### Dialog.Trigger / Dialog.Close Props
72
80
 
73
81
  Accept a `render` prop for custom element rendering (Base UI render pattern), plus standard button props.
@@ -154,9 +154,16 @@ By default the button appears where the rail would have been. To put it somewher
154
154
  </Layout.Column>
155
155
  <Layout.Column area="main">
156
156
  <DataTable.Root value={table}>
157
- <DataTable.Toolbar>
158
- <FilterRail.Trigger />
159
- </DataTable.Toolbar>
157
+ <Toolbar.Root>
158
+ <Toolbar.Row justify="between" aria-label="Table controls">
159
+ <Toolbar.Group>
160
+ <FilterRail.Trigger />
161
+ </Toolbar.Group>
162
+ <Toolbar.Group>
163
+ <DataTable.ColumnSettings />
164
+ </Toolbar.Group>
165
+ </Toolbar.Row>
166
+ </Toolbar.Root>
160
167
  <DataTable.Table />
161
168
  </DataTable.Root>
162
169
  </Layout.Column>
@@ -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`), `Tree` (checkbox mode) | 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);
@@ -58,17 +58,40 @@ function App() {
58
58
 
59
59
  ## Props
60
60
 
61
- | Prop | Type | Default | Description |
62
- | ------------- | --------------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------- |
63
- | `header` | `React.ReactNode` | `<GlobalHeaderLayout.DefaultHeader />` | Replaces the full-width app header. |
64
- | `sidebar` | `React.ReactNode` | `<GlobalHeaderLayout.DefaultSidebar />` | Replaces the primary sidebar. |
65
- | `defaultOpen` | `boolean` | `true` | Whether the sidebar starts expanded on desktop. |
66
- | `collapsible` | `boolean` | `true` | Whether the sidebar can be collapsed. |
67
- | `children` | `(props: { Outlet: () => React.ReactNode }) => React.ReactNode` | current route outlet | Custom content renderer for the stock content container. |
68
- | `body` | `React.ReactNode` | - | Escape hatch that replaces the entire region beside the sidebar. |
61
+ | Prop | Type | Default | Description |
62
+ | ------------------ | --------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
63
+ | `header` | `React.ReactNode` | `<GlobalHeaderLayout.DefaultHeader />` | Replaces the full-width app header. |
64
+ | `sidebar` | `React.ReactNode` | `<GlobalHeaderLayout.DefaultSidebar />` | Replaces the primary sidebar. |
65
+ | `defaultOpen` | `boolean` | `true` | Whether the sidebar starts expanded on desktop. Ignored in `"fixed"` and `"resize"` modes. |
66
+ | `sidebarBehaviour` | `"fixed" \| "collapse" \| "resize" \| "collapse-and-resize"` | `"collapse-and-resize"` | Selects the permitted desktop operations without restricting mobile/tablet drawer access. |
67
+ | `collapsible` | `boolean` | - | Deprecated compatibility prop: explicit `false` maps to `"fixed"` and explicit `true` to `"collapse"`. Cannot be combined with `sidebarBehaviour`. |
68
+ | `children` | `(props: { Outlet: () => React.ReactNode }) => React.ReactNode` | current route outlet | Custom content renderer for the stock content container. |
69
+ | `body` | `React.ReactNode` | - | Escape hatch that replaces the entire region beside the sidebar. |
69
70
 
70
71
  `children` and `body` are mutually exclusive. Use `body` when you need to add your own side columns around the main content.
71
72
 
73
+ ## Resizing the sidebar
74
+
75
+ `sidebarBehaviour` selects the desktop operations and defaults to `"collapse-and-resize"`. The four modes are shared with [`SidebarLayout`](./sidebar-layout.md#resizing-the-sidebar):
76
+
77
+ ```tsx
78
+ <GlobalHeaderLayout />; // Both operations are enabled by default
79
+ <GlobalHeaderLayout sidebarBehaviour="fixed" />; // Neither operation
80
+ <GlobalHeaderLayout sidebarBehaviour="collapse" />; // Collapse only
81
+ <GlobalHeaderLayout sidebarBehaviour="resize" />; // Resize only
82
+ <GlobalHeaderLayout sidebarBehaviour="collapse-and-resize" />; // Both
83
+ ```
84
+
85
+ `"fixed"` and `"resize"` keep desktop expanded, ignore `defaultOpen`, and make the hook's `setOpen()` action inert. `"fixed"` has no desktop interaction controls; `"resize"` has a resize handle but no collapse controls. `"collapse"` has collapse controls but no resize handle. Desktop toggle shortcuts are not intercepted when collapsing is disabled.
86
+
87
+ Every mode keeps the layout's normal responsive icon rail and mobile/tablet overlay, so the trigger, `toggle()`, and shortcut can still open the full menu. Desktop expansion and responsive drawer visibility are independent.
88
+
89
+ `collapsible` remains supported but deprecated. Explicit `false` maps to `"fixed"`, explicit `true` maps to `"collapse"`, and omitting both props uses the new default. TypeScript rejects specifying both props. Replace legacy `collapsible={false}` with `sidebarBehaviour="fixed"`; it keeps desktop fixed but intentionally no longer forces a fixed sidebar on mobile/tablet.
90
+
91
+ The edge resize handle and keyboard resizing match [`SidebarLayout`](./sidebar-layout.md#resizing-the-sidebar). Resize-only mode clamps pointer and keyboard widths to 192–480px without collapse resistance, and double-click only resets to 256px. With `"collapse-and-resize"`, dragging uses resistance near the rail and can open, resize, and collapse in one gesture; double-clicking at 256px collapses, and double-click or keyboard expansion restores 256px. Width is not saved across reloads. Responsive overlay widths and the collapsed rail width are unchanged.
92
+
93
+ Custom sidebars can read the effective, normalized `sidebarBehaviour` through [`useAppShellSidebar()`](./sidebar-layout.md#reading-and-controlling-the-sidebar). Its existing state and actions remain available; arbitrary custom markup still owns its presentation and sizing.
94
+
72
95
  ## Built-in parts
73
96
 
74
97
  `GlobalHeaderLayout` exposes the same composition pieces as `SidebarLayout`, plus the opinionated header and sidebar defaults:
@@ -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.
@@ -26,12 +26,12 @@ function BasicUsage() {
26
26
  return (
27
27
  <Layout>
28
28
  <Layout.Column>
29
- <h2>Main Content</h2>
30
- <p>Your main content goes here...</p>
29
+ <Text.h2>Main Content</Text.h2>
30
+ <Text.p>Your main content goes here...</Text.p>
31
31
  </Layout.Column>
32
32
  <Layout.Column>
33
- <h2>Sidebar</h2>
34
- <p>Additional info or actions...</p>
33
+ <Text.h2>Sidebar</Text.h2>
34
+ <Text.p>Additional info or actions...</Text.p>
35
35
  </Layout.Column>
36
36
  </Layout>
37
37
  );
@@ -230,7 +230,7 @@ If any `Layout.Column` has an `area` prop, all columns switch to area-based widt
230
230
  <nav>Sidebar</nav>
231
231
  </Layout.Column>
232
232
  <Layout.Column area="main">
233
- <h2>Content</h2>
233
+ <Text.h2>Content</Text.h2>
234
234
  </Layout.Column>
235
235
  </Layout>
236
236
  ```
@@ -246,9 +246,16 @@ By default, `<Layout>` grows with its content and the AppShell content area scro
246
246
  <Layout.Header title="Products" />
247
247
  <Layout.Column>
248
248
  <DataTable.Root value={table}>
249
- <DataTable.Toolbar>
250
- <DataTable.Filters />
251
- </DataTable.Toolbar>
249
+ <Toolbar.Root>
250
+ <Toolbar.Row justify="between" aria-label="Product table controls">
251
+ <Toolbar.Group>
252
+ <DataTable.Filters />
253
+ </Toolbar.Group>
254
+ <Toolbar.Group>
255
+ <DataTable.ColumnSettings />
256
+ </Toolbar.Group>
257
+ </Toolbar.Row>
258
+ </Toolbar.Root>
252
259
  <DataTable.Table />
253
260
  <DataTable.Footer>
254
261
  <DataTable.Pagination />
@@ -310,12 +317,12 @@ function OrderDetailPage() {
310
317
  </Layout.Column>
311
318
  <Layout.Column>
312
319
  <Card>
313
- <h3>Quick Actions</h3>
320
+ <Text.h3>Quick Actions</Text.h3>
314
321
  <Button fullWidth>Mark as Shipped</Button>
315
322
  <Button fullWidth>Send Invoice</Button>
316
323
  </Card>
317
324
  <Card>
318
- <h3>Order History</h3>
325
+ <Text.h3>Order History</Text.h3>
319
326
  <Timeline events={order.history} />
320
327
  </Card>
321
328
  </Layout.Column>
@@ -333,7 +340,7 @@ function Dashboard() {
333
340
  <Layout.Header title="Dashboard" />
334
341
  <Layout.Column>
335
342
  <Card>
336
- <h3>Navigation</h3>
343
+ <Text.h3>Navigation</Text.h3>
337
344
  <nav>
338
345
  <a href="#overview">Overview</a>
339
346
  <a href="#sales">Sales</a>
@@ -343,17 +350,17 @@ function Dashboard() {
343
350
  </Layout.Column>
344
351
  <Layout.Column>
345
352
  <Card>
346
- <h2>Sales Overview</h2>
353
+ <Text.h2>Sales Overview</Text.h2>
347
354
  <Chart data={salesData} />
348
355
  </Card>
349
356
  <Card>
350
- <h2>Recent Orders</h2>
357
+ <Text.h2>Recent Orders</Text.h2>
351
358
  <OrdersTable orders={recentOrders} />
352
359
  </Card>
353
360
  </Layout.Column>
354
361
  <Layout.Column>
355
362
  <Card>
356
- <h3>Activity Feed</h3>
363
+ <Text.h3>Activity Feed</Text.h3>
357
364
  <ActivityList activities={activities} />
358
365
  </Card>
359
366
  </Layout.Column>
@@ -381,7 +388,7 @@ function ProductEditor() {
381
388
  />
382
389
  <Layout.Column>
383
390
  <Card>
384
- <h3>Product Details</h3>
391
+ <Text.h3>Product Details</Text.h3>
385
392
  <Input
386
393
  label="Name"
387
394
  value={product.name}
@@ -402,7 +409,7 @@ function ProductEditor() {
402
409
  </Layout.Column>
403
410
  <Layout.Column>
404
411
  <Card>
405
- <h3>Preview</h3>
412
+ <Text.h3>Preview</Text.h3>
406
413
  <ProductCard product={product} />
407
414
  </Card>
408
415
  </Layout.Column>
@@ -439,7 +446,7 @@ function EditorPage() {
439
446
  <Layout.Header title="Settings" actions={[<Button key="save">Save Changes</Button>]} />
440
447
  <Layout.Column>
441
448
  <Card>
442
- <h3>Account Settings</h3>
449
+ <Text.h3>Account Settings</Text.h3>
443
450
  <SettingsForm />
444
451
  </Card>
445
452
  </Layout.Column>
@@ -161,6 +161,14 @@ type SelectAsyncFetcher<T> = (options: { signal: AbortSignal }) => Promise<T[]>;
161
161
 
162
162
  If the fetcher throws or rejects, `Select.Async` renders a built-in inline error state in the dropdown — the `errorText` message plus a **Retry** button that re-runs the fetch — instead of the misleading empty state. Aborted requests (the dropdown closed before the fetch settled) are ignored, and announcements are de-duped per outage. Pass `onFetchError` to run a side effect (logging, toast) — it fires once per outage and re-arms after the next successful fetch.
163
163
 
164
+ ## Long Lists
165
+
166
+ The list is at most 15rem tall and scrolls when there are more options. To change the height, compose with `Select.Parts` and pass a plain class with a trailing `!` to `Content`:
167
+
168
+ ```tsx
169
+ <Select.Parts.Content className="max-h-96!">{/* items */}</Select.Parts.Content>
170
+ ```
171
+
164
172
  ## Low-level Primitives
165
173
 
166
174
  `Select.Parts` exposes the styled sub-components for fully custom compositions:
@@ -171,6 +179,16 @@ const { Root, Trigger, Value, Content, Item, Group, GroupLabel, Separator } = Se
171
179
 
172
180
  ## Form submission
173
181
 
182
+ Inside a form, place `Select` directly under `Field.Root`, not inside `Field.Control`. `Select` draws
183
+ its own trigger and registers with the field itself.
184
+
185
+ ```tsx
186
+ <Field.Root name="country">
187
+ <Field.Label>Country</Field.Label>
188
+ <Select items={["Japan", "United States"]} placeholder="Select a country" />
189
+ </Field.Root>
190
+ ```
191
+
174
192
  Inside a `Field.Root`, the field's `name` identifies the value and these props are unnecessary —
175
193
  see [Form](./form.md). They matter for **native** submission: a plain `<form>`, `new FormData(form)`,
176
194
  or a server action.
@@ -32,13 +32,13 @@ import { Package } from "lucide-react";
32
32
 
33
33
  ## Props
34
34
 
35
- | Prop | Type | Default | Description |
36
- | ------------- | --------------------------- | ------------ | ---------------------------------------------- |
37
- | `title` | `string \| LocalizedString` | **Required** | Group title (supports i18n) |
38
- | `icon` | `React.ReactNode` | - | Icon displayed next to title |
39
- | `to` | `string` | - | Makes title clickable (navigates to URL) |
40
- | `defaultOpen` | `boolean` | `true` | Initial expanded/collapsed state |
41
- | `children` | `React.ReactNode` | **Required** | Child items (SidebarItem, nested groups, etc.) |
35
+ | Prop | Type | Default | Description |
36
+ | ------------- | --------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | `title` | `string \| LocalizedString` | **Required** | Group title (supports i18n) |
38
+ | `icon` | `React.ReactNode` | - | Icon displayed next to title |
39
+ | `to` | `string` | - | Makes title clickable (navigates to URL). In the icon rail on a touch device, tapping the group opens its flyout instead; the page is linked from the flyout heading |
40
+ | `defaultOpen` | `boolean` | `true` | Initial expanded/collapsed state |
41
+ | `children` | `React.ReactNode` | **Required** | Child items (SidebarItem, nested groups, etc.) |
42
42
 
43
43
  ## Features
44
44
 
@@ -168,6 +168,8 @@ import { Settings } from "lucide-react";
168
168
 
169
169
  Clicking "Settings" navigates to `/settings`, while child items navigate to their specific pages.
170
170
 
171
+ In the collapsed icon rail, the group icon is the link on a mouse device, and its flyout opens on hover. On a touch device the icon opens the flyout on tap, and "Settings" is the link at the top of the flyout.
172
+
171
173
  ### Multiple Groups
172
174
 
173
175
  ```tsx