@tailor-platform/app-shell 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 | Tailwind |
168
- | -------------------------------------------- | -------------------------------- | -------------------------------------------- |
169
- | `--primary` / `--primary-foreground` | primary buttons, emphasis | `bg-primary` / `text-primary-foreground` |
170
- | `--secondary` / `--secondary-foreground` | secondary buttons, neutral chips | `bg-secondary` / `text-secondary-foreground` |
171
- | `--accent` / `--accent-foreground` | hover and selected nav states | `bg-accent` / `text-accent-foreground` |
172
- | `--destructive` / `--destructive-foreground` | destructive actions, errors | `bg-destructive` / `text-destructive` |
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
 
@@ -212,7 +221,7 @@ In the default palette, `text` on `surface` and `contrast` on `solid` and `solid
212
221
 
213
222
  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
223
 
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-*`. They no longer follow `--destructive`, which still drives `Button` and `text-destructive`.
224
+ `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
225
 
217
226
  #### Sidebar & charts
218
227
 
@@ -288,7 +297,7 @@ Pair a role with a color token from the Color section above. These pairings are
288
297
  | --------------------------------- | ---------------------------------------------- |
289
298
  | Page title (`Layout.Header`) | `text-heading-lg` |
290
299
  | Section heading | `text-heading-md` |
291
- | Card title (`div` or `span`) | `text-heading-md leading-none` |
300
+ | Card title | `text-heading-md leading-none` |
292
301
  | Body copy | `text-body-md` |
293
302
  | Secondary copy, descriptions | `text-body-md text-muted-foreground` |
294
303
  | Caption, timestamp, metadata | `text-body-sm text-muted-foreground` |
@@ -401,17 +410,24 @@ function TypographyRoles() {
401
410
  <span className="text-body-sm text-muted-foreground">Updated 2h ago</span>
402
411
  ```
403
412
 
413
+ The [`Text`](../components/text.md) component 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), and its `render` prop sets the element, such as the heading level:
414
+
415
+ ```tsx
416
+ <Text variant="heading-md" render={<h3 />}>Line items</Text>
417
+ <Text variant="body-md" className="text-muted-foreground">Description copy</Text>
418
+ ```
419
+
404
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 still use the stock utilities (`text-sm`, `font-medium`, and so on) today. They will move to the roles in later changes, so the two sets can appear side by side for now.
405
421
 
406
422
  **Rules**
407
423
 
408
424
  - **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.
425
+ - **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
426
  - **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
427
  - **Four modifiers may be combined with a role.** All are stock Tailwind utilities:
412
428
  - 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
429
  - Tabular figures: `tabular-nums`.
414
- - Emphasis on inline text: `font-medium` or `font-semibold`.
430
+ - Emphasis on inline text: `font-semibold`. Do not use `font-medium`; on a label role it makes no change.
415
431
  - Uppercase: `uppercase`.
416
432
  - **Do not combine a role with other `leading-*`, `tracking-*`, or `text-[Npx]` values.** If none of the roles fits, use the nearest role.
417
433
  - **12px is the minimum size.** Do not use smaller text.
@@ -420,12 +436,12 @@ On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's
420
436
 
421
437
  Always use `tabular-nums` for numbers that stack in a column — without it, digits jitter between rows.
422
438
 
423
- **Change a role.** Each role is four CSS variables named `--app-shell-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.
439
+ **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
440
 
425
441
  ```css
426
442
  :root {
427
- --app-shell-type-heading-lg-size: 1.75rem;
428
- --app-shell-type-heading-lg-line-height: calc(36 / 28);
443
+ --type-heading-lg-size: 1.75rem;
444
+ --type-heading-lg-line-height: calc(36 / 28);
429
445
  }
430
446
  ```
431
447
 
@@ -769,7 +785,7 @@ These are visual-composition rules every screen must follow, regardless of patte
769
785
 
770
786
  | Intent | Pick |
771
787
  | ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
772
- | Destructive action (delete, void) | `Button variant="destructive"`; `bg-destructive` on custom surfaces; confirm in a dialog at `shadow-lg` |
788
+ | Destructive action (delete, void) | `Button variant="destructive"`; `bg-danger-solid` on custom surface; confirm in a dialog at `shadow-lg` |
773
789
  | Non-blocking caution | `Badge variant="warning"`, or `bg-warning-surface text-warning-text` |
774
790
  | Confirmation / completed state | `Badge variant="success"`, or `bg-success-surface text-success-text` |
775
791
  | 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`. 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`.
@@ -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. Just compare against the authored notation: AppShell writes `rgba(23, 23, 23, 1)`, not `#171717`, and the computed value preserves that form.
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
 
@@ -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
- - Inline editable cells — use `pattern/detail/*` or `pattern/form/modal` instead
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