@tailor-platform/app-shell 1.17.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 +503 -137
- package/dist/app-shell.js +8847 -7378
- package/dist/{appshell-CahhZy7v.js → appshell-DQTz-QwK.js} +489 -379
- package/dist/style-entry.d.ts +1 -656
- package/dist/testing.d.ts +118 -113
- package/dist/testing.js +1 -1
- package/dist/theme.bridge.css +127 -40
- package/dist/theme.css +4 -3
- package/dist/themes/_template.css +5 -6
- package/dist/themes/bloom.css +0 -2
- package/dist/themes/cream.css +0 -2
- package/dist/themes/default.css +112 -77
- package/dist/vite-plugin.d.ts +118 -113
- package/package.json +8 -5
- package/skills/app-shell-patterns/SKILL.md +51 -32
- package/skills/app-shell-patterns/references/api/aggregations.md +131 -0
- package/skills/app-shell-patterns/references/api/define-i18n-labels.md +123 -91
- package/skills/app-shell-patterns/references/api/define-module.md +2 -4
- 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 +724 -36
- 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/sidebar-group.md +6 -6
- package/skills/app-shell-patterns/references/components/text.md +235 -0
- package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +16 -14
- package/skills/app-shell-patterns/references/concepts/styling-theming.md +46 -22
- package/skills/app-shell-patterns/references/migrations.md +23 -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/skills/app-shell-patterns/references/patterns/i18n-labels.md +139 -0
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +3 -2
- package/dist/semantic-contrast.test.ts +0 -154
- package/dist/theme-bridge.test.ts +0 -159
|
@@ -159,17 +159,26 @@ Colors follow shadcn-style semantic naming: a surface token and its matching `-f
|
|
|
159
159
|
| `--border` | hairlines, dividers | `border-border` |
|
|
160
160
|
| `--input` | form control borders | `border-input` |
|
|
161
161
|
| `--ring` | focus rings | `ring-ring` / `outline-ring` |
|
|
162
|
+
| `--backdrop` | dim layer behind a modal | `bg-backdrop` |
|
|
162
163
|
|
|
163
164
|
Surfaces are named by **role**, not by depth — there is no numbered `surface-1/2/3` ladder, and no third tier of body text below `muted-foreground`. Stack depth with `background` → `card` → `muted` plus a border or shadow, and reach for `popover` when the surface actually floats.
|
|
164
165
|
|
|
166
|
+
`--backdrop` is the dim layer behind `Dialog` and `Sheet` (black at 50% in both modes). Use `bg-backdrop` for your own modal layers instead of `bg-black/50`. Palettes inherit it; override it on `:root` to change every backdrop at once.
|
|
167
|
+
|
|
168
|
+
In the default palette, the surface, text and line tokens in this table read the internal gray scale, a white step, and white alpha steps (dark-mode lines), so their values match the gray used across the palette. `--backdrop` reads a black alpha step, and `--ring` is mixed from `--primary`. The other palettes set their own values.
|
|
169
|
+
|
|
165
170
|
#### Brand & action
|
|
166
171
|
|
|
167
|
-
| Token | Use
|
|
168
|
-
| -------------------------------------------- |
|
|
169
|
-
| `--primary` / `--primary-foreground` | primary buttons, emphasis
|
|
170
|
-
| `--secondary` / `--secondary-foreground` | secondary buttons, neutral chips
|
|
171
|
-
| `--accent` / `--accent-foreground` | hover and selected nav states
|
|
172
|
-
| `--destructive` / `--destructive-foreground` |
|
|
172
|
+
| Token | Use | Tailwind |
|
|
173
|
+
| -------------------------------------------- | ------------------------------------------- | -------------------------------------------- |
|
|
174
|
+
| `--primary` / `--primary-foreground` | primary buttons, emphasis | `bg-primary` / `text-primary-foreground` |
|
|
175
|
+
| `--secondary` / `--secondary-foreground` | secondary buttons, neutral chips | `bg-secondary` / `text-secondary-foreground` |
|
|
176
|
+
| `--accent` / `--accent-foreground` | hover and selected nav states | `bg-accent` / `text-accent-foreground` |
|
|
177
|
+
| `--destructive` / `--destructive-foreground` | deprecated alias of danger solid / contrast | `bg-destructive` / `text-destructive` |
|
|
178
|
+
|
|
179
|
+
**Deprecated.** `--destructive`, `--destructive-foreground` and their utilities remain for existing code and will be removed in the next major. New code uses the danger roles: `bg-danger-solid`, `text-danger-contrast`, and `text-danger-text` for text. `Button variant="destructive"` is not deprecated yet; the next major renames it to `danger`.
|
|
180
|
+
|
|
181
|
+
`--destructive` and `--destructive-foreground` are aliases of the danger `solid` and `contrast` roles ([semantic color roles](#semantic-color-roles)), so `bg-destructive` is the danger solid fill in both modes and `Button variant="destructive"` reads the danger roles directly. `text-destructive` still works and resolves to the solid. For text, use `text-danger-text`, which meets 4.5:1 on `--card` and `--background`; the solid is `#dc2626` in dark mode, 3.71:1 on the dark card. To recolour destructive actions, override `--danger-solid` (and the other danger roles), not `--destructive`.
|
|
173
182
|
|
|
174
183
|
There are no `-hover` or `-active` brand tokens. Express interaction states with Tailwind variants and opacity — `hover:bg-primary/90`, `active:bg-primary/80` — which is what AppShell's own components do.
|
|
175
184
|
|
|
@@ -208,17 +217,24 @@ Each intent has eight roles. The intents are `info`, `success`, `warning`, `dang
|
|
|
208
217
|
<span className="rounded-md bg-success-surface px-2 py-0.5 text-success-text">Paid</span>
|
|
209
218
|
```
|
|
210
219
|
|
|
211
|
-
|
|
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.
|
|
212
228
|
|
|
213
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.
|
|
214
230
|
|
|
215
|
-
`Badge`, `Alert`, `CsvImporter` and `MetricCard` read these roles directly, not `--status-*` or `--alert-*`. To recolour them, override the roles. `Badge` `error` and `subtle-error`, and `Alert` `error`, follow `--danger-*`.
|
|
231
|
+
`Badge`, `Alert`, `CsvImporter` and `MetricCard` read these roles directly, not `--status-*` or `--alert-*`. To recolour them, override the roles. `Badge` `error` and `subtle-error`, and `Alert` `error`, follow `--danger-*`. `Button variant="destructive"`, invalid input borders and error text follow `--danger-*` too, and `--destructive` is an alias of `--danger-solid`.
|
|
216
232
|
|
|
217
233
|
#### Sidebar & charts
|
|
218
234
|
|
|
219
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.
|
|
220
236
|
|
|
221
|
-
`--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.
|
|
222
238
|
|
|
223
239
|
#### Alerts
|
|
224
240
|
|
|
@@ -274,7 +290,7 @@ There are 8 core roles and 2 relaxed body variants:
|
|
|
274
290
|
| `heading-sm` | `text-heading-sm` | 14px / 20px | 600 | Sub-section heading |
|
|
275
291
|
| `body-md` | `text-body-md` | 14px / 20px | 400 | Body copy, descriptions |
|
|
276
292
|
| `body-sm` | `text-body-sm` | 12px / 16px | 400 | Caption, timestamp, metadata |
|
|
277
|
-
| `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 |
|
|
278
294
|
| `label-sm` | `text-label-sm` | 12px / 16px | 500 | Small label, badge text |
|
|
279
295
|
| `code-sm` | `text-code-sm` | 12px / 18px | 400 | ID and code. Use with `font-mono` |
|
|
280
296
|
| `body-md-relaxed` | `text-body-md-relaxed` | 14px / 24px | 400 | Long text, such as a chat message or a document |
|
|
@@ -288,11 +304,11 @@ Pair a role with a color token from the Color section above. These pairings are
|
|
|
288
304
|
| --------------------------------- | ---------------------------------------------- |
|
|
289
305
|
| Page title (`Layout.Header`) | `text-heading-lg` |
|
|
290
306
|
| Section heading | `text-heading-md` |
|
|
291
|
-
| Card title
|
|
307
|
+
| Card title | `text-heading-md leading-none` |
|
|
292
308
|
| Body copy | `text-body-md` |
|
|
293
309
|
| Secondary copy, descriptions | `text-body-md text-muted-foreground` |
|
|
294
310
|
| Caption, timestamp, metadata | `text-body-sm text-muted-foreground` |
|
|
295
|
-
| Numeric value in a table or field | `text-
|
|
311
|
+
| Numeric value in a table or field | `text-body-md tabular-nums` |
|
|
296
312
|
| ID, code | `font-mono text-code-sm text-muted-foreground` |
|
|
297
313
|
|
|
298
314
|
<example-preview name="typography-roles"></example-preview>
|
|
@@ -395,23 +411,31 @@ function TypographyRoles() {
|
|
|
395
411
|
}
|
|
396
412
|
```
|
|
397
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
|
+
|
|
416
|
+
```tsx
|
|
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>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Use the role utility when you style an element that you render yourself, such as a table header cell or a link:
|
|
423
|
+
|
|
398
424
|
```tsx
|
|
399
|
-
<
|
|
400
|
-
<p className="text-body-md text-muted-foreground">Description copy</p>
|
|
401
|
-
<span className="text-body-sm text-muted-foreground">Updated 2h ago</span>
|
|
425
|
+
<th className="text-label-md text-muted-foreground">Amount</th>
|
|
402
426
|
```
|
|
403
427
|
|
|
404
|
-
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.
|
|
405
429
|
|
|
406
430
|
**Rules**
|
|
407
431
|
|
|
408
432
|
- **A role carries no color.** Set the color with a token such as `text-muted-foreground`.
|
|
409
|
-
- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size.
|
|
433
|
+
- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size. With `Text`, set the element with `render`.
|
|
410
434
|
- **A role sets weight and letter spacing on its own element.** It does not inherit them from a parent. Put an emphasis or tracking class on the element that has the role, not on its parent.
|
|
411
435
|
- **Four modifiers may be combined with a role.** All are stock Tailwind utilities:
|
|
412
436
|
- Compact line height: `leading-none`. Use it only for single-line text, such as a card title. Write it after the role when you merge with `cn()`.
|
|
413
437
|
- Tabular figures: `tabular-nums`.
|
|
414
|
-
- Emphasis on inline text: `font-
|
|
438
|
+
- Emphasis on inline text: `font-semibold`. Do not use `font-medium`; on a label role it makes no change.
|
|
415
439
|
- Uppercase: `uppercase`.
|
|
416
440
|
- **Do not combine a role with other `leading-*`, `tracking-*`, or `text-[Npx]` values.** If none of the roles fits, use the nearest role.
|
|
417
441
|
- **12px is the minimum size.** Do not use smaller text.
|
|
@@ -420,12 +444,12 @@ On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's
|
|
|
420
444
|
|
|
421
445
|
Always use `tabular-nums` for numbers that stack in a column — without it, digits jitter between rows.
|
|
422
446
|
|
|
423
|
-
**Change a role.** Each role is four CSS variables named `--
|
|
447
|
+
**Change a role.** Each role is four CSS variables named `--type-<role>-<size|line-height|weight|letter-spacing>`. Override them on `:root` to change a role for the whole app. Typography has no dark variant, so one override applies to light and dark mode. 1.17.0 named these variables `--app-shell-type-*`. The old names still work, but they are deprecated and will be removed in the next major; rename them to `--type-*`.
|
|
424
448
|
|
|
425
449
|
```css
|
|
426
450
|
:root {
|
|
427
|
-
--
|
|
428
|
-
--
|
|
451
|
+
--type-heading-lg-size: 1.75rem;
|
|
452
|
+
--type-heading-lg-line-height: calc(36 / 28);
|
|
429
453
|
}
|
|
430
454
|
```
|
|
431
455
|
|
|
@@ -769,7 +793,7 @@ These are visual-composition rules every screen must follow, regardless of patte
|
|
|
769
793
|
|
|
770
794
|
| Intent | Pick |
|
|
771
795
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
772
|
-
| Destructive action (delete, void) | `Button variant="destructive"`; `bg-
|
|
796
|
+
| Destructive action (delete, void) | `Button variant="destructive"`; `bg-danger-solid` on custom surface; confirm in a dialog at `shadow-lg` |
|
|
773
797
|
| Non-blocking caution | `Badge variant="warning"`, or `bg-warning-surface text-warning-text` |
|
|
774
798
|
| Confirmation / completed state | `Badge variant="success"`, or `bg-success-surface text-success-text` |
|
|
775
799
|
| Informational callout | `Badge variant="info"`, or `Alert variant="info"` |
|
|
@@ -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.18.0 | [Destructive colours now resolve to the danger roles](#1180-destructive-colours-now-resolve-to-the-danger-roles) |
|
|
13
14
|
| 1.17.0 | [Status and alert tokens now resolve to the colour roles](#1170-status-and-alert-tokens-now-resolve-to-the-colour-roles) |
|
|
14
15
|
| 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) |
|
|
15
16
|
| 1.12.0 | [`DateField` / `DatePicker` field chrome moved to `Field.Root`](#1120-datefield--datepicker-field-chrome-moved-to-fieldroot) |
|
|
@@ -22,6 +23,26 @@ Each entry states which versions are affected, what breaks, how to detect it, an
|
|
|
22
23
|
| 1.0.2 | [`Toaster` no longer accepts `richColors`](#102-toaster-no-longer-accepts-richcolors) |
|
|
23
24
|
| before 1.0 | [Pre-1.0 breaking changes](#before-10) |
|
|
24
25
|
|
|
26
|
+
## 1.18.0: destructive colours now resolve to the danger roles
|
|
27
|
+
|
|
28
|
+
**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`.
|
|
29
|
+
|
|
30
|
+
1.18.0 makes `--destructive` and `--destructive-foreground` aliases of `--danger-solid` and `--danger-contrast`. The names, `Button variant="destructive"` and `text-destructive` keep working. Nothing errors. `--destructive`, `--destructive-foreground` and their utilities are deprecated and will be removed in the next major; `Button variant="destructive"` will be renamed to `danger` then. Three things change silently:
|
|
31
|
+
|
|
32
|
+
- **Dark-mode `Button variant="destructive"` is a full `#dc2626` fill.** It was `#f87171` at 60% opacity. The text is `--danger-contrast` (white) and the hover fill is `--danger-solid-hover` instead of 90% opacity. Light mode keeps `#dc2626`.
|
|
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
|
+
- **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
|
+
|
|
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
|
+
|
|
38
|
+
What to change:
|
|
39
|
+
|
|
40
|
+
- For text, replace `text-destructive` with `text-danger-text`. For fills, replace `bg-destructive` with `bg-danger-solid`, and `text-destructive-foreground` with `text-danger-contrast`.
|
|
41
|
+
- For tints and borders, use `bg-danger-surface` and `border-danger-border` instead of `bg-destructive/10` and `border-destructive/30`.
|
|
42
|
+
- In a theme that recolours destructive actions, set `--danger-solid` (and `--danger-solid-hover`, `--danger-text`, `--danger-contrast` as needed) instead of `--destructive`. Remove `--destructive` and `--destructive-foreground` from the theme.
|
|
43
|
+
|
|
44
|
+
The roles are documented in [Styling & theming](https://github.com/tailor-platform/app-shell/blob/main/docs/concepts/styling-theming.md#brand--action). The decision record is `decisions/semantic-color-roles.md`.
|
|
45
|
+
|
|
25
46
|
## 1.17.0: status and alert tokens now resolve to the colour roles
|
|
26
47
|
|
|
27
48
|
**Applies to:** apps that use `bg-status-*`, `text-status-*` or `bg-alert-*` utilities, and apps whose theme overrides `--status-*`, `--alert-*` or `--destructive` to recolour `Badge`, `Alert`, `CsvImporter` or `MetricCard`.
|
|
@@ -32,7 +53,7 @@ Each entry states which versions are affected, what breaks, how to detect it, an
|
|
|
32
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`.
|
|
33
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.
|
|
34
55
|
|
|
35
|
-
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.
|
|
36
57
|
|
|
37
58
|
What to change:
|
|
38
59
|
|
|
@@ -179,7 +200,7 @@ body {
|
|
|
179
200
|
|
|
180
201
|
Toggle dark mode and confirm a real surface changes: inspect a `Card` and watch its computed `background-color` go from `rgb(255, 255, 255)` to `rgb(23, 23, 23)` on the default palette.
|
|
181
202
|
|
|
182
|
-
Reading the token directly also works — `getComputedStyle(document.documentElement).getPropertyValue("--card")` returns the winning declaration, so a stale copy shows up as its own value.
|
|
203
|
+
Reading the token directly also works — `getComputedStyle(document.documentElement).getPropertyValue("--card")` returns the winning declaration, so a stale copy shows up as its own value. Compare against the value the default palette resolves to: in dark mode `--card` returns `#171717`, the gray step it reads. (1.17.0 and earlier return `rgba(23, 23, 23, 1)`.) A copy in your own CSS shows up in the notation you wrote.
|
|
183
204
|
|
|
184
205
|
## 1.5.0: `loader` removed from file-based page definitions
|
|
185
206
|
|
|
@@ -209,7 +209,7 @@ saying so, and it is never a substitute for scrolling or paging.
|
|
|
209
209
|
|
|
210
210
|
Columns, left to right:
|
|
211
211
|
|
|
212
|
-
1. **Identity** — the item name, with the SKU beneath it in muted
|
|
212
|
+
1. **Identity** — the item name, with the SKU beneath it in muted `code-sm`. Two facts, one column.
|
|
213
213
|
2. **The record's own numbers** — quantity, unit of measure, unit price. Every document has these.
|
|
214
214
|
3. **Numbers from related records**, where they exist and the reader needs them (see below).
|
|
215
215
|
4. **Subtotal**, derived, last.
|
|
@@ -535,7 +535,7 @@ says "clickable" without a hover, and the resting underline is noise. Add
|
|
|
535
535
|
- **Internal** (another route in the app) → the `Link` exported by app-shell, with `to`. Never a bare `<a href>` for an in-app route.
|
|
536
536
|
- **External** (another system) → `<a target="_blank" rel="noopener noreferrer">` with a lucide `ExternalLink` at `size-3` after the label, so the new tab is signposted. There is no dedicated external-link component; this is the whole recipe.
|
|
537
537
|
- **Identifiers** keep their `font-mono` on top of the link style.
|
|
538
|
-
- **A `render` field owns its typography.** `DescriptionCard` applies none of its value styling to custom output, so a bare string from `render`
|
|
538
|
+
- **A `render` field owns its typography.** `DescriptionCard` applies none of its value styling to custom output, so a bare string from `render` does not match the values around it. Wrap it in `Text` with `variant="body-md"` and `text-foreground`, the same role the built-in values use — the reference implementation's `Value` helper.
|
|
539
539
|
|
|
540
540
|
## Patterns for the parts
|
|
541
541
|
|
|
@@ -571,6 +571,7 @@ import {
|
|
|
571
571
|
Link,
|
|
572
572
|
Table,
|
|
573
573
|
Tabs,
|
|
574
|
+
Text,
|
|
574
575
|
} from "@tailor-platform/app-shell";
|
|
575
576
|
import type { ActionPanelProps } from "@tailor-platform/app-shell";
|
|
576
577
|
import {
|
|
@@ -614,15 +615,17 @@ const formatDay = (value: string) =>
|
|
|
614
615
|
|
|
615
616
|
/**
|
|
616
617
|
* A `render` field owns its own presentation — DescriptionCard applies none of
|
|
617
|
-
* the built-in value typography to it — so
|
|
618
|
-
*
|
|
618
|
+
* the built-in value typography to it — so use the same role here (`body-md`)
|
|
619
|
+
* or the two dates do not match every other value on the card.
|
|
619
620
|
*/
|
|
620
621
|
const Value = ({ children }: { children: React.ReactNode }) => (
|
|
621
|
-
<
|
|
622
|
+
<Text variant="body-md" className="text-foreground" render={<span />}>
|
|
623
|
+
{children}
|
|
624
|
+
</Text>
|
|
622
625
|
);
|
|
623
626
|
|
|
624
627
|
/** House link: primary colour at rest, underline only on hover. */
|
|
625
|
-
const DOC_LINK = "font-mono text-
|
|
628
|
+
const DOC_LINK = "font-mono text-code-sm text-primary underline-offset-4 hover:underline";
|
|
626
629
|
|
|
627
630
|
export default function PurchaseOrderDetailPage({
|
|
628
631
|
order,
|
|
@@ -676,16 +679,18 @@ export default function PurchaseOrderDetailPage({
|
|
|
676
679
|
destructive: it's a problem to clear, not information to read.
|
|
677
680
|
Each hold is released on its own, beside its own reason. */}
|
|
678
681
|
{order.holds.length > 0 && (
|
|
679
|
-
<Card.Root className="border-
|
|
682
|
+
<Card.Root className="border-danger-solid">
|
|
680
683
|
<Card.Header title="On hold" />
|
|
681
684
|
<Card.Content className="flex flex-col gap-3">
|
|
682
685
|
{order.holds.map((hold) => (
|
|
683
686
|
<div key={hold.id} className="flex items-start justify-between gap-4">
|
|
684
687
|
<div>
|
|
685
|
-
<
|
|
686
|
-
|
|
688
|
+
<Text variant="label-md" className="text-foreground" render={<p />}>
|
|
689
|
+
{hold.reason}
|
|
690
|
+
</Text>
|
|
691
|
+
<Text variant="body-sm" className="text-muted-foreground">
|
|
687
692
|
Placed by {hold.placedBy} on {formatDay(hold.placedAt)}
|
|
688
|
-
</
|
|
693
|
+
</Text>
|
|
689
694
|
</div>
|
|
690
695
|
<Button
|
|
691
696
|
size="sm"
|
|
@@ -810,7 +815,9 @@ export default function PurchaseOrderDetailPage({
|
|
|
810
815
|
<Card.Header title="Line items" />
|
|
811
816
|
<Card.Content padding="none">
|
|
812
817
|
{order.lineItems.length === 0 ? (
|
|
813
|
-
<
|
|
818
|
+
<Text variant="body-md" className="px-6 text-muted-foreground">
|
|
819
|
+
No line items on this order.
|
|
820
|
+
</Text>
|
|
814
821
|
) : (
|
|
815
822
|
<Table.Root>
|
|
816
823
|
<Table.Header>
|
|
@@ -829,9 +836,9 @@ export default function PurchaseOrderDetailPage({
|
|
|
829
836
|
{/* Two facts, one column. */}
|
|
830
837
|
<Table.Cell>
|
|
831
838
|
<span className="block">{line.itemName}</span>
|
|
832
|
-
<
|
|
839
|
+
<Text variant="code-sm" className="block text-muted-foreground">
|
|
833
840
|
{line.sku}
|
|
834
|
-
</
|
|
841
|
+
</Text>
|
|
835
842
|
</Table.Cell>
|
|
836
843
|
<Table.Cell align="right" className="tabular-nums">
|
|
837
844
|
{line.qty}
|
|
@@ -844,9 +851,13 @@ export default function PurchaseOrderDetailPage({
|
|
|
844
851
|
<Table.Cell align="right" className="tabular-nums">
|
|
845
852
|
{line.received}
|
|
846
853
|
{line.received !== line.qty && (
|
|
847
|
-
<
|
|
854
|
+
<Text
|
|
855
|
+
variant="body-sm"
|
|
856
|
+
className="ml-1 text-muted-foreground"
|
|
857
|
+
render={<span />}
|
|
858
|
+
>
|
|
848
859
|
({line.qty - line.received})
|
|
849
|
-
</
|
|
860
|
+
</Text>
|
|
850
861
|
)}
|
|
851
862
|
</Table.Cell>
|
|
852
863
|
<Table.Cell align="right" className="tabular-nums">
|
|
@@ -877,9 +888,9 @@ export default function PurchaseOrderDetailPage({
|
|
|
877
888
|
<Card.Header title="Goods receipts" />
|
|
878
889
|
<Card.Content padding="none">
|
|
879
890
|
{order.goodsReceipts.length === 0 ? (
|
|
880
|
-
<
|
|
891
|
+
<Text variant="body-md" className="px-6 text-muted-foreground">
|
|
881
892
|
No goods receipts linked to this order.
|
|
882
|
-
</
|
|
893
|
+
</Text>
|
|
883
894
|
) : (
|
|
884
895
|
<Table.Root>
|
|
885
896
|
<Table.Header>
|
|
@@ -922,15 +933,15 @@ export default function PurchaseOrderDetailPage({
|
|
|
922
933
|
href={file.href}
|
|
923
934
|
target="_blank"
|
|
924
935
|
rel="noopener noreferrer"
|
|
925
|
-
className="inline-flex items-center gap-2 text-
|
|
936
|
+
className="inline-flex items-center gap-2 text-body-md text-primary underline-offset-4 hover:underline"
|
|
926
937
|
>
|
|
927
938
|
<FileText className="size-4 shrink-0" />
|
|
928
939
|
{file.name}
|
|
929
940
|
<ExternalLink className="size-3 shrink-0" />
|
|
930
941
|
</a>
|
|
931
|
-
<
|
|
942
|
+
<Text variant="body-sm" className="shrink-0 text-muted-foreground" render={<span />}>
|
|
932
943
|
{file.sizeLabel} · {formatDay(file.uploadedAt)}
|
|
933
|
-
</
|
|
944
|
+
</Text>
|
|
934
945
|
</div>
|
|
935
946
|
))}
|
|
936
947
|
</Card.Content>
|
|
@@ -1011,9 +1022,9 @@ export default function PurchaseOrderDetailPage({
|
|
|
1011
1022
|
<Table.Row key={line.id}>
|
|
1012
1023
|
<Table.Cell>
|
|
1013
1024
|
<span className="block">{line.account}</span>
|
|
1014
|
-
<
|
|
1025
|
+
<Text variant="code-sm" className="block text-muted-foreground">
|
|
1015
1026
|
{line.accountCode}
|
|
1016
|
-
</
|
|
1027
|
+
</Text>
|
|
1017
1028
|
</Table.Cell>
|
|
1018
1029
|
<Table.Cell align="right" className="tabular-nums">
|
|
1019
1030
|
{line.debit === null ? "—" : `${line.debit.toLocaleString()}`}
|
|
@@ -1047,14 +1058,14 @@ export default function PurchaseOrderDetailPage({
|
|
|
1047
1058
|
href={externalSource.href}
|
|
1048
1059
|
target="_blank"
|
|
1049
1060
|
rel="noopener noreferrer"
|
|
1050
|
-
className="inline-flex items-center gap-1 text-
|
|
1061
|
+
className="inline-flex items-center gap-1 text-body-md text-primary underline-offset-4 hover:underline"
|
|
1051
1062
|
>
|
|
1052
1063
|
{externalSource.recordLabel}
|
|
1053
1064
|
<ExternalLink className="size-3" />
|
|
1054
1065
|
</a>
|
|
1055
|
-
<
|
|
1066
|
+
<Text variant="body-sm" className="mt-1 text-muted-foreground">
|
|
1056
1067
|
Synced {externalSource.syncedAt.toLocaleDateString()}
|
|
1057
|
-
</
|
|
1068
|
+
</Text>
|
|
1058
1069
|
</Card.Content>
|
|
1059
1070
|
</Card.Root>
|
|
1060
1071
|
)}
|
|
@@ -147,7 +147,9 @@ placeholder.
|
|
|
147
147
|
render_ — a submit gate, a dependent placeholder, a live character count. That state is
|
|
148
148
|
load-bearing, not mirrored; see `form/composer`.
|
|
149
149
|
- **`Field.Control` is already a styled input.** Write `<Field.Control />`, not
|
|
150
|
-
`<Field.Control render={<Input />} />`.
|
|
150
|
+
`<Field.Control render={<Input />} />`. Every other control — `Select`, `Combobox`,
|
|
151
|
+
`Autocomplete`, `Checkbox`, `Textarea`, `DateField`, `DatePicker` — goes directly under
|
|
152
|
+
`Field.Root`. Passing one to `Field.Control`'s `render` draws a second frame around it.
|
|
151
153
|
- **Object items need `itemToStringValue`.** Items shaped `{ value, label }` submit `value`
|
|
152
154
|
automatically; any other object submits as a JSON string unless you supply it.
|
|
153
155
|
- **Server errors go through `Form`'s `errors` prop**, keyed by field `name` — not a toast or a
|
|
@@ -165,4 +167,5 @@ placeholder.
|
|
|
165
167
|
- Registering the create path as a separate top-level route — that unmounts the parent list
|
|
166
168
|
- Reaching for React Hook Form on a form this size — `onFormSubmit` already covers it
|
|
167
169
|
- Holding a `Select`/`Combobox` value in `useState` just to submit it — `Field.Root` already does
|
|
170
|
+
- Wrapping a `Select`/`Combobox` in `Field.Control` (`render={<Select />}`) — a double frame
|
|
168
171
|
- Surfacing API validation failures in a toast or banner instead of routing them via `errors`
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: i18n Labels
|
|
3
|
+
description: Decide UI language before implementation and keep display labels separate from stable data
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/patterns/i18n-labels.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# i18n Labels
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
Read this cross-cutting pattern **before implementing a screen**, alongside its page or layout pattern. It applies to single-language apps as well as apps with a language selector. For the complete API and fallback rules, see [defineI18nLabels](../api/define-i18n-labels.md).
|
|
13
|
+
|
|
14
|
+
## Decide the UI Language First
|
|
15
|
+
|
|
16
|
+
Record the UI language in the screen or app specification independently of the language used to write the document. An English specification, field name, or enum does not imply English UI copy.
|
|
17
|
+
|
|
18
|
+
For a Japanese-only app, explicitly pass `locale="ja"` (or `"ja-JP"`) to the root `AppShell`; do not rely on each developer's browser preferences. Keep the same policy across navigation, headings, forms, dialogs, accessibility labels, empty/loading/error states, validation, and notifications. Browser detection is appropriate only when that is the intended product behavior.
|
|
19
|
+
|
|
20
|
+
Use `defineI18nLabels` by default, including for single-language apps, to avoid a later screen-by-screen extraction. Its current API always requires an `en` base, even when the app selects `ja`. A project may explicitly opt out of catalog-based labels for a deliberately single-language app; record that decision and keep its chosen UI language consistent. AppShell does not force catalog adoption or generate missing translations.
|
|
21
|
+
|
|
22
|
+
## Key Granularity and Naming
|
|
23
|
+
|
|
24
|
+
- Use a **flat** map with semantic dotted keys, such as `"requests.list.title"`, `"requests.actions.approve"`, and `"requests.status.PENDING"`. Dots are a naming convention, not nested objects.
|
|
25
|
+
- Name the key after its purpose, not the current wording. Copy edits should not rename keys.
|
|
26
|
+
- One key represents one independently translated message. Translate a whole sentence using typed props rather than joining fragments, so each language controls word order and pluralization.
|
|
27
|
+
- Share genuinely equivalent operations such as `"actions.cancel"`; do not reuse a key merely because two English strings currently happen to match. A navigation group, page heading, and permission description can require different wording.
|
|
28
|
+
- Keep IDs, route segments, field names, enum values, and comparison values untranslated. Translate only their user-facing presentation. User-entered content and proper names are data, not catalog keys.
|
|
29
|
+
|
|
30
|
+
## Split by Feature, Not by Locale
|
|
31
|
+
|
|
32
|
+
Start with one `i18n-labels.ts` per feature, containing all of that feature's supported locales. Keep a small shared catalog for genuinely common copy. Export each factory result and its Hook; alias the Hook at import sites when a component needs several catalogs.
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
src/
|
|
36
|
+
i18n-labels.ts # shared actions and generic feedback
|
|
37
|
+
features/
|
|
38
|
+
requests/
|
|
39
|
+
i18n-labels.ts # request keys, with en + ja together
|
|
40
|
+
request-list.tsx
|
|
41
|
+
users/
|
|
42
|
+
i18n-labels.ts # user keys, with en + ja together
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Avoid a global catalog that every feature must edit, but do not create a separate catalog for every small component. Keep definitions at module scope, outside React render functions. Do not add a new wrapper, global translator, or dictionary-merging framework: each `defineI18nLabels` result already supplies its own typed resolvers.
|
|
46
|
+
|
|
47
|
+
## Example: Stable Status, Localized Display
|
|
48
|
+
|
|
49
|
+
The shared setup for the runnable example is a feature catalog in `features/requests/i18n-labels.ts`:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { defineI18nLabels } from "@tailor-platform/app-shell";
|
|
53
|
+
|
|
54
|
+
export const labels = defineI18nLabels({
|
|
55
|
+
en: {
|
|
56
|
+
"requests.list.title": "Requests",
|
|
57
|
+
"requests.actions.approve": "Approve",
|
|
58
|
+
"requests.status.PENDING": "Pending approval",
|
|
59
|
+
"requests.status.APPROVED": "Approved",
|
|
60
|
+
"requests.status.unknown": "Unknown status",
|
|
61
|
+
},
|
|
62
|
+
ja: {
|
|
63
|
+
"requests.list.title": "申請一覧",
|
|
64
|
+
"requests.actions.approve": "承認",
|
|
65
|
+
"requests.status.PENDING": "承認待ち",
|
|
66
|
+
"requests.status.APPROVED": "承認済み",
|
|
67
|
+
"requests.status.unknown": "不明な状態",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const useRequestT = labels.useT;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Import `useRequestT` from that module in the screen below. Here, `request` stands in for a validated server record; its ID and status remain unchanged in every locale.
|
|
75
|
+
|
|
76
|
+
<example-preview name="localized-request"></example-preview>
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
function LocalizedRequest() {
|
|
80
|
+
const t = useRequestT();
|
|
81
|
+
const request = { id: "REQ-1042", status: "PENDING" };
|
|
82
|
+
const canApprove = request.status === "PENDING";
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Layout>
|
|
86
|
+
<Layout.Header
|
|
87
|
+
title={t("requests.list.title")}
|
|
88
|
+
actions={[
|
|
89
|
+
<Button key="approve" disabled={!canApprove}>
|
|
90
|
+
{t("requests.actions.approve")}
|
|
91
|
+
</Button>,
|
|
92
|
+
]}
|
|
93
|
+
/>
|
|
94
|
+
<Layout.Column>
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<span>{request.id}</span>
|
|
97
|
+
<Badge variant={canApprove ? "warning" : "success"}>
|
|
98
|
+
{t.dynamic(`requests.status.${request.status}`, t("requests.status.unknown"))}
|
|
99
|
+
</Badge>
|
|
100
|
+
</div>
|
|
101
|
+
</Layout.Column>
|
|
102
|
+
</Layout>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The comparison uses `"PENDING"`, never `t("requests.status.PENDING")`. The runtime-key fallback is itself translated. For a known key, prefer `t(key)` to `t.dynamic()` so TypeScript checks it. For a function-valued label, use `t(key, props)`; `t.dynamic()` cannot supply props.
|
|
108
|
+
|
|
109
|
+
Navigation definitions use the same catalog's `labels.t("requests.list.title")`, not the component Hook. Pass it to `meta.title` or `meta.breadcrumbTitle` so AppShell resolves it with its current locale. Other component props that accept only strings use the Hook's `t()` instead.
|
|
110
|
+
|
|
111
|
+
## Server-originated Text
|
|
112
|
+
|
|
113
|
+
Keep **machine-readable meaning** separate from **display copy**:
|
|
114
|
+
|
|
115
|
+
| Server data | Frontend treatment |
|
|
116
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
117
|
+
| Stable enum or status code | Keep the value unchanged; map it to a display label |
|
|
118
|
+
| Domain error code plus structured parameters | Validate the response, select a known label by code, and pass parameters to a function-valued label |
|
|
119
|
+
| Unknown error code or unexpected response shape | Show a localized generic failure message; preserve a safe support identifier when available |
|
|
120
|
+
| User-authored text, identifiers, or proper names | Display as data, not as a label key |
|
|
121
|
+
| English prose with no usable code/parameters | It cannot be reliably localized or used for matching; show a localized fallback and treat structured errors as a backend prerequisite |
|
|
122
|
+
|
|
123
|
+
For example, an `APPROVAL_ROUTE_BLOCKED` code plus a separate `routeName` parameter can select a message like `"requests.errors.routeBlocked"` with `{ routeName }`. Do not find the route name by parsing an English sentence, translate arbitrary server text as a catalog key, or compare translated error messages to decide behavior.
|
|
124
|
+
|
|
125
|
+
If a transport collapses all domain codes into `PIPELINE_ERROR`, the frontend cannot recover the lost meaning. Fixing error propagation is a platform concern, not something this pattern or `defineI18nLabels` solves. Do not pretend that a generic code identifies a specific domain failure. Avoid showing raw technical error details to end users.
|
|
126
|
+
|
|
127
|
+
## Validation and E2E
|
|
128
|
+
|
|
129
|
+
- Compile catalogs and call sites: explicitly declared locales must supply the English base keys and compatible props. This checks structure, not the language or quality of the wording.
|
|
130
|
+
- Verify the intended locale in the browser, including navigation, dialogs, empty/error states, accessibility names, notifications, and unknown-code fallbacks. Do not assume successful type checks prove the absence of mixed-language UI.
|
|
131
|
+
- Test business state with stable codes. Test rendered labels in the selected locale; do not leave English display expectations in Japanese UI tests.
|
|
132
|
+
- Scope E2E locators by role and region, such as a heading in `main` or a button inside a named dialog. A translated phrase can legitimately occur in the sidebar, breadcrumb, and page; an unscoped text locator can become ambiguous.
|
|
133
|
+
- When adding a locale, review the app's catalogs **and** built-in component coverage. Custom catalog translations do not automatically translate AppShell's built-in labels.
|
|
134
|
+
|
|
135
|
+
## Related
|
|
136
|
+
|
|
137
|
+
- [defineI18nLabels](../api/define-i18n-labels.md) — `useT`, `t.dynamic`, function props, metadata, and fallback behavior
|
|
138
|
+
- [useResolvedLocale](../api/use-resolved-locale.md) — full locale for date and number formatting
|
|
139
|
+
- [Sidebar Navigation](../concepts/sidebar-navigation.md) — localized groups, sections, and item titles
|
|
@@ -68,6 +68,7 @@ Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, a
|
|
|
68
68
|
- **Tabs only above `DataTable`** — best when workflows are organized as obvious buckets
|
|
69
69
|
- **Tabs + chips** — when buckets are primary and finer filters help
|
|
70
70
|
- **Bulk actions** — `rowSelection.actions` on `useDataTable` turns `DataTable.Footer` into a bulk-action bar while rows are selected; see `interaction/multi-select`
|
|
71
|
+
- **Targeted inline entry** — opt a typed column into `edit` for quantities, text, choices, or dates. Observe raw drafts with optional `onValueChange`; validate and adopt confirmed editor outputs with required `onValueCommitted`. The page owns persistence, optimistic updates, error feedback, and rollback, whether saving each field immediately or collecting confirmed rows for Save/Cancel. Use `canEdit(row, { selected })` for per-row control; see [DataTable inline editing](../components/data-table.md#inline-editing) and [consumer-owned saving patterns](../components/data-table.md#consumer-owned-saving-patterns).
|
|
71
72
|
- **`Table` primitives** — small static lists without collection hooks
|
|
72
73
|
|
|
73
74
|
## Constraints
|
|
@@ -78,7 +79,7 @@ Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, a
|
|
|
78
79
|
- Handle every state: `DataTable` renders the loading skeleton and error row; always provide a **labelled empty state** (what the list is + how to add the first record) rather than a bare empty table
|
|
79
80
|
- Status Badge colors must use design system tokens (variant prop): the **primary** status column uses **filled** semantic variants; **secondary** status columns (delivery, billing) use **`outline-*`** (see `design-system.md` → Composition & emphasis rules)
|
|
80
81
|
- Bulk actions go in the footer bar from `rowSelection.actions`, which appears only while ≥1 row is selected — never a separate toolbar or floating bar
|
|
81
|
-
- Whole row is clickable via `onClickRow`; wrap the primary identifier cell in `<Link>` for keyboard/SR access. No per-row "View" / "Open" buttons
|
|
82
|
+
- Whole row is clickable via `onClickRow`; wrap the primary identifier cell in `<Link>` for keyboard/SR access. No per-row "View" / "Open" buttons. Editable cells stop row-click navigation so users can enter a value without opening detail.
|
|
82
83
|
- Per-row `Menu` (overflow `…`) is reserved for non-navigation actions (Archive, Duplicate, Delete)
|
|
83
84
|
- Wide lists (many columns / horizontal scroll): pin the column users scan by to the **left** — the record's identifier (invoice / order **reference**) or its **name** (customer, product) — so it stays anchored while the rest scrolls; optionally pin a single high-signal **status** or **total** to the **right**. Keep the pinned set small (≈1 left, at most 1 right) — over-pinning eats the scroll area. Let users override via `<DataTable.Toolbar columnSettings>` (show/hide + reorder + re-pin) with a stable `tableId` so each user's layout persists
|
|
84
85
|
|
|
@@ -89,5 +90,5 @@ Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, a
|
|
|
89
90
|
- Tabs that mutate only local UI state while pagination/filters assume the full server set
|
|
90
91
|
- Using `<table>` directly instead of `<DataTable>` for live collections
|
|
91
92
|
- Client-side filtering on 1000+ records without server-side support
|
|
92
|
-
-
|
|
93
|
+
- Treating inline entry as a full form or save framework: cross-field rules, business-error draft retention, and save/rollback orchestration belong to the page or a dedicated form; use `pattern/detail/*` or `pattern/form/modal` when that is the primary workflow
|
|
93
94
|
- Per-row "View" / "Open" buttons duplicating the row-click navigation
|