@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.
- package/dist/app-shell-core.css +2 -2
- package/dist/app-shell.d.ts +1108 -190
- package/dist/app-shell.js +13090 -10783
- package/dist/{appshell-Y1AYCuVJ.js → appshell-B4QkQvXF.js} +519 -392
- package/dist/fonts/inter.css +7 -7
- package/dist/fonts/noto-sans-jp.css +116 -116
- package/dist/style-entry.d.ts +1 -656
- package/dist/testing.d.ts +132 -97
- package/dist/testing.js +1 -1
- package/dist/vite-plugin.d.ts +132 -97
- package/package.json +15 -10
- package/skills/app-shell-patterns/SKILL.md +38 -27
- package/skills/app-shell-patterns/references/api/aggregations.md +131 -0
- package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +32 -2
- package/skills/app-shell-patterns/references/api/define-module.md +5 -5
- package/skills/app-shell-patterns/references/api/router/use-params.md +2 -2
- package/skills/app-shell-patterns/references/api/use-ai-chat.md +46 -8
- package/skills/app-shell-patterns/references/api/use-app-shell-config.md +4 -4
- package/skills/app-shell-patterns/references/api/use-app-shell-data.md +5 -5
- package/skills/app-shell-patterns/references/api/use-app-shell.md +2 -2
- package/skills/app-shell-patterns/references/api/use-auth.md +3 -3
- package/skills/app-shell-patterns/references/api/use-route-error.md +12 -12
- package/skills/app-shell-patterns/references/components/action-panel.md +57 -23
- package/skills/app-shell-patterns/references/components/activity-card.md +3 -3
- package/skills/app-shell-patterns/references/components/ai-chat.md +37 -3
- package/skills/app-shell-patterns/references/components/app-shell.md +6 -5
- package/skills/app-shell-patterns/references/components/badge.md +7 -3
- package/skills/app-shell-patterns/references/components/card.md +2 -2
- package/skills/app-shell-patterns/references/components/data-table.md +514 -77
- package/skills/app-shell-patterns/references/components/date-picker.md +2 -2
- package/skills/app-shell-patterns/references/components/default-sidebar.md +2 -2
- package/skills/app-shell-patterns/references/components/dialog.md +8 -0
- package/skills/app-shell-patterns/references/components/filter-rail.md +10 -3
- package/skills/app-shell-patterns/references/components/form.md +19 -9
- package/skills/app-shell-patterns/references/components/global-header-layout.md +31 -8
- package/skills/app-shell-patterns/references/components/item-list.md +191 -0
- package/skills/app-shell-patterns/references/components/layout.md +24 -17
- package/skills/app-shell-patterns/references/components/select.md +18 -0
- package/skills/app-shell-patterns/references/components/sidebar-group.md +9 -7
- package/skills/app-shell-patterns/references/components/sidebar-layout.md +74 -11
- package/skills/app-shell-patterns/references/components/sidebar-section.md +8 -8
- package/skills/app-shell-patterns/references/components/tabs.md +32 -2
- package/skills/app-shell-patterns/references/components/text.md +75 -38
- package/skills/app-shell-patterns/references/components/tree.md +342 -0
- package/skills/app-shell-patterns/references/components/with-guard.md +5 -5
- package/skills/app-shell-patterns/references/concepts/authentication.md +1 -1
- package/skills/app-shell-patterns/references/concepts/routing-navigation.md +3 -3
- package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +16 -14
- package/skills/app-shell-patterns/references/concepts/styling-theming.md +28 -20
- package/skills/app-shell-patterns/references/migrations.md +22 -2
- package/skills/app-shell-patterns/references/pages/document-detail.md +32 -25
- package/skills/app-shell-patterns/references/patterns/form-modal.md +4 -1
- package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +1 -1
- package/dist/semantic-contrast.test.ts +0 -227
- package/dist/theme-bridge.test.ts +0 -182
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tree
|
|
3
|
+
description: Browse, pick one, or tick many items in a hierarchy — inline with Tree, or in a search dialog with TreePicker
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/components/tree.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# Tree
|
|
9
|
+
|
|
10
|
+
`Tree` shows your own hierarchical items (accounts with sub-accounts, warehouses with bins, categories with products) as an ARIA tree. It can be a plain browser, a single-select for master–detail views, or a multi-select checkbox form control.
|
|
11
|
+
|
|
12
|
+
The tree is the primitive, so it has no search box of its own. For the common "search, pick, then add" flow, **`TreePicker`** is a checkbox tree in a dialog with search, Cancel, and an "Add N items" button, the same split as `DateField` and `DatePicker`.
|
|
13
|
+
|
|
14
|
+
Trees are single-column. If you need a hierarchy with real columns (sorting, pinning, wide data), that's a tree grid, which belongs with the data table rather than here.
|
|
15
|
+
|
|
16
|
+
## Import
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { Tree, TreePicker } from "@tailor-platform/app-shell";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Basic Usage
|
|
23
|
+
|
|
24
|
+
Pass your own items and say how to read them with `mapItem`, as with `Select` and `Combobox`. `children` points at your own child array, and an item with no children is a leaf.
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
type Account = { code: string; name: string; children?: Account[] };
|
|
28
|
+
|
|
29
|
+
<Tree
|
|
30
|
+
aria-label="Chart of accounts"
|
|
31
|
+
className="h-96"
|
|
32
|
+
items={accounts}
|
|
33
|
+
mapItem={(account) => ({
|
|
34
|
+
key: account.code,
|
|
35
|
+
label: `${account.code} ${account.name}`,
|
|
36
|
+
children: account.children,
|
|
37
|
+
})}
|
|
38
|
+
onAction={(account) => navigate(`/accounts/${account.code}`)}
|
|
39
|
+
/>;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Item types
|
|
43
|
+
|
|
44
|
+
`Tree` takes its item type from `items`, so you don't pass one. The case that needs a type is a tree whose levels are different types, like categories holding products: type `items` as the union of every level, so `mapItem` can hand back either level's children.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
type CatalogNode = Category | Product;
|
|
48
|
+
|
|
49
|
+
const categories: CatalogNode[] = loadCategories();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Typed as `Category[]`, the item type is just `Category`, and `children: category.products` is a type error. Annotating `mapItem`'s parameter as the union works too.
|
|
53
|
+
|
|
54
|
+
### Keys
|
|
55
|
+
|
|
56
|
+
A `key` is the item's identity across the whole tree. The same key in two places is one item, so a product listed in two collections ticks in both places at once.
|
|
57
|
+
|
|
58
|
+
When codes repeat with different meanings, like bin `A1` in two warehouses, compose a unique key. `mapItem` receives the item's `parents` for exactly this.
|
|
59
|
+
|
|
60
|
+
A key can't be a parent in one place and a leaf in another. One key is one item, and an item can't both have children and not, so those rows can't tick consistently. The tree logs a console warning naming any such keys.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
const isWarehouse = (node: Warehouse | Bin): node is Warehouse => "bins" in node;
|
|
64
|
+
|
|
65
|
+
mapItem={(item, { parents }) =>
|
|
66
|
+
isWarehouse(item)
|
|
67
|
+
? { key: item.id, label: item.name, children: item.bins }
|
|
68
|
+
: { key: `${parents.find(isWarehouse)?.id}/${item.code}`, label: item.code }
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`parents` has the same union type as `items`, so the example narrows it with a type guard rather than a cast (`parents[0] as Warehouse`). A cast tells TypeScript to trust you, and throws at runtime if there's no warehouse above. The guard has TypeScript check it, so that case stays visible (`?.id`) and you decide how to handle it.
|
|
73
|
+
|
|
74
|
+
## Selection modes
|
|
75
|
+
|
|
76
|
+
`selectionMode` sets what clicking and Space do to a row.
|
|
77
|
+
|
|
78
|
+
| Mode | What a row does | State |
|
|
79
|
+
| ------------------ | ------------------------------------------------------------------------- | ------------------------ |
|
|
80
|
+
| `"none"` (default) | Clicking a leaf runs `onAction`, and clicking a parent opens or closes it | – |
|
|
81
|
+
| `"single"` | One row is selected at a time, parents included | `selectedKey` |
|
|
82
|
+
| `"checkbox"` | Rows have checkboxes, and the tree is a multi-select form control | `value` (a list of keys) |
|
|
83
|
+
|
|
84
|
+
Enter runs `onAction(item, key)` on any enabled row, in every mode. The props are a union on `selectionMode`, so `value` or `name` without `selectionMode="checkbox"` is a type error rather than silently ignored.
|
|
85
|
+
|
|
86
|
+
### Single
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
90
|
+
|
|
91
|
+
<Tree
|
|
92
|
+
{...props}
|
|
93
|
+
selectionMode="single"
|
|
94
|
+
selectedKey={selected}
|
|
95
|
+
onSelectedKeyChange={(key) => setSelected(key)}
|
|
96
|
+
/>;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Selecting a closed parent opens it. Its caret closes it again, so selecting an open parent never hides what you were looking at.
|
|
100
|
+
|
|
101
|
+
The selected row is always revealed: when it starts out inside closed groups, or `selectedKey` changes from outside to a hidden row, its ancestors open. A group the user closes afterwards stays closed, and the tab stop then sits on its nearest visible ancestor. Pass `revealSelected={false}` to leave groups as they are.
|
|
102
|
+
|
|
103
|
+
### Checkbox
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
const [skus, setSkus] = useState<string[]>([]);
|
|
107
|
+
|
|
108
|
+
<Tree {...props} selectionMode="checkbox" value={skus} onValueChange={setSkus} />;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`onValueChange(value, { selected })` gives the new value plus the items it lists, as `{ key, item, parents }[]` in tree order. Keys in the value that aren't in `items` (not loaded yet, say) are kept as they were.
|
|
112
|
+
|
|
113
|
+
## How ticking works
|
|
114
|
+
|
|
115
|
+
`selectionStrategy` decides how ticking spreads and what ends up in the value. The default covers the most common case: parents are just containers, and the value lists leaves.
|
|
116
|
+
|
|
117
|
+
Take a parent P with two leaves, A and B, starting from nothing ticked:
|
|
118
|
+
|
|
119
|
+
| Value | Tick P | Tick A and B | Can P be ticked on its own? |
|
|
120
|
+
| -------------------- | ----------- | ------------ | --------------------------- |
|
|
121
|
+
| `"leaves"` (default) | `[A, B]` | `[A, B]` | No |
|
|
122
|
+
| `"topmost"` | `[P]` | `[P]` | No |
|
|
123
|
+
| `"all"` | `[P, A, B]` | `[P, A, B]` | No |
|
|
124
|
+
| `"cascade"` | `[P, A, B]` | `[A, B]` | Yes |
|
|
125
|
+
| `"independent"` | `[P]` | `[A, B]` | Yes |
|
|
126
|
+
|
|
127
|
+
- **`"leaves"`, `"topmost"` and `"all"`** treat parents as containers. A parent's checkbox comes from its leaves, so unticking A after ticking P leaves P indeterminate. With `"topmost"`, `P` stands for everything beneath it.
|
|
128
|
+
- **`"cascade"`** makes parents items in their own right. Ticking one ticks everything beneath it too, but unticking A leaves `[P, B]`, with P still ticked.
|
|
129
|
+
- **`"independent"`** ticks every item on its own.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
// Everything in a warehouse comes back as the warehouse key, not every bin.
|
|
133
|
+
<Tree {...props} selectionMode="checkbox" selectionStrategy="topmost" />
|
|
134
|
+
|
|
135
|
+
// A parent purchase order can be approved without its child orders.
|
|
136
|
+
<Tree {...props} selectionMode="checkbox" selectionStrategy="independent" />
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Every combination round-trips: passing a value back in shows the same ticks. With `"leaves"`, `"topmost"` or `"all"`, a parent key in the value stands for every leaf beneath it, so `defaultValue={["wh-x"]}` ticks all of Warehouse X.
|
|
140
|
+
|
|
141
|
+
## Rows
|
|
142
|
+
|
|
143
|
+
The row shows the item's `label` by default. `renderItem` replaces it, and receives the row's state.
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
renderItem={({ item, isLeaf }) =>
|
|
147
|
+
isLeaf ? (
|
|
148
|
+
<span className="flex w-full justify-between gap-3">
|
|
149
|
+
<span>{item.name}</span>
|
|
150
|
+
<span className="text-muted-foreground tabular-nums">{money(item.price)}</span>
|
|
151
|
+
</span>
|
|
152
|
+
) : (
|
|
153
|
+
<strong>{item.name}</strong>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| Row state | Type | Description |
|
|
159
|
+
| --------------- | --------- | ------------------------------------------------------------------------------------------------- |
|
|
160
|
+
| `item` | `T` | Your original item. |
|
|
161
|
+
| `key` | `string` | The item's key. |
|
|
162
|
+
| `parents` | `T[]` | Ancestors, top level first. |
|
|
163
|
+
| `depth` | `number` | 0 for top-level rows. |
|
|
164
|
+
| `isLeaf` | `boolean` | The item has no children. |
|
|
165
|
+
| `checked` | `boolean` | Checkbox mode: the row is ticked. Where parents are containers, every enabled leaf beneath it is. |
|
|
166
|
+
| `indeterminate` | `boolean` | Checkbox mode, where parents are containers: some, but not all, enabled leaves beneath it are. |
|
|
167
|
+
| `selected` | `boolean` | Single mode: the row is the selected one. |
|
|
168
|
+
| `expanded` | `boolean` | The parent is open. |
|
|
169
|
+
| `disabled` | `boolean` | The item, an ancestor, or the whole tree is disabled. |
|
|
170
|
+
|
|
171
|
+
## Filtering
|
|
172
|
+
|
|
173
|
+
`filter` narrows the tree to matching items plus their ancestors, and opens the groups they're in. It receives the item with its `label` and `parents`. A parent that matches with no matching children keeps all of them, so it can still be opened.
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
const [query, setQuery] = useState("");
|
|
177
|
+
const filter = useMemo(
|
|
178
|
+
() => (query ? (_: Account, { label }: { label: string }) => label.includes(query) : undefined),
|
|
179
|
+
[query],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
<Input value={query} onChange={(event) => setQuery(event.target.value)} />
|
|
183
|
+
<Tree {...props} filter={filter} />
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Groups a filter opened can still be closed. They stay closed until the matches change, so an inline `filter` recreated on every render won't reopen them.
|
|
187
|
+
|
|
188
|
+
In checkbox mode, ticking a group while filtering acts on what's visible: it ticks the group's matching leaves, not the hidden ones. The group's checkbox still reflects the whole group, so it shows as partly ticked until every leaf beneath it is. Press it again to clear the visible matches.
|
|
189
|
+
|
|
190
|
+
## Forms
|
|
191
|
+
|
|
192
|
+
In checkbox mode, `Tree` works like `Select` and the other form controls: as a plain form field, inside `Field.Root`, and with React Hook Form.
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
<form action={saveCount}>
|
|
196
|
+
<Field.Root name="bins">
|
|
197
|
+
<Field.Label>Bins to count</Field.Label>
|
|
198
|
+
<Tree {...props} selectionMode="checkbox" required />
|
|
199
|
+
<Field.Description>Pick at least one bin.</Field.Description>
|
|
200
|
+
</Field.Root>
|
|
201
|
+
<Button type="submit">Start count</Button>
|
|
202
|
+
</form>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- **Submitting:** `name` (or the `Field.Root`'s name) renders one hidden input per value key, so the server reads `formData.getAll("bins")`. `itemToStringValue(item, { parents })` changes the submitted string.
|
|
206
|
+
- **`required`** blocks submission until at least one item is ticked.
|
|
207
|
+
- **Inside `Field.Root`**, `Field.Label` names the tree, `Field.Description` and `Field.Error` are announced with it, and an invalid field sets `data-invalid` on the root.
|
|
208
|
+
- **`disabled`** (or a disabled `Field.Root`) locks the whole tree and leaves it out of submission.
|
|
209
|
+
- **`onBlur`** fires once focus leaves the tree, not between rows. React Hook Form uses it to mark the field touched.
|
|
210
|
+
- **Enter** runs `onAction`, or submits the owning form when there's no `onAction`, like Enter in a text input.
|
|
211
|
+
|
|
212
|
+
Only checkbox mode is a form control. In `"none"` and `"single"` modes the tree doesn't register with `Field`.
|
|
213
|
+
|
|
214
|
+
## Disabled items
|
|
215
|
+
|
|
216
|
+
Return `disabled: true` from `mapItem` to lock an item. A disabled parent locks everything beneath it, and `disabled` on the tree locks every row. A locked row can't be ticked or selected, and Enter doesn't run `onAction` on it. A disabled item that's already ticked stays ticked.
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
mapItem={(bin) => ({ key: bin.code, label: bin.code, disabled: bin.status === "counting" })}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
How a parent behaves next to disabled children depends on whether it's a container or an item:
|
|
223
|
+
|
|
224
|
+
| Mode | Ticking a parent | A parent whose children are all disabled |
|
|
225
|
+
| -------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------- |
|
|
226
|
+
| Checkbox, `"leaves"` / `"topmost"` / `"all"` | Skips disabled leaves. It counts as ticked once every _enabled_ leaf beneath it is. | Locked, since there's nothing it could tick. |
|
|
227
|
+
| Checkbox, `"cascade"` | Ticks itself and every enabled item beneath it. | Usable. Ticking it ticks only itself. |
|
|
228
|
+
| Checkbox, `"independent"` | Ticks only itself. | Usable. |
|
|
229
|
+
| `"single"` and `"none"` selection modes | – | Usable: it can be selected or run. |
|
|
230
|
+
|
|
231
|
+
## TreePicker
|
|
232
|
+
|
|
233
|
+
A checkbox tree in a dialog, with search. It takes the same item, row and ticking props as `Tree`, plus the search and commit props. Each open starts empty, and `onCommit` receives the value's items in tree order. Ticking a group while searching ticks its matching items, as described under [Filtering](#filtering).
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
const isProduct = (item: Category | Product): item is Product => "sku" in item;
|
|
237
|
+
|
|
238
|
+
<TreePicker
|
|
239
|
+
open={open}
|
|
240
|
+
onOpenChange={setOpen}
|
|
241
|
+
title="Add items from catalogue"
|
|
242
|
+
items={categories}
|
|
243
|
+
mapItem={mapCatalogItem}
|
|
244
|
+
renderItem={renderCatalogItem}
|
|
245
|
+
matchesSearch={(item, query) => item.name.toLowerCase().includes(query.toLowerCase())}
|
|
246
|
+
onCommit={(picked) => addToOrder(picked.flatMap(({ item }) => (isProduct(item) ? [item] : [])))}
|
|
247
|
+
/>;
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`onCommit`'s items have the union type too, so narrow them with a type guard rather than casting to `Product`. With the default `"leaves"` only products come back, but a guard keeps that true if you later switch to `"topmost"` or `"all"`, which can return categories.
|
|
251
|
+
|
|
252
|
+
## Props
|
|
253
|
+
|
|
254
|
+
### Shared (Tree and TreePicker)
|
|
255
|
+
|
|
256
|
+
| Prop | Type | Default | Description |
|
|
257
|
+
| ------------ | ---------------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------- |
|
|
258
|
+
| `items` | `ReadonlyArray<T>` | required | Your top-level items. |
|
|
259
|
+
| `mapItem` | `(item: T, { parents }) => { key, label, children?, disabled? }` | required | Reads an item's key, label, children and disabled state. |
|
|
260
|
+
| `renderItem` | `(row) => ReactNode` | the item's `label` | The row's content. |
|
|
261
|
+
| `emptyText` | `ReactNode` | "No items." | Shown when there's nothing to show ("No matching items." while filtering). |
|
|
262
|
+
|
|
263
|
+
### Tree
|
|
264
|
+
|
|
265
|
+
| Prop | Type | Default | Description |
|
|
266
|
+
| -------------------------------- | ------------------------------------------ | -------- | ----------------------------------------------------------------------- |
|
|
267
|
+
| `selectionMode` | `"none" \| "single" \| "checkbox"` | `"none"` | What clicking and Space do to a row. |
|
|
268
|
+
| `onAction` | `(item: T, key: string) => void` | – | Enter on a row. In `"none"` mode, a click on a leaf too. |
|
|
269
|
+
| `filter` | `(item: T, { label, parents }) => boolean` | – | Shows only matches and their ancestors. |
|
|
270
|
+
| `disabled` | `boolean` | `false` | Locks every row, and in checkbox mode leaves it out of form submission. |
|
|
271
|
+
| `aria-label` / `aria-labelledby` | `string` | – | Names the tree, when there's no `Field.Label`. |
|
|
272
|
+
| `className` | `string` | – | Merged onto the root. Give it a height for long trees. |
|
|
273
|
+
|
|
274
|
+
With `selectionMode="single"`:
|
|
275
|
+
|
|
276
|
+
| Prop | Type | Default | Description |
|
|
277
|
+
| ------------------------------------ | -------------------------------- | ------- | ------------------------------------------------------ |
|
|
278
|
+
| `selectedKey` / `defaultSelectedKey` | `string \| null` | `null` | Controlled / initial selected row. |
|
|
279
|
+
| `onSelectedKeyChange` | `(key: string, item: T) => void` | – | Fires when the user selects a row. |
|
|
280
|
+
| `revealSelected` | `boolean` | `true` | Opens the selected row\'s ancestors when it\'s hidden. |
|
|
281
|
+
|
|
282
|
+
With `selectionMode="checkbox"`:
|
|
283
|
+
|
|
284
|
+
| Prop | Type | Default | Description |
|
|
285
|
+
| ---------------------------- | -------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------- |
|
|
286
|
+
| `value` / `defaultValue` | `string[]` | – | Controlled / initial value: the ticked keys. |
|
|
287
|
+
| `onValueChange` | `(value, { selected }) => void` | – | Every change, plus the items the value lists. |
|
|
288
|
+
| `selectionStrategy` | `"leaves" \| "topmost" \| "all" \| "cascade" \| "independent"` | `"leaves"` | How ticking works and what the value lists. See [How ticking works](#how-ticking-works). |
|
|
289
|
+
| `name` / `form` | `string` | – | Hidden inputs for native form submission. `Field.Root`'s name wins. |
|
|
290
|
+
| `itemToStringValue` | `(item, { parents }) => string` | the key | The string each hidden input carries. |
|
|
291
|
+
| `required` | `boolean` | `false` | At least one item must be ticked. |
|
|
292
|
+
| `id` / `inputRef` / `onBlur` | – | – | Form control id, the hidden proxy input, and blur from the whole tree. |
|
|
293
|
+
|
|
294
|
+
### TreePicker
|
|
295
|
+
|
|
296
|
+
| Prop | Type | Default | Description |
|
|
297
|
+
| ------------------- | ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------- |
|
|
298
|
+
| `open` | `boolean` | required | Controls dialog visibility. |
|
|
299
|
+
| `onOpenChange` | `(open: boolean) => void` | required | Fires on every user-driven open or close, including after a commit. |
|
|
300
|
+
| `onCommit` | `(selected: TreeSelectedItem<T>[]) => void` | required | Receives the value's items in tree order. |
|
|
301
|
+
| `selectionStrategy` | as on `Tree` | `"leaves"` | How ticking works while picking. |
|
|
302
|
+
| `matchesSearch` | `(item: T, query: string, context: { label, parents }) => boolean` | match on `label` | Search predicate. An item is kept when it or a descendant matches. |
|
|
303
|
+
| `filterSlot` | `ReactNode` | – | Beside the search input, e.g. a category filter. |
|
|
304
|
+
| `searchPlaceholder` | `string` | `"Search"` | Search input placeholder. |
|
|
305
|
+
| `title` | `ReactNode` | `"Select items"` | Dialog title. Also labels the tree for assistive tech. |
|
|
306
|
+
| `ctaLabel` | `(count: number) => ReactNode` | `"Add N items"` | Primary button label. |
|
|
307
|
+
| `className` | `string` | – | Merged onto the dialog panel. |
|
|
308
|
+
|
|
309
|
+
The default labels are localised (English and Japanese) through the app's locale, like DataTable's. Passing a label prop overrides it.
|
|
310
|
+
|
|
311
|
+
## Behaviour
|
|
312
|
+
|
|
313
|
+
**Keyboard.** The tree is a `role="tree"` with roving focus. Arrow Up / Down move between visible rows, Arrow Right opens a closed parent or steps into it, Arrow Left closes or steps out to the parent, and Home / End jump to the ends. Space selects or ticks the focused row, and Enter runs `onAction` (in the picker, it commits).
|
|
314
|
+
|
|
315
|
+
**Mouse.** In checkbox mode, clicking a group row opens or closes it and its checkbox ticks everything inside, while clicking an item row ticks it. In single mode, clicking selects. In `"none"` mode, clicking a leaf runs `onAction`.
|
|
316
|
+
|
|
317
|
+
**Picker search.** Matches stay visible with their ancestors opened. Escape clears the search first, and a second Escape closes the dialog. Enter in the search box or in a `filterSlot` control (a menu or select) is left to that control.
|
|
318
|
+
|
|
319
|
+
**Picker height.** The picker keeps one height and the list scrolls inside it, so opening or closing a group never resizes the dialog or moves what's on screen.
|
|
320
|
+
|
|
321
|
+
## Loading an async catalogue
|
|
322
|
+
|
|
323
|
+
There's deliberately no async variant yet: filtering and auto-expanding need the whole tree up front. Fetch once before or on open, and drive `emptyText` from your own loading flag. Keys in `value` for items that haven't loaded yet are kept, so a controlled selection survives the load.
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
const { data, isLoading } = useCatalogQuery();
|
|
327
|
+
|
|
328
|
+
<TreePicker
|
|
329
|
+
open={open}
|
|
330
|
+
onOpenChange={setOpen}
|
|
331
|
+
items={data ?? []}
|
|
332
|
+
mapItem={mapCatalogItem}
|
|
333
|
+
emptyText={isLoading ? "Loading catalogue…" : undefined}
|
|
334
|
+
onCommit={handleCommit}
|
|
335
|
+
/>;
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Related
|
|
339
|
+
|
|
340
|
+
- [Combobox](./combobox.md) — inline single or multi select for flat option lists
|
|
341
|
+
- [Select](./select.md) — dropdown select, with the same `items` + `mapItem` pattern
|
|
342
|
+
- [Dialog](./dialog.md) — the modal shell `TreePicker` composes
|
|
@@ -18,14 +18,14 @@ import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";
|
|
|
18
18
|
## Basic Usage
|
|
19
19
|
|
|
20
20
|
```tsx
|
|
21
|
-
import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";
|
|
21
|
+
import { WithGuard, pass, hidden, Text } from "@tailor-platform/app-shell";
|
|
22
22
|
|
|
23
23
|
const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass() : hidden());
|
|
24
24
|
|
|
25
25
|
function AdminPanel() {
|
|
26
26
|
return (
|
|
27
27
|
<div>
|
|
28
|
-
<h1>Dashboard</h1>
|
|
28
|
+
<Text.h1>Dashboard</Text.h1>
|
|
29
29
|
|
|
30
30
|
<WithGuard guards={[isAdmin]}>
|
|
31
31
|
<Button>Delete All Users</Button>
|
|
@@ -113,7 +113,7 @@ const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass()
|
|
|
113
113
|
Show alternative content when guard fails:
|
|
114
114
|
|
|
115
115
|
```tsx
|
|
116
|
-
<WithGuard guards={[isAdmin]} fallback={<p>You need admin access to view this.</p>}>
|
|
116
|
+
<WithGuard guards={[isAdmin]} fallback={<Text.p>You need admin access to view this.</Text.p>}>
|
|
117
117
|
<AdminPanel />
|
|
118
118
|
</WithGuard>
|
|
119
119
|
```
|
|
@@ -212,8 +212,8 @@ const isPremium = ({ context }) =>
|
|
|
212
212
|
guards={[isPremium]}
|
|
213
213
|
fallback={
|
|
214
214
|
<Card>
|
|
215
|
-
<h3>Premium Feature</h3>
|
|
216
|
-
<p>Upgrade to access advanced analytics.</p>
|
|
215
|
+
<Text.h3>Premium Feature</Text.h3>
|
|
216
|
+
<Text.p>Upgrade to access advanced analytics.</Text.p>
|
|
217
217
|
<Button>Upgrade Now</Button>
|
|
218
218
|
</Card>
|
|
219
219
|
}
|
|
@@ -131,7 +131,7 @@ Reach for `TestRouter` when the component under test is the subject; mount `AppS
|
|
|
131
131
|
## Example
|
|
132
132
|
|
|
133
133
|
```tsx
|
|
134
|
-
import { useNavigate, useParams, useLocation, Link } from "@tailor-platform/app-shell";
|
|
134
|
+
import { useNavigate, useParams, useLocation, Link, Text } from "@tailor-platform/app-shell";
|
|
135
135
|
|
|
136
136
|
const MyComponent = () => {
|
|
137
137
|
const navigate = useNavigate();
|
|
@@ -145,8 +145,8 @@ const MyComponent = () => {
|
|
|
145
145
|
|
|
146
146
|
return (
|
|
147
147
|
<div>
|
|
148
|
-
<p>Current path: {location.pathname}</p>
|
|
149
|
-
<p>Route param ID: {id}</p>
|
|
148
|
+
<Text.p>Current path: {location.pathname}</Text.p>
|
|
149
|
+
<Text.p>Route param ID: {id}</Text.p>
|
|
150
150
|
|
|
151
151
|
{/* Client-side link */}
|
|
152
152
|
<Link to="/products">View Products</Link>
|
|
@@ -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,13 @@ 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 all five intents, `text` on `surface-hover` is at least 4.5:1 for `neutral`, 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. This is on purpose: a lighter step would make the tinted background hard to tell apart from the page, and the hover harder to see. 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.
|
|
221
227
|
|
|
222
228
|
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
229
|
|
|
@@ -227,7 +233,7 @@ Most `surface` and `border` values are translucent tints, so an opacity modifier
|
|
|
227
233
|
|
|
228
234
|
`--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
235
|
|
|
230
|
-
`--chart-1` … `--chart-5` → `bg-chart-1`, `text-chart-1`, `fill-chart-1`. Use them in order for categorical series.
|
|
236
|
+
`--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
237
|
|
|
232
238
|
#### Alerts
|
|
233
239
|
|
|
@@ -283,7 +289,7 @@ There are 8 core roles and 2 relaxed body variants:
|
|
|
283
289
|
| `heading-sm` | `text-heading-sm` | 14px / 20px | 600 | Sub-section heading |
|
|
284
290
|
| `body-md` | `text-body-md` | 14px / 20px | 400 | Body copy, descriptions |
|
|
285
291
|
| `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
|
|
292
|
+
| `label-md` | `text-label-md` | 14px / 20px | 500 | Field label, short UI text |
|
|
287
293
|
| `label-sm` | `text-label-sm` | 12px / 16px | 500 | Small label, badge text |
|
|
288
294
|
| `code-sm` | `text-code-sm` | 12px / 18px | 400 | ID and code. Use with `font-mono` |
|
|
289
295
|
| `body-md-relaxed` | `text-body-md-relaxed` | 14px / 24px | 400 | Long text, such as a chat message or a document |
|
|
@@ -301,7 +307,7 @@ Pair a role with a color token from the Color section above. These pairings are
|
|
|
301
307
|
| Body copy | `text-body-md` |
|
|
302
308
|
| Secondary copy, descriptions | `text-body-md text-muted-foreground` |
|
|
303
309
|
| Caption, timestamp, metadata | `text-body-sm text-muted-foreground` |
|
|
304
|
-
| Numeric value in a table or field | `text-
|
|
310
|
+
| Numeric value in a table or field | `text-body-md tabular-nums` |
|
|
305
311
|
| ID, code | `font-mono text-code-sm text-muted-foreground` |
|
|
306
312
|
|
|
307
313
|
<example-preview name="typography-roles"></example-preview>
|
|
@@ -404,20 +410,21 @@ function TypographyRoles() {
|
|
|
404
410
|
}
|
|
405
411
|
```
|
|
406
412
|
|
|
413
|
+
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-*`. The `prefer-text-components` rule in `@tailor-platform/eslint-plugin-app-shell` reports a size on these elements, and a weight, line height, or letter spacing that is set without a role. It is not in `recommended`, so enable it in your `oxlint.config.ts`. 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`):
|
|
414
|
+
|
|
407
415
|
```tsx
|
|
408
|
-
<
|
|
409
|
-
<
|
|
410
|
-
<
|
|
416
|
+
<Text.h3 variant="heading-md">Line items</Text.h3>
|
|
417
|
+
<Text variant="body-md" className="text-muted-foreground">Description copy</Text>
|
|
418
|
+
<Text variant="body-sm" className="text-muted-foreground">Updated 2h ago</Text>
|
|
411
419
|
```
|
|
412
420
|
|
|
413
|
-
|
|
421
|
+
Use the role utility when you style an element that you render yourself, such as a table header cell or a link:
|
|
414
422
|
|
|
415
423
|
```tsx
|
|
416
|
-
<
|
|
417
|
-
<Text variant="body-md" className="text-muted-foreground">Description copy</Text>
|
|
424
|
+
<th className="text-label-md text-muted-foreground">Amount</th>
|
|
418
425
|
```
|
|
419
426
|
|
|
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
|
|
427
|
+
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
428
|
|
|
422
429
|
**Rules**
|
|
423
430
|
|
|
@@ -522,21 +529,22 @@ Always give motion a reduced-motion escape (`motion-reduce:transition-none`, or
|
|
|
522
529
|
|
|
523
530
|
### Z-index
|
|
524
531
|
|
|
525
|
-
These exist as raw `:root` variables in the shipped base layer, but are **not** bridged into Tailwind — `z-
|
|
532
|
+
These exist as raw `:root` variables in the shipped base layer, but are **not** bridged into Tailwind — `z-nav` is not a class. Use the stock numeric utility, or reference the variable when you need it to track AppShell's layering:
|
|
533
|
+
|
|
534
|
+
| Token | Value | Use |
|
|
535
|
+
| ------------ | ----- | ----------------------------------------------------- |
|
|
536
|
+
| `--z-nav` | 10 | persistent sidebar |
|
|
537
|
+
| `--z-portal` | 50 | menu, tooltip, popover, modal, sheet, dialog backdrop |
|
|
538
|
+
| `--z-toast` | 100 | toast, always above overlays |
|
|
526
539
|
|
|
527
|
-
|
|
528
|
-
| ------------------ | ----- | ----------------------------- |
|
|
529
|
-
| `--z-sidebar` | 10 | persistent sidebar |
|
|
530
|
-
| `--z-sidebar-rail` | 20 | sidebar collapsed rail |
|
|
531
|
-
| `--z-popup` | 50 | menu, tooltip, popover |
|
|
532
|
-
| `--z-overlay` | 50 | modal, sheet, dialog backdrop |
|
|
540
|
+
The old names still resolve: `--z-sidebar` is an alias of `--z-nav`, and `--z-popup` and `--z-overlay` are aliases of `--z-portal`. They are deprecated and will be removed in the next major. `--z-sidebar-rail` (20) is a value inside the sidebar, not a layer.
|
|
533
541
|
|
|
534
542
|
```tsx
|
|
535
543
|
<div className="z-50" />
|
|
536
|
-
<div className="z-[var(--z-
|
|
544
|
+
<div className="z-[var(--z-portal)]" />
|
|
537
545
|
```
|
|
538
546
|
|
|
539
|
-
Never invent a z value — `z-index: 9999` is always wrong. Popups and overlays share `50` intentionally: sequencing comes from DOM order, not z escalation.
|
|
547
|
+
Never invent a z value — `z-index: 9999` is always wrong. Popups and overlays share `50` intentionally: sequencing comes from DOM order, not z escalation. Every floating AppShell component (dialog, sheet, menu, select, combobox, tooltip, toast) renders through a portal to the end of `body`, and the layout root is `isolation: isolate`, so a z-index inside the page never reaches them. The toast is the one layer above the band: it is always visible over an open dialog or sheet.
|
|
540
548
|
|
|
541
549
|
### Icons
|
|
542
550
|
|
|
@@ -10,6 +10,7 @@ Each entry states which versions are affected, what breaks, how to detect it, an
|
|
|
10
10
|
|
|
11
11
|
| Version | Change |
|
|
12
12
|
| ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| 1.20.0 | [z-index tokens renamed; old names are aliases](#1200-z-index-tokens-renamed-old-names-are-aliases) |
|
|
13
14
|
| 1.18.0 | [Destructive colours now resolve to the danger roles](#1180-destructive-colours-now-resolve-to-the-danger-roles) |
|
|
14
15
|
| 1.17.0 | [Status and alert tokens now resolve to the colour roles](#1170-status-and-alert-tokens-now-resolve-to-the-colour-roles) |
|
|
15
16
|
| 1.15.0 | [Base UI 1.8.0 breaks closed-`Select` and dialog-click tests](#1150-base-ui-180-breaks-closed-select-and-dialog-click-tests) |
|
|
@@ -23,6 +24,25 @@ Each entry states which versions are affected, what breaks, how to detect it, an
|
|
|
23
24
|
| 1.0.2 | [`Toaster` no longer accepts `richColors`](#102-toaster-no-longer-accepts-richcolors) |
|
|
24
25
|
| before 1.0 | [Pre-1.0 breaking changes](#before-10) |
|
|
25
26
|
|
|
27
|
+
## 1.20.0: z-index tokens renamed; old names are aliases
|
|
28
|
+
|
|
29
|
+
**Applies to:** apps whose theme overrides `--z-sidebar`, `--z-sidebar-rail`, `--z-popup` or `--z-overlay`, and apps that reference those names on their own elements.
|
|
30
|
+
|
|
31
|
+
1.20.0 names the z-index tokens by layer: `--z-nav` (10), `--z-portal` (50) and `--z-toast` (100). The old names keep working: `--z-sidebar` is now `var(--z-nav)`, and `--z-popup` and `--z-overlay` are both `var(--z-portal)`. They are deprecated and will be removed in the next major. Nothing errors, and nothing moves. Two things change silently:
|
|
32
|
+
|
|
33
|
+
- **Themes that override an old name no longer reach the components.** The sidebar reads `--z-nav`; dialogs, sheets, menus, selects, comboboxes and tooltips read `--z-portal`. An override of `--z-popup` or `--z-overlay` still changes `z-[var(--z-popup)]` on your own elements, but not AppShell's layers. There is one token for the whole portal band because popups and overlays share the value on purpose: the order inside the band is the order of opening, so a `Select` inside a `Dialog` is above it. Raising one above the other puts a `Select` or `Menu` opened inside a `Dialog` behind the dialog.
|
|
34
|
+
- **`--z-sidebar-rail` is no longer read by AppShell.** The sidebar rail orders itself inside the sidebar. The variable is still declared (20) for elements of your own that reference it, and it leaves the documented table.
|
|
35
|
+
|
|
36
|
+
How to detect it: search the app for `--z-sidebar`, `--z-popup` and `--z-overlay` in class names and in theme CSS. Open a `Dialog` with a `Select` inside it, a `Dialog` over a `Sheet`, and fire a toast over each after upgrading; the order should not change.
|
|
37
|
+
|
|
38
|
+
What to change:
|
|
39
|
+
|
|
40
|
+
- Replace `--z-sidebar` with `--z-nav`, and `--z-popup` and `--z-overlay` with `--z-portal`, in class names such as `z-[var(--z-portal)]` and in theme CSS.
|
|
41
|
+
- In a theme that moves AppShell's layers, set `--z-nav`, `--z-portal` or `--z-toast` instead of the old names. Remove the old names from the theme.
|
|
42
|
+
- Do not set `--z-sidebar-rail`; it no longer affects the rail.
|
|
43
|
+
|
|
44
|
+
The tokens are documented in [Styling & theming](https://github.com/tailor-platform/app-shell/blob/main/docs/concepts/styling-theming.md#z-index). The decision record is `decisions/floating-ui-stacking.md`.
|
|
45
|
+
|
|
26
46
|
## 1.18.0: destructive colours now resolve to the danger roles
|
|
27
47
|
|
|
28
48
|
**Applies to:** apps that show `Button variant="destructive"` in dark mode, apps that use `text-destructive`, `border-destructive` or `bg-destructive` on their own elements, and apps whose theme overrides `--destructive` or `--destructive-foreground`.
|
|
@@ -33,7 +53,7 @@ Each entry states which versions are affected, what breaks, how to detect it, an
|
|
|
33
53
|
- **`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
54
|
- **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
55
|
|
|
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.
|
|
56
|
+
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
57
|
|
|
38
58
|
What to change:
|
|
39
59
|
|
|
@@ -53,7 +73,7 @@ The roles are documented in [Styling & theming](https://github.com/tailor-platfo
|
|
|
53
73
|
- **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
74
|
- **`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
75
|
|
|
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.
|
|
76
|
+
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
77
|
|
|
58
78
|
What to change:
|
|
59
79
|
|