@tailor-platform/app-shell 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +330 -89
- package/dist/app-shell.js +8451 -7633
- package/dist/{appshell-Y1AYCuVJ.js → appshell-DQTz-QwK.js} +484 -374
- package/dist/style-entry.d.ts +1 -656
- package/dist/testing.d.ts +61 -56
- package/dist/testing.js +1 -1
- package/dist/vite-plugin.d.ts +61 -56
- package/package.json +8 -5
- package/skills/app-shell-patterns/SKILL.md +37 -27
- package/skills/app-shell-patterns/references/api/aggregations.md +131 -0
- package/skills/app-shell-patterns/references/components/action-panel.md +55 -23
- package/skills/app-shell-patterns/references/components/badge.md +7 -3
- package/skills/app-shell-patterns/references/components/data-table.md +430 -49
- package/skills/app-shell-patterns/references/components/form.md +19 -9
- package/skills/app-shell-patterns/references/components/item-list.md +191 -0
- package/skills/app-shell-patterns/references/components/select.md +10 -0
- package/skills/app-shell-patterns/references/components/text.md +75 -38
- package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +16 -14
- package/skills/app-shell-patterns/references/concepts/styling-theming.md +19 -11
- package/skills/app-shell-patterns/references/migrations.md +2 -2
- package/skills/app-shell-patterns/references/pages/document-detail.md +36 -25
- package/skills/app-shell-patterns/references/patterns/form-modal.md +4 -1
- package/dist/semantic-contrast.test.ts +0 -227
- package/dist/theme-bridge.test.ts +0 -182
|
@@ -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>`.
|
|
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`
|
|
137
|
+
`Field.Control` is a text input. It is the control for plain text-like fields, and you can pass it a native element such as `render={<textarea />}`. The controls in the last row of the table below go **directly under `Field.Root`**, as a sibling of `Field.Label`. Each one registers itself with the `Field` context, so it gets label association, `aria-describedby`, `disabled`, and validation state without `Field.Control`.
|
|
138
|
+
|
|
139
|
+
| Control | Where it goes |
|
|
140
|
+
| ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
141
|
+
| Plain text, email, number, password, … input | `<Field.Control type="…" />` |
|
|
142
|
+
| Native element (`<textarea>`, `<input type="…">`) | `<Field.Control render={<textarea />} />` |
|
|
143
|
+
| `Select`, `Combobox`, `Autocomplete`, `Checkbox`, `Textarea`, `DateField`, `DatePicker`, `DateRangePicker` (and their `.Async`) | Directly under `Field.Root` — not `Field.Control` |
|
|
144
|
+
|
|
145
|
+
`Input` is the exception. It is a plain `<input>` that does not connect to `Field.Root`, so inside a field use `<Field.Control />` in its place.
|
|
146
|
+
|
|
147
|
+
Do not pass the self-wired controls to `Field.Control`'s `render` prop. They draw their own frame, so `<Field.Control render={<Select />} />` renders a second border around the select. `Field.Control` also passes them `<input>` props they don't take. `Field.Control` logs a console warning when it is given one of these controls directly as an element. A wrapper component or the function form of `render` is not detected.
|
|
138
148
|
|
|
139
149
|
```tsx
|
|
140
150
|
const [country, setCountry] = React.useState<string | null>(null);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ItemList
|
|
3
|
+
description: Compact icon-and-label lists for consistent navigation inside and outside the Sidebar
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/components/item-list.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# ItemList
|
|
9
|
+
|
|
10
|
+
`ItemList` presents compact icon-and-label rows for navigation and actions. Use it for in-content navigation, tables of contents, or other lists that should share the Sidebar's list presentation without depending on its layout or providers.
|
|
11
|
+
|
|
12
|
+
It does not require `AppShell`, a router, or a Sidebar provider. Resource metadata, translations, permissions, and active-route matching remain the caller's responsibility.
|
|
13
|
+
|
|
14
|
+
## Import
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { ItemList } from "@tailor-platform/app-shell";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Basic Usage
|
|
21
|
+
|
|
22
|
+
<example-preview name="basic-usage"></example-preview>
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
function BasicUsage() {
|
|
26
|
+
const [view, setView] = useState("Overview");
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<ItemList.Root aria-label="Workspace">
|
|
30
|
+
<ItemList.Item
|
|
31
|
+
icon={<Home />}
|
|
32
|
+
isActive={view === "Overview"}
|
|
33
|
+
onClick={() => setView("Overview")}
|
|
34
|
+
>
|
|
35
|
+
Overview
|
|
36
|
+
</ItemList.Item>
|
|
37
|
+
<ItemList.Separator />
|
|
38
|
+
<ItemList.Section title="Business">
|
|
39
|
+
<ItemList.Item icon={<ShoppingCart />} onClick={() => setView("Orders")}>
|
|
40
|
+
Orders
|
|
41
|
+
</ItemList.Item>
|
|
42
|
+
<ItemList.Group title="Products" icon={<Package />}>
|
|
43
|
+
<ItemList.Item onClick={() => setView("All products")}>All products</ItemList.Item>
|
|
44
|
+
<ItemList.Item onClick={() => setView("Categories")}>Categories</ItemList.Item>
|
|
45
|
+
</ItemList.Group>
|
|
46
|
+
</ItemList.Section>
|
|
47
|
+
</ItemList.Root>
|
|
48
|
+
<output>Current view: {view}</output>
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Sub-components
|
|
55
|
+
|
|
56
|
+
| Sub-component | Description |
|
|
57
|
+
| -------------------- | -------------------------------------------------------------------------------- |
|
|
58
|
+
| `ItemList.Root` | A semantic `ul`; accepts standard list props |
|
|
59
|
+
| `ItemList.Item` | A compact icon-and-label row containing a button or supplied interactive element |
|
|
60
|
+
| `ItemList.Group` | A collapsible, indented child list; open by default |
|
|
61
|
+
| `ItemList.Section` | A labeled, non-collapsible list whose items retain their normal indentation |
|
|
62
|
+
| `ItemList.Separator` | A decorative divider that remains a valid list child |
|
|
63
|
+
|
|
64
|
+
## Items, Links, and Actions
|
|
65
|
+
|
|
66
|
+
`ItemList.Item` is for links and actions, not static rows. It requires `onClick`, `render`, or both; omitting both is a type error, including for a disabled Item. With `onClick`, it renders a `button` with `type="button"` by default. Supply an anchor or application link through `render` for navigation.
|
|
67
|
+
|
|
68
|
+
`render` replaces the interactive element inside the row, not the containing `li`. `children` supplies its label, and `icon` supplies an optional decorative leading icon. Standard button props, classes, attributes, and refs apply to the inner operation. A disabled Item is skipped by Tab and cancels clicks, including navigation through a supplied link.
|
|
69
|
+
|
|
70
|
+
<example-preview name="links-and-actions"></example-preview>
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
function LinksAndActions() {
|
|
74
|
+
const [checks, setChecks] = useState(0);
|
|
75
|
+
const handleCheckNotifications = () => {
|
|
76
|
+
setChecks((count) => count + 1);
|
|
77
|
+
};
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
<ItemList.Root aria-label="Quick access">
|
|
81
|
+
<ItemList.Item icon={<Home />} render={<a href="https://example.com/help" />}>
|
|
82
|
+
Help center
|
|
83
|
+
</ItemList.Item>
|
|
84
|
+
<ItemList.Item icon={<Bell />} onClick={handleCheckNotifications}>
|
|
85
|
+
Check notifications
|
|
86
|
+
</ItemList.Item>
|
|
87
|
+
</ItemList.Root>
|
|
88
|
+
<output>Notification checks: {checks}</output>
|
|
89
|
+
</>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For application navigation, use `Link` or `NavLink` from `@tailor-platform/app-shell` inside an AppShell-managed page. Do not import routing APIs directly from `react-router` or create a separate router around the list. The routes in this example belong to the documentation app; use your application's route paths in your own pages.
|
|
95
|
+
|
|
96
|
+
<example-preview name="app-shell-routing"></example-preview>
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
function AppShellRouting() {
|
|
100
|
+
return (
|
|
101
|
+
<ItemList.Root aria-label="Documentation pages">
|
|
102
|
+
<ItemList.Item render={<Link to="/components/button" />}>Button documentation</ItemList.Item>
|
|
103
|
+
<ItemList.Item render={<NavLink to="/components/item-list" end />}>
|
|
104
|
+
ItemList documentation
|
|
105
|
+
</ItemList.Item>
|
|
106
|
+
</ItemList.Root>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`NavLink` owns route matching and supplies `aria-current="page"` when matched; ItemList styles that attribute with the same emphasis as `isActive`. Use `end` when only the exact destination should be current. With a plain link, the caller supplies `aria-current="page"` when appropriate.
|
|
112
|
+
|
|
113
|
+
`isActive` remains a boolean for explicit visual emphasis, including local actions. It does not imply `aria-current`, selection state, a `listbox` role, or `aria-selected`. ItemList does not perform route matching.
|
|
114
|
+
|
|
115
|
+
### Item Props
|
|
116
|
+
|
|
117
|
+
| Prop | Type | Default | Description |
|
|
118
|
+
| ----------- | -------------------------------------- | ------- | ------------------------------------------------------------------ |
|
|
119
|
+
| `icon` | `ReactNode` | — | Optional decorative leading icon |
|
|
120
|
+
| `children` | `ReactNode` | — | Compact row label |
|
|
121
|
+
| `render` | `ReactElement` | — | Replaces the inner button; required unless `onClick` is supplied |
|
|
122
|
+
| `onClick` | `MouseEventHandler<HTMLButtonElement>` | — | Action handler; required unless `render` is supplied |
|
|
123
|
+
| `isActive` | `boolean` | `false` | Explicit visual emphasis without route matching or selection state |
|
|
124
|
+
| `disabled` | `boolean` | — | Disables clicks and removes the operation from the tab order |
|
|
125
|
+
| `className` | `string` | — | Classes applied to the inner operation |
|
|
126
|
+
|
|
127
|
+
Other standard button props, such as `title` and ARIA attributes, are forwarded to the inner operation. Put link-specific props such as `href`, `target`, and `rel` on the element passed to `render`.
|
|
128
|
+
|
|
129
|
+
Rows without an `icon` do not reserve icon space. To align a mixed list, explicitly pass `icon={<span />}` on a row that needs an empty icon slot. Lists with no icons keep their compact indentation.
|
|
130
|
+
|
|
131
|
+
## Groups and Heading Hierarchies
|
|
132
|
+
|
|
133
|
+
`expandOnClick` defaults to `true`: clicking a group's heading toggles its child list, whether or not `render` is supplied. `render` only replaces the heading's main operation; it does not determine the expansion behavior. `defaultOpen` defaults to `true`; set it to `false` to start collapsed. Groups can be nested.
|
|
134
|
+
|
|
135
|
+
Set `expandOnClick={false}` to separate the heading's main operation from expansion. This requires `onClick` or `render` so the heading does not become an inert button. A separate button, named by the group title, expands and collapses the group. Use this for linked headings so navigation does not toggle the list.
|
|
136
|
+
|
|
137
|
+
`onClick` applies only to the heading's main operation. Clicking a child Item or the separate expand button does not invoke it. `isActive` and `aria-current="page"` control emphasis on the main heading operation, not the separate expand button.
|
|
138
|
+
|
|
139
|
+
For a table of contents, use a linked Group for a parent heading and Items for its child headings. Keep the list data aligned with the actual heading hierarchy and anchor IDs in the document.
|
|
140
|
+
|
|
141
|
+
<example-preview name="linked-group"></example-preview>
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
function LinkedGroup() {
|
|
145
|
+
return (
|
|
146
|
+
<ItemList.Root aria-label="Contents">
|
|
147
|
+
<ItemList.Group
|
|
148
|
+
title="Installing"
|
|
149
|
+
render={<a href="#installing" aria-label="Installing" />}
|
|
150
|
+
expandOnClick={false}
|
|
151
|
+
>
|
|
152
|
+
<ItemList.Item render={<a href="#configuration" aria-label="Configuration" />}>
|
|
153
|
+
Configuration
|
|
154
|
+
</ItemList.Item>
|
|
155
|
+
</ItemList.Group>
|
|
156
|
+
</ItemList.Root>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Group Props
|
|
162
|
+
|
|
163
|
+
| Prop | Type | Default | Description |
|
|
164
|
+
| --------------- | -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| `title` | `ReactNode` | — | Accessible group label, including text and optional decorations |
|
|
166
|
+
| `icon` | `ReactNode` | — | Optional decorative leading icon |
|
|
167
|
+
| `render` | `ReactElement` | — | Replaces the main heading operation without changing expansion behavior |
|
|
168
|
+
| `onClick` | `MouseEventHandler<HTMLButtonElement>` | — | Handler for the main heading operation only |
|
|
169
|
+
| `expandOnClick` | `boolean` | `true` | Toggle from the heading; `false` requires `onClick` or `render` and adds a separate expand button |
|
|
170
|
+
| `isActive` | `boolean` | `false` | Explicit visual emphasis for the heading operation |
|
|
171
|
+
| `defaultOpen` | `boolean` | `true` | Initial uncontrolled expanded state |
|
|
172
|
+
| `children` | `ReactNode` | — | Nested Items, Groups, Sections, or Separators |
|
|
173
|
+
|
|
174
|
+
Other standard `li` props, including `className`, apply to the containing group item, not its heading operation. `onClick` is the exception described above.
|
|
175
|
+
|
|
176
|
+
A Group's rendered label names its heading operation, separate expand button, and child list. Keep meaningful text in a rich `title`; ReactNode content is not converted into a string. Meaningful details such as counts can be read with the label, while purely decorative elements should use `aria-hidden="true"`. An icon-only title needs screen-reader text. Do not put links or buttons inside the title, because the heading itself is interactive.
|
|
177
|
+
|
|
178
|
+
## Sections and Separators
|
|
179
|
+
|
|
180
|
+
Use `Group` for hierarchy and `Section` for flat classification. A Section accepts a resolved string `title` and `children`, keeps child rows at their normal indentation, and does not add an interactive heading. Other standard `li` props apply to its outer item.
|
|
181
|
+
|
|
182
|
+
Neither Group nor Section requires wrapping its children in another `ItemList.Root`. Use `ItemList.Separator` between logical runs of items; it is decorative and renders a list item rather than an invalid direct `div` child of `ul`.
|
|
183
|
+
|
|
184
|
+
## Styling and Accessibility
|
|
185
|
+
|
|
186
|
+
- Keep labels concise; they truncate in narrow containers.
|
|
187
|
+
- Use `aria-label` or `aria-labelledby` on Root when a list needs an accessible name.
|
|
188
|
+
- Native links and buttons retain their keyboard interactions. Group buttons announce their expanded state and support Enter and Space.
|
|
189
|
+
- Resolve translations before passing labels; ItemList does not translate them. Group titles can contain ReactNode content, while Section titles remain strings.
|
|
190
|
+
- ItemList is an ordinary list, not a menu or selection widget. It does not provide arrow-key selection, search, reordering, or Sidebar icon-rail behavior.
|
|
191
|
+
- Existing Sidebar components retain route metadata resolution, rail tooltips and flyouts, and drawer dismissal while sharing the underlying list presentation.
|
|
@@ -171,6 +171,16 @@ const { Root, Trigger, Value, Content, Item, Group, GroupLabel, Separator } = Se
|
|
|
171
171
|
|
|
172
172
|
## Form submission
|
|
173
173
|
|
|
174
|
+
Inside a form, place `Select` directly under `Field.Root`, not inside `Field.Control`. `Select` draws
|
|
175
|
+
its own trigger and registers with the field itself.
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
<Field.Root name="country">
|
|
179
|
+
<Field.Label>Country</Field.Label>
|
|
180
|
+
<Select items={["Japan", "United States"]} placeholder="Select a country" />
|
|
181
|
+
</Field.Root>
|
|
182
|
+
```
|
|
183
|
+
|
|
174
184
|
Inside a `Field.Root`, the field's `name` identifies the value and these props are unnecessary —
|
|
175
185
|
see [Form](./form.md). They matter for **native** submission: a plain `<form>`, `new FormData(form)`,
|
|
176
186
|
or a server action.
|
|
@@ -7,7 +7,7 @@ description: Text with a typography role, rendered as an element you choose
|
|
|
7
7
|
|
|
8
8
|
# Text
|
|
9
9
|
|
|
10
|
-
The `Text` component applies a [typography role](../concepts/styling-theming.md#typography) to its content. A role sets font size, line height, font weight, and letter spacing together. The role does not set the color, and it does not choose the HTML element.
|
|
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
|
|
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
|
|
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` —
|
|
47
|
+
- `Text` — const
|
|
48
48
|
- `TextProps` — type
|
|
49
49
|
|
|
50
|
-
|
|
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.
|
|
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
|
|
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"
|
|
85
|
-
|
|
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
|
-
|
|
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"
|
|
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,
|
|
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"
|
|
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.
|
|
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/
|
|
129
|
-
<Text variant="
|
|
130
|
-
|
|
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"
|
|
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 `
|
|
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`,
|
|
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,
|
|
235
|
+
AppShell's own components, including these two, render their titles and descriptions with `Text`.
|
|
@@ -69,16 +69,18 @@ const App = () => (
|
|
|
69
69
|
### SidebarItem
|
|
70
70
|
|
|
71
71
|
A navigation item that automatically resolves title and icon from resource definitions.
|
|
72
|
+
Active internal sidebar links expose `aria-current="page"` on the link itself, including
|
|
73
|
+
items with custom `render` content. Inactive and external links do not expose it.
|
|
72
74
|
|
|
73
75
|
#### Props
|
|
74
76
|
|
|
75
|
-
| Prop | Type | Required | Description
|
|
76
|
-
| ---------- | ----------------- | -------- |
|
|
77
|
-
| `to` | `string` | Yes | Target URL. External URLs (http://...) are rendered as external links
|
|
78
|
-
| `title` | `LocalizedString` | No | Override title (i18n supported). When omitted, auto-resolved from resource meta
|
|
79
|
-
| `icon` | `ReactNode` | No | Override icon. When omitted, auto-resolved from resource meta
|
|
80
|
-
| `external` | `boolean` | No | Opens link in new tab with external link icon
|
|
81
|
-
| `render` | `function` | No | Custom render function for full UI control
|
|
77
|
+
| Prop | Type | Required | Description |
|
|
78
|
+
| ---------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| `to` | `string` | Yes | Target URL. External URLs (http://...) are rendered as external links |
|
|
80
|
+
| `title` | `LocalizedString` | No | Override title (i18n supported). When omitted, auto-resolved from resource meta |
|
|
81
|
+
| `icon` | `ReactNode` | No | Override icon. When omitted, auto-resolved from resource meta; with no icon at all, the icon rail shows the title's first letter |
|
|
82
|
+
| `external` | `boolean` | No | Opens link in new tab with external link icon |
|
|
83
|
+
| `render` | `function` | No | Custom render function for full UI control |
|
|
82
84
|
|
|
83
85
|
#### Examples
|
|
84
86
|
|
|
@@ -124,13 +126,13 @@ A collapsible group for organizing navigation items.
|
|
|
124
126
|
|
|
125
127
|
#### Props
|
|
126
128
|
|
|
127
|
-
| Prop | Type | Required | Description
|
|
128
|
-
| ------------- | ----------------- | -------- |
|
|
129
|
-
| `title` | `LocalizedString` | Yes | Group title (supports i18n)
|
|
130
|
-
| `icon` | `ReactNode` | No | Group icon
|
|
131
|
-
| `to` | `string` | No | When specified, title becomes a clickable link
|
|
132
|
-
| `defaultOpen` | `boolean` | No | Initial expanded state. Default: `true`
|
|
133
|
-
| `children` | `ReactNode` | Yes | Child items (SidebarItem, SidebarGroup, etc.)
|
|
129
|
+
| Prop | Type | Required | Description |
|
|
130
|
+
| ------------- | ----------------- | -------- | ---------------------------------------------------------------------- |
|
|
131
|
+
| `title` | `LocalizedString` | Yes | Group title (supports i18n) |
|
|
132
|
+
| `icon` | `ReactNode` | No | Group icon. When omitted, the icon rail shows the title's first letter |
|
|
133
|
+
| `to` | `string` | No | When specified, title becomes a clickable link |
|
|
134
|
+
| `defaultOpen` | `boolean` | No | Initial expanded state. Default: `true` |
|
|
135
|
+
| `children` | `ReactNode` | Yes | Child items (SidebarItem, SidebarGroup, etc.) |
|
|
134
136
|
|
|
135
137
|
#### Examples
|
|
136
138
|
|
|
@@ -217,7 +217,14 @@ Each intent has eight roles. The intents are `info`, `success`, `warning`, `dang
|
|
|
217
217
|
<span className="rounded-md bg-success-surface px-2 py-0.5 text-success-text">Paid</span>
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
The hue roles have the same values in every shipped palette. `cream` and `bloom` change brand and surface colors, not status colors. The `neutral` roles follow the palette, because they read the system tokens.
|
|
221
|
+
|
|
222
|
+
**Contrast.** AppShell's thresholds are 4.5:1 for text (WCAG 1.4.3) and 3:1 for non-text (WCAG 1.4.11). In every shipped palette (`default`, `cream`, `bloom`), in light and dark mode, on `--card` and `--background`, `text` on `surface` and `contrast` on `solid` and `solid-hover` are at least 4.5:1 for the four hue intents, and `danger` `solid` (the invalid-input border) is at least 3:1. A browser test checks this in Chromium against the built stylesheet. Known exceptions:
|
|
223
|
+
|
|
224
|
+
- `text` on `surface-hover` is below 4.5:1 for `danger` and `success` in light mode. The test holds it at 4.2:1 or more.
|
|
225
|
+
- In `cream` and `bloom`, the main content area is transparent over the shell gradient. At the top of the `bloom` light gradient, `text` on `surface` is 4.49:1 for `success` and 4.48:1 for `danger`. Put status content on a card when it needs to pass.
|
|
226
|
+
- `warning` `solid` and `indicator` are 2.15:1 against a light card. Use them with a label, not as the only sign of a state.
|
|
227
|
+
- The `neutral` intent reads the system tokens and is not checked.
|
|
221
228
|
|
|
222
229
|
Most `surface` and `border` values are translucent tints, so an opacity modifier compounds rather than replaces — `bg-info-surface/50` halves the tint's alpha instead of setting it to 50%. Some light-mode values are opaque instead of translucent: the `warning` surface, surface-hover and border, the `info` border and the `danger` surface-hover. They do not blend with a tinted parent. All dark-mode values are translucent.
|
|
223
230
|
|
|
@@ -227,7 +234,7 @@ Most `surface` and `border` values are translucent tints, so an opacity modifier
|
|
|
227
234
|
|
|
228
235
|
`--sidebar`, `--sidebar-foreground`, `--sidebar-border`, `--sidebar-primary(-foreground)`, `--sidebar-accent(-foreground)`, `--sidebar-ring` → `bg-sidebar`, `text-sidebar-foreground`, and so on. These let a palette tint the shell independently of page content.
|
|
229
236
|
|
|
230
|
-
`--chart-1` … `--chart-5` → `bg-chart-1`, `text-chart-1`, `fill-chart-1`. Use them in order for categorical series.
|
|
237
|
+
`--chart-1` … `--chart-5` → `bg-chart-1`, `text-chart-1`, `fill-chart-1`. Use them in order for categorical series. They tell series apart and carry no status, so they are not semantic roles and have literal values.
|
|
231
238
|
|
|
232
239
|
#### Alerts
|
|
233
240
|
|
|
@@ -283,7 +290,7 @@ There are 8 core roles and 2 relaxed body variants:
|
|
|
283
290
|
| `heading-sm` | `text-heading-sm` | 14px / 20px | 600 | Sub-section heading |
|
|
284
291
|
| `body-md` | `text-body-md` | 14px / 20px | 400 | Body copy, descriptions |
|
|
285
292
|
| `body-sm` | `text-body-sm` | 12px / 16px | 400 | Caption, timestamp, metadata |
|
|
286
|
-
| `label-md` | `text-label-md` | 14px / 20px | 500 | Field label, short UI text
|
|
293
|
+
| `label-md` | `text-label-md` | 14px / 20px | 500 | Field label, short UI text |
|
|
287
294
|
| `label-sm` | `text-label-sm` | 12px / 16px | 500 | Small label, badge text |
|
|
288
295
|
| `code-sm` | `text-code-sm` | 12px / 18px | 400 | ID and code. Use with `font-mono` |
|
|
289
296
|
| `body-md-relaxed` | `text-body-md-relaxed` | 14px / 24px | 400 | Long text, such as a chat message or a document |
|
|
@@ -301,7 +308,7 @@ Pair a role with a color token from the Color section above. These pairings are
|
|
|
301
308
|
| Body copy | `text-body-md` |
|
|
302
309
|
| Secondary copy, descriptions | `text-body-md text-muted-foreground` |
|
|
303
310
|
| Caption, timestamp, metadata | `text-body-sm text-muted-foreground` |
|
|
304
|
-
| Numeric value in a table or field | `text-
|
|
311
|
+
| Numeric value in a table or field | `text-body-md tabular-nums` |
|
|
305
312
|
| ID, code | `font-mono text-code-sm text-muted-foreground` |
|
|
306
313
|
|
|
307
314
|
<example-preview name="typography-roles"></example-preview>
|
|
@@ -404,20 +411,21 @@ function TypographyRoles() {
|
|
|
404
411
|
}
|
|
405
412
|
```
|
|
406
413
|
|
|
414
|
+
For headings, body copy, labels, and captions, use the [`Text`](../components/text.md) component. Do not style a bare `p`, `h1` to `h6`, or `span` with `text-sm`, `text-xs`, or `font-*`. A form field label is [`Field.Label`](../components/form.md), which already has the `label-md` role. `Text` applies the same roles with a `variant` prop. It renders a default element for each role (`h2` for headings, `p` for body text, `span` for the others). To set the element, such as the heading level, use an element component (`Text.h1` to `Text.h6`, `Text.p`, `Text.span`, `Text.div`):
|
|
415
|
+
|
|
407
416
|
```tsx
|
|
408
|
-
<
|
|
409
|
-
<
|
|
410
|
-
<
|
|
417
|
+
<Text.h3 variant="heading-md">Line items</Text.h3>
|
|
418
|
+
<Text variant="body-md" className="text-muted-foreground">Description copy</Text>
|
|
419
|
+
<Text variant="body-sm" className="text-muted-foreground">Updated 2h ago</Text>
|
|
411
420
|
```
|
|
412
421
|
|
|
413
|
-
|
|
422
|
+
Use the role utility when you style an element that you render yourself, such as a table header cell or a link:
|
|
414
423
|
|
|
415
424
|
```tsx
|
|
416
|
-
<
|
|
417
|
-
<Text variant="body-md" className="text-muted-foreground">Description copy</Text>
|
|
425
|
+
<th className="text-label-md text-muted-foreground">Amount</th>
|
|
418
426
|
```
|
|
419
427
|
|
|
420
|
-
On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's line height and letter spacing, including `leading-none`; see "Change a role" below. AppShell's own components
|
|
428
|
+
On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's line height and letter spacing, including `leading-none`; see "Change a role" below. AppShell's own components use the roles: content text renders with `Text`, and controls use the role utility.
|
|
421
429
|
|
|
422
430
|
**Rules**
|
|
423
431
|
|
|
@@ -33,7 +33,7 @@ Each entry states which versions are affected, what breaks, how to detect it, an
|
|
|
33
33
|
- **`text-destructive` resolves to the solid.** In dark mode it changes from `#f87171` to `#dc2626`, which is 3.71:1 on the dark card, below 4.5:1 for text. Components shipped with AppShell now use `text-danger-text` for error text, and invalid borders and focus rings read `--danger-solid`. In dark mode the invalid border changes from `#f87171` to `#dc2626`.
|
|
34
34
|
- **Themes that override `--destructive` no longer reach the components.** `Button`, invalid `Input`, `Textarea`, `Checkbox`, `Field` and `DateField` borders and rings, error text and the `CsvImporter` error cells read `--danger-*`. An override of `--destructive` still changes `bg-destructive` and `text-destructive` on your own elements, so the two reds can drift apart. The `cream` and `bloom` palettes no longer set `--destructive`; their values were the same as the default.
|
|
35
35
|
|
|
36
|
-
How to detect it: search the app for `destructive` in class names and in theme CSS, including Theme Generator output that sets `--destructive`. Compare `Button variant="destructive"`, an invalid `Input` and an error message in light and dark mode after upgrading.
|
|
36
|
+
How to detect it: search the app for `destructive` in class names and in theme CSS, including Theme Generator output that sets `--destructive`. In JS and TS files, the `no-deprecated-tokens` rule in `@tailor-platform/eslint-plugin-app-shell` reports these names and the role to use instead. The rule is available from version 0.2.0 of the plugin, and it does not read CSS files. Compare `Button variant="destructive"`, an invalid `Input` and an error message in light and dark mode after upgrading.
|
|
37
37
|
|
|
38
38
|
What to change:
|
|
39
39
|
|
|
@@ -53,7 +53,7 @@ The roles are documented in [Styling & theming](https://github.com/tailor-platfo
|
|
|
53
53
|
- **Component recolouring moves to the roles.** `Badge`, `Alert`, `CsvImporter` and `MetricCard` read the roles directly. A theme that overrides `--status-*` or `--alert-*` no longer changes these components; override `--{intent}-*` instead. `Badge` `error` / `subtle-error` and `Alert` `error` read `--danger-*`, so overriding `--destructive` no longer reaches them either. `Button variant="destructive"` and `text-destructive` still follow `--destructive`.
|
|
54
54
|
- **`text-status-*` loses contrast in dark mode.** The `--status-*` values are now the step-9 fills, which are below 4.5:1 as text on the dark card.
|
|
55
55
|
|
|
56
|
-
How to detect it: search the app for `status-`, `alert-` and theme overrides of `--status-`, `--alert-` and `--destructive`. Compare `Badge` and `Alert` in light and dark mode after upgrading.
|
|
56
|
+
How to detect it: search the app for `status-`, `alert-` and theme overrides of `--status-`, `--alert-` and `--destructive`. In JS and TS files, the `no-deprecated-tokens` rule in `@tailor-platform/eslint-plugin-app-shell` reports these names and the role to use instead. The rule is available from version 0.2.0 of the plugin, and it does not read CSS files. Compare `Badge` and `Alert` in light and dark mode after upgrading.
|
|
57
57
|
|
|
58
58
|
What to change:
|
|
59
59
|
|