@tailor-platform/app-shell 1.16.0 → 1.18.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 (45) hide show
  1. package/dist/app-shell-core.css +1 -1
  2. package/dist/app-shell.d.ts +762 -51
  3. package/dist/app-shell.js +7642 -5636
  4. package/dist/{appshell-B0wh5Lq0.js → appshell-Y1AYCuVJ.js} +323 -304
  5. package/dist/semantic-contrast.test.ts +227 -0
  6. package/dist/style-entry.d.ts +61 -26
  7. package/dist/testing.d.ts +66 -29
  8. package/dist/testing.js +1 -1
  9. package/dist/theme-bridge.test.ts +124 -1
  10. package/dist/theme.bridge.css +178 -0
  11. package/dist/theme.css +8 -2
  12. package/dist/themes/_template.css +8 -3
  13. package/dist/themes/bloom.css +0 -8
  14. package/dist/themes/cream.css +0 -8
  15. package/dist/themes/default.css +458 -103
  16. package/dist/vite-plugin.d.ts +61 -26
  17. package/package.json +2 -2
  18. package/skills/app-shell-patterns/SKILL.md +48 -37
  19. package/skills/app-shell-patterns/references/api/define-i18n-labels.md +123 -91
  20. package/skills/app-shell-patterns/references/api/define-module.md +4 -6
  21. package/skills/app-shell-patterns/references/api/define-resource.md +2 -2
  22. package/skills/app-shell-patterns/references/api/use-app-shell-config.md +53 -47
  23. package/skills/app-shell-patterns/references/api/use-app-shell-data.md +30 -26
  24. package/skills/app-shell-patterns/references/api/use-app-shell.md +37 -30
  25. package/skills/app-shell-patterns/references/api/use-toast.md +28 -31
  26. package/skills/app-shell-patterns/references/components/app-shell.md +4 -4
  27. package/skills/app-shell-patterns/references/components/card.md +79 -7
  28. package/skills/app-shell-patterns/references/components/data-table.md +599 -77
  29. package/skills/app-shell-patterns/references/components/date-picker.md +32 -21
  30. package/skills/app-shell-patterns/references/components/filter-rail.md +165 -0
  31. package/skills/app-shell-patterns/references/components/sidebar-group.md +12 -6
  32. package/skills/app-shell-patterns/references/components/sidebar-section.md +114 -0
  33. package/skills/app-shell-patterns/references/components/text.md +198 -0
  34. package/skills/app-shell-patterns/references/concepts/custom-components.md +1 -1
  35. package/skills/app-shell-patterns/references/concepts/file-based-routing.md +7 -1
  36. package/skills/app-shell-patterns/references/concepts/graphql.md +20 -14
  37. package/skills/app-shell-patterns/references/concepts/routing-navigation.md +41 -3
  38. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +32 -0
  39. package/skills/app-shell-patterns/references/concepts/styling-theming.md +278 -65
  40. package/skills/app-shell-patterns/references/migrations.md +43 -1
  41. package/skills/app-shell-patterns/references/pages/document-detail.md +6 -6
  42. package/skills/app-shell-patterns/references/patterns/form-modal.md +26 -20
  43. package/skills/app-shell-patterns/references/patterns/i18n-labels.md +139 -0
  44. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +129 -118
  45. package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +5 -4
@@ -181,6 +181,14 @@ Locale and timezone come from AppShell automatically. Override per field with `l
181
181
  <DatePicker aria-label="Date" locale="ja-JP" />
182
182
  ```
183
183
 
184
+ ### Day of the week
185
+
186
+ `showDayOfWeek` adds the locale's short day of the week where the locale places it — `2025/12/19(金)` for ja-JP, `Fri, 12/19/2025` for en-US. It is read-only and derived from the entered date: it shows a placeholder until the date is complete, and while a date segment is mid-entry it keeps the previous day of the week (muted), so typing `25` into the day never flashes the 2nd's. Screen readers hear the full day name with each date segment's value.
187
+
188
+ ```tsx
189
+ <DatePicker aria-label="Closing date" showDayOfWeek />
190
+ ```
191
+
184
192
  ## Keyboard
185
193
 
186
194
  - **Segments:** `↑`/`↓` increment/decrement, digits type-to-fill (auto-advance), `←`/`→` move between segments, `Backspace` clears, `/` commits the current segment and advances.
@@ -191,26 +199,29 @@ Locale and timezone come from AppShell automatically. Override per field with `l
191
199
 
192
200
  ### DateFieldProps
193
201
 
194
- | Prop | Type | Description |
195
- | -------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------- |
196
- | `value` / `defaultValue` | `DateValue \| null` | Controlled / uncontrolled value |
197
- | `onChange` | `(v: DateValue \| null) => void` | Fires when the value changes |
198
- | `onBlur` | `() => void` | Fires when focus leaves the whole segmented control |
199
- | `minValue` / `maxValue` | `DateValue` | Inclusive date range bounds |
200
- | `isDateUnavailable` | `(date: DateValue) => boolean` | Marks specific dates unavailable |
201
- | `isDisabled` | `boolean` | Disables interaction and form submission |
202
- | `isReadOnly` | `boolean` | Allows focus/navigation without editing |
203
- | `isRequired` | `boolean` | Marks the control required |
204
- | `isInvalid` | `boolean` | Adds invalid styling / `aria-invalid` to the segmented UI |
205
- | `placeholderValue` | `DateValue` | Seeds unset segments |
206
- | `autoFocus` | `boolean` | Focus the first segment on mount |
207
- | `locale` | `string` | BCP-47 locale override |
208
- | `name` | `string` | Emits a form value through the proxy input |
209
- | `id` | `string` | Proxy input id (use with external `<label htmlFor>`). |
210
- | `firstDayOfWeek` | `"sun" \| "mon" \| "tue" \| "wed" \| "thu" \| "fri" \| "sat"` | Override the locale week start used by `w` / `k` shortcuts |
211
- | `aria-label` / `aria-labelledby` | `string` | Accessible name |
212
- | `aria-describedby` | `string` | IDs of description / error elements |
213
- | `className` | `string` | Root element class |
202
+ | Prop | Type | Description |
203
+ | -------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------ |
204
+ | `value` / `defaultValue` | `DateValue \| null` | Controlled / uncontrolled value |
205
+ | `onChange` | `(v: DateValue \| null) => void` | Fires when the value changes |
206
+ | `onBlur` | `() => void` | Fires when focus leaves the whole segmented control |
207
+ | `minValue` / `maxValue` | `DateValue` | Inclusive date range bounds |
208
+ | `isDateUnavailable` | `(date: DateValue) => boolean` | Marks specific dates unavailable |
209
+ | `isDisabled` | `boolean` | Disables interaction and form submission |
210
+ | `isReadOnly` | `boolean` | Allows focus/navigation without editing |
211
+ | `isRequired` | `boolean` | Marks the control required |
212
+ | `isInvalid` | `boolean` | Adds invalid styling / `aria-invalid` to the segmented UI |
213
+ | `placeholderValue` | `DateValue` | Seeds unset segments |
214
+ | `granularity` | `"day" \| "hour" \| "minute" \| "second"` | Smallest editable unit; time granularities add time segments |
215
+ | `hourCycle` | `12 \| 24` | Force 12- or 24-hour time; defaults to the locale |
216
+ | `autoFocus` | `boolean` | Focus the first segment on mount |
217
+ | `locale` | `string` | BCP-47 locale override |
218
+ | `showDayOfWeek` | `boolean` | Show the locale's short day of the week next to the date |
219
+ | `name` | `string` | Emits a form value through the proxy input |
220
+ | `id` | `string` | Proxy input id (use with external `<label htmlFor>`). |
221
+ | `firstDayOfWeek` | `"sun" \| "mon" \| "tue" \| "wed" \| "thu" \| "fri" \| "sat"` | Override the locale week start used by `w` / `k` shortcuts |
222
+ | `aria-label` / `aria-labelledby` | `string` | Accessible name |
223
+ | `aria-describedby` | `string` | IDs of description / error elements |
224
+ | `className` | `string` | Root element class |
214
225
 
215
226
  ### DatePickerProps
216
227
 
@@ -227,7 +238,7 @@ See the calendar docs in-code: controlled/uncontrolled value, min/max, unavailab
227
238
 
228
239
  ### DateRangePickerProps
229
240
 
230
- The `DatePickerProps` surface (labeling, `isInvalid`, `min/maxValue`, `isDateUnavailable`, `granularity`, `firstDayOfWeek`, `timeZone`, `locale`, `autoFocus`, `isDisabled/ReadOnly/Required`), with the range-specific differences:
241
+ The `DatePickerProps` surface (labeling, `isInvalid`, `min/maxValue`, `isDateUnavailable`, `granularity`, `hourCycle`, `firstDayOfWeek`, `timeZone`, `locale`, `showDayOfWeek`, `autoFocus`, `isDisabled/ReadOnly/Required`), with the range-specific differences:
231
242
 
232
243
  | Prop | Type | Description |
233
244
  | ------------------------ | -------------------------------- | ------------------------------------------------------------------------------- |
@@ -0,0 +1,165 @@
1
+ ---
2
+ title: FilterRail
3
+ description: An always-visible faceted filter rail with user layout settings, driven by the same CollectionControl as DataTable
4
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/components/filter-rail.docs.outline.md — do not edit by hand. -->
7
+
8
+ # FilterRail
9
+
10
+ `FilterRail` shows every filterable axis beside the results instead of hiding it behind a filter dropdown. Each section shows its options up front, so users see what they can filter by without opening anything.
11
+
12
+ It writes the same `CollectionControl` a `DataTable` reads, so there is no second filter engine: a checkbox section **is** the `in` operator, a radio section is `eq`, a range is `between`.
13
+
14
+ ```tsx
15
+ import { FilterRail, type FilterRailSection } from "@tailor-platform/app-shell";
16
+
17
+ const sections: FilterRailSection[] = [
18
+ {
19
+ id: "category",
20
+ label: "Category",
21
+ control: "checkbox",
22
+ field: "category",
23
+ options: categories,
24
+ },
25
+ { id: "status", label: "Status", control: "radio", field: "status", options: statuses },
26
+ ];
27
+
28
+ <FilterRail.Root control={control} sections={sections}>
29
+ <FilterRail.Sections />
30
+ </FilterRail.Root>;
31
+ ```
32
+
33
+ ## Sections and parts
34
+
35
+ What the rail shows is **config**: `sections`, one entry per axis, in screen order. How the rail is assembled is **parts**: leave a part out and that feature is not rendered.
36
+
37
+ | Part | Renders |
38
+ | --------------------- | ----------------------------------------------------------------------------- |
39
+ | `FilterRail.Root` | The rail. Takes `control`, `sections`, `layout`. |
40
+ | `FilterRail.Header` | A title row with a slot for actions. |
41
+ | `FilterRail.ClearAll` | Clears the rail's own filters only. Hidden while none are active. |
42
+ | `FilterRail.Settings` | Show / hide, reorder and sort sections. Needs `layout`. |
43
+ | `FilterRail.Sections` | The sections, in the user's layout. Scrolls while the header stays put. |
44
+ | `FilterRail.Provider` | Holds the config when a `Trigger` sits outside the rail (see Narrow screens). |
45
+ | `FilterRail.Trigger` | The narrow-screen "Filters" button, placed anywhere inside `Provider`. |
46
+
47
+ ```tsx
48
+ <FilterRail.Root control={control} sections={sections} layout={layout}>
49
+ <FilterRail.Header>
50
+ <FilterRail.ClearAll />
51
+ <FilterRail.Settings />
52
+ </FilterRail.Header>
53
+ <FilterRail.Sections />
54
+ </FilterRail.Root>
55
+ ```
56
+
57
+ ### Section types
58
+
59
+ Every section has an `id`, a `label` and an optional `hint`. The operator is fixed per section — the user picks values, never operators.
60
+
61
+ | `control` | Commits | Notes |
62
+ | ------------- | ------------------------------------------ | ----------------------------------------------------------- |
63
+ | `checkbox` | `in` (or `nin`) with the ticked values | `sort`, `truncate` (default 8), `pinned`, `searchThreshold` |
64
+ | `radio` | `eq` (or `ne`) with one value | An "Any" row removes the filter; `anyLabel: null` hides it |
65
+ | `boolean` | `eq` with `true` / `false` | Three states: Any, `trueLabel`, `falseLabel` |
66
+ | `text` | `contains`, `hasPrefix` or `eq` | Debounced (`debounceMs`, default 250) |
67
+ | `numberRange` | `between` `{ min, max }`, or `gte` / `lte` | Commits on blur / Enter; one end alone is half-open |
68
+ | `date` | `eq`, `gte` or `lte` a `YYYY-MM-DD` | Uses `DatePicker` |
69
+ | `dateRange` | `between` `{ min, max }` | Uses `DateRangePicker`; optional `presets` |
70
+ | `custom` | Whatever `render` writes | Declares the `fields` it owns so Clear all includes them |
71
+
72
+ A checkbox list long enough to be truncated also gets a search box, so a value can be found without scrolling (`searchThreshold` overrides when it appears).
73
+
74
+ A field holds one filter, so two sections must not share a field; the rail warns in the console when they do.
75
+
76
+ #### Typing fields
77
+
78
+ Give `FilterRailSection` the row's keys so a `field` that is not on the table is a type error, the same way `createColumnHelper<Row>()` types columns. This works with a typed control or a plain one:
79
+
80
+ ```tsx
81
+ const sections: FilterRailSection<keyof Product & string>[] = [
82
+ { id: "category", label: "Category", control: "checkbox", field: "category", options },
83
+ // field: "categroy" → type error
84
+ ];
85
+ ```
86
+
87
+ With a typed control (from `useCollectionVariables({ tableMetadata })`), a section whose field the control does not have is also rejected.
88
+
89
+ ```tsx
90
+ const stockLevel: FilterRailSection = {
91
+ id: "stockLevel",
92
+ label: "Stock level",
93
+ control: "custom",
94
+ fields: ["stock"],
95
+ render: ({ getFilter, setFilter }) => (
96
+ <Button
97
+ variant="outline"
98
+ size="xs"
99
+ onClick={() => setFilter("stock", { operator: "between", value: { min: 1, max: 20 } })}
100
+ >
101
+ Low stock
102
+ </Button>
103
+ ),
104
+ };
105
+ ```
106
+
107
+ The rail never hides state: a selected option is never truncated away, never disabled and never hidden by a section's search box.
108
+
109
+ ### Collapsing sections
110
+
111
+ Each section collapses and expands from the chevron by its title. Sections start expanded; set `defaultCollapsed: true` to start one collapsed (for a rarely used axis). While a section is collapsed, its header still shows how many values are selected and keeps its **Clear**, so a collapsed section never hides an applied filter. The open / closed state is kept while the page is open, including across the narrow-screen sheet opening and closing.
112
+
113
+ ```tsx
114
+ { id: "warehouse", label: "Warehouse", control: "checkbox", field: "warehouse", options, defaultCollapsed: true }
115
+ ```
116
+
117
+ ## Choosing a control
118
+
119
+ Checkbox and radio sections list every option, so they are only for **small, fixed sets** whose values you know up front — status, category, size, a handful of warehouses. Their `options` are a static array; the rail does not fetch them.
120
+
121
+ Do **not** use a checkbox or radio list for a field whose values **keep growing** — product names, SKUs, customers, suppliers, order numbers. The list gets longer with every new record, takes over the rail, and becomes a scroll hunt. Give those fields a search instead:
122
+
123
+ | The field's values are… | Use |
124
+ | ------------------------------------------------ | --------------------------------------------------------------------------- |
125
+ | A small fixed set (≈ up to 20), pick any | `checkbox` |
126
+ | A small fixed set, pick one | `radio` (or `boolean` for yes / no) |
127
+ | Open-ended and growing (names, codes, customers) | `text` — a search that commits `contains`, `hasPrefix` or `eq` |
128
+ | Open-ended, but users must pick exact records | a `custom` section rendering a searchable picker (e.g. an async `Combobox`) |
129
+ | Numbers or dates | `numberRange`, `date`, `dateRange` |
130
+
131
+ In development the rail warns in the console when a checkbox or radio section lists more than 50 options, as a nudge towards a search.
132
+
133
+ ## User layout
134
+
135
+ `useFilterRailLayout(storageKey)` persists the user's section order, hidden sections and option sort to localStorage. Pass it as `layout` and add `FilterRail.Settings`. Hiding a section that carries a filter also clears that filter. To share a layout across a team, pass your own `{ value, onChange, onReset }` binding instead.
136
+
137
+ ## Beside a DataTable
138
+
139
+ The rail sits in its own `Layout.Column`, outside `DataTable.Root`, which is why `control` is a prop. Do not also render `DataTable.Filters` for the same fields — one fact in two places can drift.
140
+
141
+ ## Narrow screens
142
+
143
+ Below the `lg` breakpoint there is no room for a side column, so the rail turns itself into a left sheet behind a **Filters** button carrying the active count — the same way the Sidebar handles small screens. Apps write no breakpoint code.
144
+
145
+ By default the button appears where the rail would have been. To put it somewhere else — a table toolbar, a page header — wrap the page in `FilterRail.Provider`, move the config there, and place `FilterRail.Trigger` wherever you want it. The Trigger renders nothing on wide screens, and while one is mounted the rail skips its own button.
146
+
147
+ ```tsx
148
+ <FilterRail.Provider control={control} sections={sections}>
149
+ <Layout fill>
150
+ <Layout.Column area="left">
151
+ <FilterRail.Root>
152
+ <FilterRail.Sections />
153
+ </FilterRail.Root>
154
+ </Layout.Column>
155
+ <Layout.Column area="main">
156
+ <DataTable.Root value={table}>
157
+ <DataTable.Toolbar>
158
+ <FilterRail.Trigger />
159
+ </DataTable.Toolbar>
160
+ <DataTable.Table />
161
+ </DataTable.Root>
162
+ </Layout.Column>
163
+ </Layout>
164
+ </FilterRail.Provider>
165
+ ```
@@ -124,13 +124,13 @@ Titles support i18n with `defineI18nLabels`:
124
124
  import { defineI18nLabels } from "@tailor-platform/app-shell";
125
125
 
126
126
  const labels = defineI18nLabels({
127
- products: {
128
- en: "Products",
129
- ja: "商品",
127
+ en: {
128
+ products: "Products",
129
+ settings: "Settings",
130
130
  },
131
- settings: {
132
- en: "Settings",
133
- ja: "設定",
131
+ ja: {
132
+ products: "商品",
133
+ settings: "設定",
134
134
  },
135
135
  });
136
136
 
@@ -381,6 +381,11 @@ The title styling is controlled by AppShell's theme. `SidebarGroup` takes no `cl
381
381
  - You want to organize a large navigation menu
382
382
  - Users need to focus on specific sections
383
383
 
384
+ **Use [SidebarSection](./sidebar-section.md) when:**
385
+
386
+ - You only need a heading above items that should always stay visible
387
+ - The items should keep top-level styling instead of an indented submenu
388
+
384
389
  **Use SidebarItem directly when:**
385
390
 
386
391
  - You have standalone top-level navigation
@@ -453,6 +458,7 @@ Without `to`, only the chevron is clickable (for expand/collapse).
453
458
  ## Related Components
454
459
 
455
460
  - [SidebarItem](./sidebar-item.md) - Individual navigation items
461
+ - [SidebarSection](./sidebar-section.md) - Static headings for flat items
456
462
  - [DefaultSidebar](./sidebar-layout.md) - Container for groups
457
463
  - [WithGuard](./with-guard.md) - Conditional rendering
458
464
 
@@ -0,0 +1,114 @@
1
+ ---
2
+ title: SidebarSection
3
+ description: Static, non-collapsible heading for grouping flat sidebar navigation items
4
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/components/sidebar-section.docs.outline.md — do not edit by hand. -->
7
+
8
+ # SidebarSection
9
+
10
+ `SidebarSection` places a static heading above a run of flat navigation items. Unlike [`SidebarGroup`](./sidebar-group.md), a section is neither collapsible nor navigable, and its items keep the same styling and indentation as top-level items.
11
+
12
+ ## Import
13
+
14
+ ```tsx
15
+ import { SidebarSection } from "@tailor-platform/app-shell";
16
+ ```
17
+
18
+ ## Basic Usage
19
+
20
+ ```tsx
21
+ import {
22
+ DefaultSidebar,
23
+ SidebarItem,
24
+ SidebarSection,
25
+ SidebarSeparator,
26
+ } from "@tailor-platform/app-shell";
27
+
28
+ <DefaultSidebar>
29
+ <SidebarItem to="/overview" />
30
+
31
+ <SidebarSeparator />
32
+
33
+ <SidebarSection title="Services">
34
+ <SidebarItem to="/ai-gateways" />
35
+ <SidebarItem to="/applications" />
36
+ <SidebarItem to="/auth-connections" />
37
+ </SidebarSection>
38
+
39
+ <SidebarSeparator />
40
+
41
+ <SidebarSection title="Logs">
42
+ <SidebarItem to="/activity-logs" />
43
+ <SidebarItem to="/dataplane-logs" />
44
+ </SidebarSection>
45
+ </DefaultSidebar>;
46
+ ```
47
+
48
+ ## Props
49
+
50
+ | Prop | Type | Default | Description |
51
+ | ---------- | --------------------------- | ------------ | --------------------------------- |
52
+ | `title` | `string \| LocalizedString` | **Required** | Section heading (supports i18n) |
53
+ | `children` | `React.ReactNode` | **Required** | Child items (usually SidebarItem) |
54
+
55
+ ## Behavior
56
+
57
+ - **Not interactive.** The heading is plain text: it is not a link and has no expand/collapse toggle.
58
+ - **Flat items.** Children render at the same level as top-level `SidebarItem`s, not as an indented submenu.
59
+ - **Icon rail.** When the sidebar collapses to an icon rail, the heading is hidden and the items stay in the rail as ordinary icons. Unlike `SidebarGroup`, there is no hover flyout.
60
+ - **No automatic dividers.** Add a `SidebarSeparator` between sections where you want a line.
61
+
62
+ ## Internationalization
63
+
64
+ Titles support i18n with `defineI18nLabels`:
65
+
66
+ ```tsx
67
+ import { defineI18nLabels } from "@tailor-platform/app-shell";
68
+
69
+ const labels = defineI18nLabels({
70
+ en: { services: "Services" },
71
+ ja: { services: "サービス" },
72
+ });
73
+
74
+ <SidebarSection title={labels.t("services")}>
75
+ <SidebarItem to="/ai-gateways" />
76
+ </SidebarSection>;
77
+ ```
78
+
79
+ ## When to Use
80
+
81
+ **Use SidebarSection when:**
82
+
83
+ - You want to label a run of items with a heading, but keep them always visible
84
+ - The items are peers of your top-level items, not children of a parent page
85
+ - You want the items to stay as individual icons in the icon rail
86
+
87
+ **Use SidebarGroup when:**
88
+
89
+ - Users should be able to collapse the items
90
+ - The items belong under a parent with its own icon (or landing page via `to`)
91
+ - You need nesting
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 |
101
+
102
+ ## Accessibility
103
+
104
+ The children are rendered as their own list, labelled by the heading, so screen readers announce the section name (for example, "Services, list, 3 items") when entering it.
105
+
106
+ ## Related Components
107
+
108
+ - [SidebarGroup](./sidebar-group.md) - Collapsible, nested navigation groups
109
+ - [SidebarItem](./sidebar-item.md) - Individual navigation items
110
+ - [DefaultSidebar](./default-sidebar.md) - Container for sidebar navigation
111
+
112
+ ## Related Concepts
113
+
114
+ - [Sidebar Navigation](../concepts/sidebar-navigation.md) - Sidebar customization guide
@@ -0,0 +1,198 @@
1
+ ---
2
+ title: Text
3
+ description: Text with a typography role, rendered as an element you choose
4
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/components/text.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Text
9
+
10
+ The `Text` component applies a [typography role](../concepts/styling-theming.md#typography) to its content. A role sets font size, line height, font weight, and letter spacing together. The role does not set the color, and it does not choose the HTML element. Use the `render` prop to choose the element (Base UI render pattern).
11
+
12
+ ## Import
13
+
14
+ ```tsx
15
+ import { Text } from "@tailor-platform/app-shell";
16
+ ```
17
+
18
+ ## Basic Usage
19
+
20
+ <example-preview name="basic-usage"></example-preview>
21
+
22
+ ```tsx
23
+ function BasicUsage() {
24
+ return (
25
+ <div className="flex flex-col gap-1">
26
+ <Text variant="heading-md">Purchase orders</Text>
27
+ <Text className="text-muted-foreground">Orders sent to suppliers this month.</Text>
28
+ </div>
29
+ );
30
+ }
31
+ ```
32
+
33
+ ## Props
34
+
35
+ _Generated from the type surface — first-party props only._
36
+
37
+ ### TextProps
38
+
39
+ | Prop | Type | Default | Description |
40
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ |
41
+ | `ref?` | `React.Ref<HTMLElement>` | - | Ref to the rendered element. |
42
+ | `render?` | `React.ReactElement` | - | Custom element to render as (Base UI render prop). Use it to 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. |
44
+
45
+ ## Exports
46
+
47
+ - `Text` — function
48
+ - `TextProps` — type
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:
51
+
52
+ ```tsx
53
+ <Text variant="label-md" render={<label htmlFor="quantity" />}>
54
+ Quantity
55
+ </Text>
56
+ ```
57
+
58
+ Some lint rules report this example; see [Lint](#lint).
59
+
60
+ ## Roles
61
+
62
+ `variant` takes one of the ten roles. Each role renders a default element when you do not pass `render`:
63
+
64
+ | `variant` | Default element | Use |
65
+ | ----------------- | --------------- | ----------------------------------------------- |
66
+ | `heading-lg` | `h2` | Page title (`Layout.Header`) |
67
+ | `heading-md` | `h2` | Section heading, card title |
68
+ | `heading-sm` | `h2` | Sub-section heading |
69
+ | `body-md` | `p` | Body copy, descriptions (default role) |
70
+ | `body-sm` | `p` | Caption, timestamp, metadata |
71
+ | `body-md-relaxed` | `p` | Long text, such as a chat message or a document |
72
+ | `body-sm-relaxed` | `p` | Long text at the small size |
73
+ | `label-md` | `span` | Field label, short UI text, figures in a table |
74
+ | `label-sm` | `span` | Small label, badge text |
75
+ | `code-sm` | `span` | ID and code. `Text` adds `font-mono` for you |
76
+
77
+ <example-preview name="roles"></example-preview>
78
+
79
+ ```tsx
80
+ function Roles() {
81
+ return (
82
+ <div className="flex flex-col gap-3">
83
+ {/* 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>
93
+ <Text variant="body-md">body-md: Orders sent to suppliers this month.</Text>
94
+ <Text variant="body-sm">body-sm: Updated 2 hours ago</Text>
95
+ <Text variant="label-md">label-md: Total amount</Text>
96
+ <Text variant="label-sm">label-sm: Draft</Text>
97
+ <Text variant="code-sm">code-sm: PO-2026-0001</Text>
98
+ </div>
99
+ );
100
+ }
101
+ ```
102
+
103
+ The sizes and line heights are in the [Typography section of the styling guide](../concepts/styling-theming.md#typography).
104
+
105
+ ## Heading Level
106
+
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`:
108
+
109
+ ```tsx
110
+ <Text variant="heading-md" render={<h3 />}>
111
+ Line items
112
+ </Text>
113
+ ```
114
+
115
+ To use a heading role for text that is not a heading, render a `div` or a `span`:
116
+
117
+ ```tsx
118
+ <Text variant="heading-md" render={<div />}>
119
+ 1,240 orders
120
+ </Text>
121
+ ```
122
+
123
+ ## Lint
124
+
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:
126
+
127
+ ```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
131
+ </Text>
132
+ ```
133
+
134
+ ## Color and Modifiers
135
+
136
+ Set the color with a color token in `className`. Four modifiers may also be combined with a role: `leading-none` for single-line text, `tabular-nums`, `font-semibold` for emphasis, and `uppercase`.
137
+
138
+ <example-preview name="color-and-modifiers"></example-preview>
139
+
140
+ ```tsx
141
+ function ColorAndModifiers() {
142
+ return (
143
+ <div className="flex flex-col gap-3">
144
+ <Text variant="body-md" className="text-muted-foreground">
145
+ Orders sent this month.
146
+ </Text>
147
+ <Text variant="label-md" className="tabular-nums">
148
+ 1,240.00
149
+ </Text>
150
+ </div>
151
+ );
152
+ }
153
+ ```
154
+
155
+ ```tsx
156
+ <Text variant="body-md" className="text-muted-foreground">
157
+ Orders sent this month.
158
+ </Text>
159
+ <Text variant="heading-md" render={<h3 />} className="leading-none">
160
+ Card title
161
+ </Text>
162
+ <Text variant="label-md" className="tabular-nums">
163
+ 1,240.00
164
+ </Text>
165
+ ```
166
+
167
+ Do not combine a role with other `leading-*`, `tracking-*`, or `text-*` size values. Pick a different `variant` instead. A `text-*` size class such as `text-sm` has no effect on `Text`: AppShell's stylesheet loads after yours, so the role's size wins.
168
+
169
+ ## Using Roles Without Text
170
+
171
+ Each role is also a plain utility, for example `text-heading-md`. Use the utility when you style your own element and do not need `Text`:
172
+
173
+ ```tsx
174
+ <th className="text-label-md text-muted-foreground">Amount</th>
175
+ ```
176
+
177
+ ## TypeScript
178
+
179
+ ```typescript
180
+ import { type TextProps } from "@tailor-platform/app-shell";
181
+ ```
182
+
183
+ ## Accessibility
184
+
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.
186
+ - 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.
188
+
189
+ ## Palettes
190
+
191
+ The `cream` and `bloom` palettes set the line height and letter spacing of `h1` to `h6` elements. Under these palettes, a heading role on a heading element uses the palette values (line height 1.2, letter spacing `-0.03em`), including when you add `leading-none`. The same role on a `div` or `span` uses the role values. See the [Typography section of the styling guide](../concepts/styling-theming.md#typography).
192
+
193
+ ## Related Components
194
+
195
+ - [Card](./card.md) - The table above lists `heading-md` for card titles
196
+ - [Layout](./layout.md) - The table above lists `heading-lg` for the page title
197
+
198
+ AppShell's own components, including these two, still use the stock utilities. They will move to the roles in a later change.
@@ -44,7 +44,7 @@ export function StepIndicator({ steps, current }: Props) {
44
44
  flex items-center gap-2 rounded-md px-3 py-2
45
45
  bg-muted text-sm text-muted-foreground
46
46
  data-[active]:bg-primary data-[active]:text-primary-foreground
47
- data-[completed]:text-status-completed
47
+ data-[completed]:text-success-text
48
48
  "
49
49
  >
50
50
  {i < current && <Check className="size-4" />}
@@ -143,12 +143,18 @@ type AppShellPageProps = {
143
143
  meta?: {
144
144
  title?: LocalizedString;
145
145
  icon?: ReactNode;
146
- breadcrumbTitle?: string | ((segment: string) => string);
146
+ breadcrumbTitle?: string | ((segment: string) => LocalizedString);
147
147
  };
148
148
  guards?: Guard[];
149
149
  };
150
150
  ```
151
151
 
152
+ For a localized breadcrumb, use `meta: { breadcrumbTitle: labels.t("orders") }`, just like `meta.title`.
153
+ AppShell recognizes translation functions created by `labels.t()` and resolves them with the current locale.
154
+ For a dynamic label, use `breadcrumbTitle: (segment) => labels.t("order", { id: segment })`.
155
+ Other callbacks receive the URL segment; AppShell resolves any returned `LocalizedString` with the current locale.
156
+ The existing `() => labels.t("orders")` form remains supported. Wrap handwritten locale functions in an outer callback rather than passing them directly.
157
+
152
158
  ## Path Conventions
153
159
 
154
160
  | Directory Name | Converts To | Description |