@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
@@ -48,12 +48,13 @@ This gives you:
48
48
 
49
49
  ### Common props
50
50
 
51
- | Prop | Type | Default | Description |
52
- | ------------- | ----------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ |
53
- | `sidebar` | `React.ReactNode` | `<SidebarLayout.DefaultSidebar />` | Replaces the entire sidebar region. |
54
- | `defaultOpen` | `boolean` | `true` | Whether the sidebar starts expanded on desktop. |
55
- | `collapsible` | `boolean` | `true` | Whether the sidebar can be collapsed. When `false`, toggle controls are hidden and `defaultOpen` is ignored. |
56
- | `topBar` | `React.ReactNode` | - | Full-width bar above both the sidebar and the content region. |
51
+ | Prop | Type | Default | Description |
52
+ | ------------------ | ------------------------------------------------------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | `sidebar` | `React.ReactNode` | `<SidebarLayout.DefaultSidebar />` | Replaces the entire sidebar region. |
54
+ | `defaultOpen` | `boolean` | `true` | Whether the sidebar starts expanded on desktop. Ignored in `"fixed"` and `"resize"` modes. |
55
+ | `sidebarBehaviour` | `"fixed" \| "collapse" \| "resize" \| "collapse-and-resize"` | `"collapse-and-resize"` | Selects the permitted desktop operations. Mobile/tablet drawer access is independent of this setting. |
56
+ | `collapsible` | `boolean` | - | Deprecated compatibility prop: explicit `false` maps to `"fixed"` and explicit `true` to `"collapse"`. Cannot be combined with `sidebarBehaviour`. |
57
+ | `topBar` | `React.ReactNode` | - | Full-width bar above both the sidebar and the content region. |
57
58
 
58
59
  ### Default content mode
59
60
 
@@ -74,6 +75,61 @@ Use this when you need to compose your own columns beside the sidebar.
74
75
 
75
76
  `body` is mutually exclusive with `header` and `children`, because it replaces the region those props normally describe.
76
77
 
78
+ ## Resizing the sidebar
79
+
80
+ `sidebarBehaviour` selects the desktop operations. It defaults to `"collapse-and-resize"`:
81
+
82
+ | Mode | Collapse | Resize |
83
+ | ----------------------- | -------- | ------ |
84
+ | `"fixed"` | No | No |
85
+ | `"collapse"` | Yes | No |
86
+ | `"resize"` | No | Yes |
87
+ | `"collapse-and-resize"` | Yes | Yes |
88
+
89
+ ```tsx
90
+ <SidebarLayout />; // Both operations are enabled by default
91
+ <SidebarLayout sidebarBehaviour="fixed" />;
92
+ <SidebarLayout sidebarBehaviour="collapse" />;
93
+ <SidebarLayout sidebarBehaviour="resize" />;
94
+ <SidebarLayout sidebarBehaviour="collapse-and-resize" />;
95
+ ```
96
+
97
+ `"fixed"` and `"resize"` always keep the desktop sidebar expanded, ignore `defaultOpen`, and make `useAppShellSidebar().setOpen()` inert. They hide desktop collapse controls and do not intercept the desktop toggle shortcut. `"collapse"` has toggle controls but no resize handle.
98
+
99
+ All modes keep the layout's normal mobile/tablet drawer and icon-rail overlay presentation. The trigger, `toggle()`, and shortcut can still open the full menu when desktop collapsing is disabled. These responsive visibility states are independent of desktop expansion.
100
+
101
+ ### Deprecated `collapsible`
102
+
103
+ The existing boolean remains supported but is deprecated. Explicit `false` maps to `"fixed"`; explicit `true` maps to `"collapse"`. Omitting both props uses the new `"collapse-and-resize"` default. TypeScript rejects specifying both props together.
104
+
105
+ ```tsx
106
+ // Before
107
+ <SidebarLayout collapsible={false} />;
108
+
109
+ // After
110
+ <SidebarLayout sidebarBehaviour="fixed" />;
111
+ ```
112
+
113
+ This intentionally changes the responsive presentation of legacy `collapsible={false}`: it no longer forces a fixed sidebar on mobile/tablet. It keeps desktop fixed while using the normal responsive drawer/overlay. A completely custom `sidebar` still owns its own presentation and sizing.
114
+
115
+ ### Resize interaction
116
+
117
+ The initial width is 256px. In `"resize"` and `"collapse-and-resize"` modes, the whole outer edge of the expanded desktop sidebar is a resize handle. Hovering it shows the resize cursor and a small pill beside the border that follows the pointer. Drag the edge, or focus the handle and use Left/Right arrows (16px steps), Home, or End (480px). Double-click restores the default 256px width.
118
+
119
+ In `"resize"` mode, dragging follows the pointer and clamps to 192–480px without collapse resistance. Home and Left at the minimum keep the sidebar at 192px; double-clicking at 256px does not collapse it.
120
+
121
+ In `"collapse-and-resize"` mode, Home or Left at the minimum collapses the sidebar, and double-clicking at 256px collapses it. The collapsed desktop sidebar keeps its handle. Double-clicking it or using Enter/Space restores 256px; a single pointer click does nothing.
122
+
123
+ In `"collapse-and-resize"` mode, dragging uses a position-based resistance curve, not a delayed CSS transition. Between the collapsed width and the minimum expanded width (192px), most of the interval uses a 25% movement gain: moving the pointer 10px moves the edge only 2.5px. In the final quarter near the opposite boundary, a smooth position-based blend catches up to the pointer. The edge stops when the pointer stops; it does not catch up over time. At 192px and above it follows the pointer 1:1, up to 480px. Shrinking through the same range resists toward the collapsed position. It collapses slightly early, when the pointer enters the final 20% of that interval, with the curve already meeting the collapsed width at that point. The same pointer stays captured across both transitions, without a jump to 256px.
124
+
125
+ With collapsing enabled, releasing inside the resistance range settles back to the current mode's boundary (collapsed width or 192px).
126
+
127
+ Live drag pixels are applied through CSS custom properties rather than React state on every pointer movement. React commits the width at mode changes and when the gesture ends; ARIA values stay current during dragging. Width changes still require browser layout because the content column resizes live.
128
+
129
+ Width is not persisted across remounts or reloads. The icon rail keeps its existing collapsed width. Mobile drawers and tablet overlays keep their existing widths and do not show handles.
130
+
131
+ This applies to the built-in sidebar primitives, including `SidebarLayout.DefaultSidebar`. An arbitrary custom `sidebar` element still owns its own sizing.
132
+
77
133
  ## Extending the built-in header
78
134
 
79
135
  The supported "small customization" path is to pass `SidebarLayout.DefaultHeader` into the `header` slot and fill its `actions` cluster.
@@ -162,26 +218,33 @@ Use `body` when the stock content column is too restrictive and you need your ow
162
218
 
163
219
  ## Reading and controlling the sidebar
164
220
 
165
- Use `useAppShellSidebar()` to read and control the sidebar state instead of observing internal DOM attributes or clicking the trigger through the DOM.
221
+ Use `useAppShellSidebar()` to read and control the sidebar state instead of observing internal DOM attributes or clicking the trigger through the DOM. It also exposes the effective `sidebarBehaviour`, already normalized from either public prop, so custom sidebars do not need to handle the deprecated boolean themselves.
222
+
223
+ `open` and `setOpen()` describe desktop expansion. Drawer visibility is separate: `toggle()` selects the mobile drawer, tablet overlay, or desktop state according to the viewport. The four existing hook fields (`open`, `isMobile`, `setOpen`, and `toggle`) remain available.
166
224
 
167
225
  ```tsx
168
226
  import { useAppShellSidebar } from "@tailor-platform/app-shell";
169
227
 
170
228
  function CustomHeader() {
171
- const { open, isMobile, toggle } = useAppShellSidebar();
229
+ const { sidebarBehaviour, open, isMobile, toggle } = useAppShellSidebar();
172
230
 
173
231
  return (
174
232
  <div className="flex items-center justify-between border-b px-4 py-3">
175
233
  <button type="button" onClick={toggle}>
176
- {open ? "Hide navigation" : "Show navigation"}
234
+ Toggle navigation
177
235
  </button>
178
- <span>{isMobile ? "Mobile" : "Desktop"}</span>
236
+ <span>
237
+ {sidebarBehaviour}: {open ? "Desktop expanded" : "Desktop collapsed"}
238
+ </span>
239
+ <span>{isMobile ? "Mobile" : "Non-mobile"}</span>
179
240
  </div>
180
241
  );
181
242
  }
182
243
  ```
183
244
 
184
- Outside `SidebarLayout`, the hook returns a safe fallback (`open: true`, `isMobile: false`, no-op setters) rather than throwing.
245
+ Outside `SidebarLayout`, the hook returns a safe fallback (`sidebarBehaviour: "fixed"`, `open: true`, `isMobile: false`, no-op actions) rather than throwing. This fallback describes the absence of a managed sidebar; it is not the layout's default mode.
246
+
247
+ The desktop capabilities can be derived from the reported mode: `"collapse"` and `"collapse-and-resize"` permit collapse, while `"resize"` and `"collapse-and-resize"` permit resizing. Reading this setting does not automatically add resize behavior to arbitrary custom markup.
185
248
 
186
249
  ## Reusing the built-in breadcrumb
187
250
 
@@ -90,14 +90,14 @@ const labels = defineI18nLabels({
90
90
  - The items belong under a parent with its own icon (or landing page via `to`)
91
91
  - You need nesting
92
92
 
93
- | Feature | SidebarSection | SidebarGroup |
94
- | ------------------- | ----------------- | ------------------------- |
95
- | **Collapsible** | ❌ No | ✅ Yes |
96
- | **Heading is link** | ❌ No | Optional (`to` prop) |
97
- | **Icon** | ❌ No | Optional |
98
- | **Child styling** | Same as top level | Indented submenu |
99
- | **Icon rail** | Heading hidden | Group icon opens a flyout |
100
- | **Nesting** | Not intended | Can contain groups |
93
+ | Feature | SidebarSection | SidebarGroup |
94
+ | ------------------- | ----------------- | -------------------------------------------------- |
95
+ | **Collapsible** | ❌ No | ✅ Yes |
96
+ | **Heading is link** | ❌ No | Optional (`to` prop) |
97
+ | **Icon** | ❌ No | Optional |
98
+ | **Child styling** | Same as top level | Indented submenu |
99
+ | **Icon rail** | Heading hidden | Group icon opens a flyout (hover, or tap on touch) |
100
+ | **Nesting** | Not intended | Can contain groups |
101
101
 
102
102
  ## Accessibility
103
103
 
@@ -41,7 +41,7 @@ function BasicUsage() {
41
41
  | Sub-component | Description |
42
42
  | ------------- | -------------------------------------------------------------- |
43
43
  | `Tabs.Root` | Manages tab selection state |
44
- | `Tabs.List` | Groups the individual tab buttons |
44
+ | `Tabs.List` | Groups the individual tab buttons on one scrolling row |
45
45
  | `Tabs.Tab` | An interactive tab button that toggles the corresponding panel |
46
46
  | `Tabs.Panel` | A panel displayed when the corresponding tab is active |
47
47
 
@@ -61,7 +61,9 @@ function BasicUsage() {
61
61
 
62
62
  ### Tabs.List Props
63
63
 
64
- Accepts `className` and all standard HTML `<div>` props.
64
+ Accepts `className` and all standard HTML `<div>` props except `render`. The
65
+ HTML props apply to the `tablist` element, which is also the scroll container;
66
+ `className` applies to the visual track inside it (see [Overflow](#overflow)).
65
67
 
66
68
  ### Tabs.Tab Props
67
69
 
@@ -96,6 +98,34 @@ const [activeTab, setActiveTab] = useState("overview");
96
98
  </Tabs.Root>;
97
99
  ```
98
100
 
101
+ ## Overflow
102
+
103
+ Tabs stay on one row. When they are wider than the space available, the list
104
+ scrolls sideways instead of wrapping or growing the page. The scrollbar is
105
+ hidden: touch, a trackpad, Shift + mouse wheel, and the arrow keys scroll it,
106
+ and the tab cut off at the edge shows that there is more. Base UI scrolls the
107
+ active tab into view when the tabs mount, and scrolls the tab that the arrow
108
+ keys, Home or End move focus to into view, so keyboard navigation never lands
109
+ on a hidden tab. Nothing changes while the tabs fit.
110
+
111
+ To make this work, the `tablist` element (`data-slot="tabs-list"`) is the
112
+ scroll container, and the tabs sit in a visual track inside it
113
+ (`data-slot="tabs-list-track"`) that carries the variant styles and your
114
+ `className`. The scroll container has 4px of padding, a matching negative
115
+ margin and the same `scroll-padding`, so the tabs' focus ring is not clipped
116
+ and the list keeps its footprint. A `Tabs.List` placed next to inline content
117
+ starts on its own line.
118
+
119
+ Two cases are not scrolled into view automatically: a tab selected by changing
120
+ a controlled `value` from outside (a deep link, for example), and a list that
121
+ mounts while hidden, such as inside a closed dialog. Call
122
+ `scrollIntoView({ inline: "nearest", block: "nearest" })` on the tab if you
123
+ need either.
124
+
125
+ Scrolling keeps every tab reachable, but it does not shorten the list. If a
126
+ list regularly overflows at the widths your users have, prefer fewer tabs or
127
+ another control, such as a `Select`, over relying on the scroll.
128
+
99
129
  ## Examples
100
130
 
101
131
  ### With Disabled Tab
@@ -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`.