@nxgt/material 3.52.0 → 3.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +5 -4
  3. package/dist/{async-slice-BGtmF3K5.js → async-slice-DDSsekfm.js} +2 -2
  4. package/dist/{button-Ct0KRBsN.js → button-Ct8DSLVv.js} +1 -1
  5. package/dist/charts.js +1064 -932
  6. package/dist/{chip-Ca1SEd70.js → chip-BzoNsd-W.js} +2 -2
  7. package/dist/{code-editor-DtwRhK8B.js → code-editor-DsLQeiZc.js} +6 -6
  8. package/dist/code-editor.js +5 -5
  9. package/dist/{code-language-items-CEclGxhA.js → code-language-items-4zXlIEZ0.js} +2 -2
  10. package/dist/components/components.css +1 -5
  11. package/dist/components/ui/actions/action-card/action-card-tile.css +11 -2
  12. package/dist/components/ui/collections/data-table/data-table-items-record.css +27 -0
  13. package/dist/components/ui/collections/data-table/index.css +14 -0
  14. package/dist/{components-CzELzsOi.js → components-DZPMvc1w.js} +15976 -15821
  15. package/dist/components.js +26 -26
  16. package/dist/{dialog-BIy04Hpd.js → dialog-DwPmwmA3.js} +4 -4
  17. package/dist/dnd.js +1 -1
  18. package/dist/draw-animate.js +1 -1
  19. package/dist/{dropdown-menu-B0Kd8x3G.js → dropdown-menu-CDN6Nqx6.js} +1 -1
  20. package/dist/editor.js +9 -9
  21. package/dist/{empty-state-CKxORwFE.js → empty-state-Bb9W9did.js} +2 -2
  22. package/dist/{escape-owner-BGKjiglh.js → escape-owner-DI_lg2ID.js} +1 -1
  23. package/dist/{field-blur-Dz1zPaYK.js → field-blur-BcJH8H9B.js} +1 -1
  24. package/dist/{field-blur-context-1T6GkSNc.js → field-blur-context-Dg1nwWOV.js} +1 -1
  25. package/dist/hooks.js +2 -2
  26. package/dist/{hover-card-BwQ9IRxa.js → hover-card-Qse4gnop.js} +2 -2
  27. package/dist/i18n-provider-DlVPS58N.js +79 -0
  28. package/dist/i18n.js +5 -5
  29. package/dist/{icon-badge-D5Ns7ndZ.js → icon-badge-DVPBQOKe.js} +3 -3
  30. package/dist/{icon-button-VzdqY_Tv.js → icon-button-C3wDxsh6.js} +12 -12
  31. package/dist/lib/components/ui/actions/action-card/action-card-semantics.d.ts +17 -1
  32. package/dist/lib/components/ui/actions/action-card/action-card.d.ts +16 -2
  33. package/dist/lib/components/ui/actions/action-card/partials/action-card-text.d.ts +6 -2
  34. package/dist/lib/components/ui/actions/action-card/partials/action-card-title-button.d.ts +15 -1
  35. package/dist/lib/components/ui/actions/action-card/partials/action-card-trail.d.ts +2 -1
  36. package/dist/lib/components/ui/collections/data-grid/columns/column-codec.d.ts +3 -3
  37. package/dist/lib/components/ui/collections/data-grid/columns/column-edit.d.ts +5 -5
  38. package/dist/lib/components/ui/collections/data-grid/columns/row-meta.d.ts +22 -0
  39. package/dist/lib/components/ui/collections/data-grid/grid-meta.d.ts +10 -3
  40. package/dist/lib/components/ui/collections/data-table/data-table.d.ts +1 -1
  41. package/dist/lib/components/ui/collections/data-table/layout-types.d.ts +27 -0
  42. package/dist/lib/components/ui/collections/data-table/partials/layouts/data-table-item-context.d.ts +2 -0
  43. package/dist/lib/components/ui/collections/data-table/partials/layouts/data-table-item-row.d.ts +8 -0
  44. package/dist/lib/components/ui/collections/data-table/types.d.ts +1 -1
  45. package/dist/lib/components/ui/collections/filter/bar/filter-bar-classes.d.ts +7 -2
  46. package/dist/lib/components/ui/collections/server-table/partials/server-table-band-classes.d.ts +5 -0
  47. package/dist/lib/components/ui/display/list-tile/internal.d.ts +1 -0
  48. package/dist/lib/components/ui/display/list-tile/partials/list-tile-dense-text.d.ts +28 -0
  49. package/dist/lib/components/ui/feedback/toast/toast-text.d.ts +7 -0
  50. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  51. package/dist/lib/components/ui/forms/index.d.ts +2 -0
  52. package/dist/lib/components/ui/forms/use-fast-form.d.ts +25 -0
  53. package/dist/lib/components/ui/forms/use-focus-on-refusal.d.ts +14 -0
  54. package/dist/lib/components/ui/layout/app-shell/activity-content-aside.d.ts +3 -1
  55. package/dist/lib/components/ui/layout/app-shell/activity-content.d.ts +10 -1
  56. package/dist/lib/components/ui/selection/switcher/partials/switcher-trigger.d.ts +3 -1
  57. package/dist/lib/components/ui/selection/switcher/switcher.d.ts +1 -1
  58. package/dist/lib/components/ui/selection/switcher/types.d.ts +7 -0
  59. package/dist/lib/components/ui/widgets/native-charts/core/reference-text.d.ts +14 -6
  60. package/dist/lib/components/ui/widgets/native-charts/core/reference.d.ts +57 -10
  61. package/dist/lib/components/ui/widgets/native-charts/core/types.d.ts +13 -5
  62. package/dist/lib/components/ui/widgets/native-charts/hooks/chart-model-types.d.ts +3 -2
  63. package/dist/lib/components/ui/widgets/native-charts/index.d.ts +1 -1
  64. package/dist/lib/components/ui/widgets/native-charts/line-chart/line-chart-types.d.ts +6 -1
  65. package/dist/lib/components/ui/widgets/native-charts/line-chart/partials/reference-marks.d.ts +1 -1
  66. package/dist/lib/i18n/material-resources-registry.d.ts +21 -0
  67. package/dist/lib/i18n/resources/en/en.d.ts +4 -0
  68. package/dist/lib/i18n/resources/en/material.json.d.ts +5 -1
  69. package/dist/lib/i18n/resources/fr/fr.d.ts +4 -0
  70. package/dist/lib/i18n/resources/fr/material.json.d.ts +5 -1
  71. package/dist/lib/i18n/resources/resources.d.ts +8 -0
  72. package/dist/lib/i18n/translator.d.ts +1 -1
  73. package/dist/lib.js +1 -1
  74. package/dist/main.js +43 -43
  75. package/dist/map.js +8 -8
  76. package/dist/material.css +1 -1
  77. package/dist/pdf-reader.js +1 -1
  78. package/dist/print.js +2 -2
  79. package/dist/{public-D-J3nZFm.js → public-BoVE0ffh.js} +9 -9
  80. package/dist/{rich-code-block-Ig0ERAa2.js → rich-code-block-DAUM7Yh_.js} +5 -5
  81. package/dist/{sortable-provider-DfPkSN90.js → sortable-provider-BLXsGE_0.js} +2 -2
  82. package/dist/{spinner-KQb3YuR_.js → spinner-Rg4PnBPH.js} +1 -1
  83. package/dist/{stored-language-Cb9FMEwk.js → stored-language-B1lJMPbR.js} +10 -2
  84. package/dist/theme-picker.js +2 -2
  85. package/dist/{theme-toggle-BY-SpsFV.js → theme-toggle-jVdxF_mc.js} +1 -1
  86. package/dist/{toolbar-items-30w9rG5h.js → toolbar-items-BFY_hns5.js} +4 -4
  87. package/dist/{translate-material-BPl8TQLg.js → translate-material-q-JNxP1m.js} +2 -2
  88. package/dist/{translator-BnQXtpSZ.js → translator-BU1o3_l9.js} +1 -1
  89. package/dist/{use-chat-composer-BSKJRYMB.js → use-chat-composer-DGbDuIUa.js} +3758 -3716
  90. package/dist/{use-heading-spy-BgXA8uz6.js → use-heading-spy-CP63sAcS.js} +1 -1
  91. package/dist/{use-material-translation-ByM3gD9R.js → use-material-translation-nNF8fUXz.js} +2 -2
  92. package/dist/use-shape-morph-DXdy2X5g.js +35 -0
  93. package/docs/components/buttons-and-actions.md +30 -1
  94. package/docs/components/data-table-filter-and-lists.md +2 -2
  95. package/docs/components/navigation-and-layout.md +1 -0
  96. package/docs/components/rich-widgets.md +2 -1
  97. package/docs/components/selection-and-choice.md +20 -0
  98. package/docs/guides/app-shell.md +22 -0
  99. package/docs/guides/cards-and-tiles.md +40 -4
  100. package/docs/guides/charts.md +30 -2
  101. package/docs/guides/data-grid.md +42 -2
  102. package/docs/guides/forms.md +35 -0
  103. package/docs/guides/i18n.md +6 -2
  104. package/docs/roadmap.md +84 -684
  105. package/docs/troubleshooting.md +21 -0
  106. package/lib/components/components.css +1 -5
  107. package/lib/components/ui/actions/action-card/action-card-tile.css +11 -2
  108. package/lib/components/ui/collections/data-table/data-table-items-record.css +27 -0
  109. package/lib/components/ui/collections/data-table/index.css +14 -0
  110. package/package.json +1 -1
  111. package/dist/i18n-provider-BFNrM0Qu.js +0 -45
  112. package/dist/use-shape-morph-BKA_R5P5.js +0 -35
@@ -1,4 +1,4 @@
1
- import { r as e } from "./i18n-provider-BFNrM0Qu.js";
1
+ import { r as e } from "./i18n-provider-DlVPS58N.js";
2
2
  import { _ as t, g as n, h as r, n as i, v as a, y as o } from "./number.utils-BG_eEt5g.js";
3
3
  import { useCallback as s, useEffect as c, useMemo as l, useRef as u, useState as d } from "react";
4
4
  import { busySettler as f, busySettler as p, createSubmitGuard as m, createSubmitGuard as h } from "@nxgt/material-core/submit-guard";
@@ -1,5 +1,5 @@
1
- import { r as e } from "./i18n-provider-BFNrM0Qu.js";
2
- import { t } from "./translate-material-BPl8TQLg.js";
1
+ import { r as e } from "./i18n-provider-DlVPS58N.js";
2
+ import { t } from "./translate-material-q-JNxP1m.js";
3
3
  import { useCallback as n } from "react";
4
4
  //#region lib/i18n/use-material-translation.ts
5
5
  function r() {
@@ -0,0 +1,35 @@
1
+ import { useLayoutEffect as e, useRef as t } from "react";
2
+ import { morph as n, morphAt as r, toSvgPath as i } from "@nxgt/material-core/shapes";
3
+ //#region lib/components/ui/display/shape/use-shape-morph.ts
4
+ var a = .8, o = 14, s = o * Math.sqrt(1 - a * a);
5
+ function c(e) {
6
+ let t = e / 1e3;
7
+ return 1 - Math.exp(-.8 * o * t) * (Math.cos(s * t) + a * o / s * Math.sin(s * t));
8
+ }
9
+ var l = () => typeof window.matchMedia == "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
10
+ function u(a, o, s) {
11
+ let u = t(o);
12
+ e(() => {
13
+ let e = a.current, t = u.current;
14
+ if (!e || t === o || !s || l()) {
15
+ e && t !== o && e.setAttribute("d", i(o)), u.current = o;
16
+ return;
17
+ }
18
+ let d = n(t, o), f = i(o), p = 0, m = performance.now(), h = (t) => {
19
+ let n = t - m;
20
+ if (n >= 520) {
21
+ u.current = o, e.setAttribute("d", f);
22
+ return;
23
+ }
24
+ let a = r(d, c(n));
25
+ u.current = a, e.setAttribute("d", i(a)), p = requestAnimationFrame(h);
26
+ };
27
+ return e.setAttribute("d", i(d.from)), p = requestAnimationFrame(h), () => cancelAnimationFrame(p);
28
+ }, [
29
+ a,
30
+ o,
31
+ s
32
+ ]);
33
+ }
34
+ //#endregion
35
+ export { u as n, c as t };
@@ -1138,9 +1138,38 @@ keyboard focus of the title button, only while it is clipped (Escape dismisses
1138
1138
  it; moving the pointer across the line does not). Without `action` the DOM is as
1139
1139
  before. Nothing is added for a Vue port yet (React first).
1140
1140
 
1141
+ #### A link title, several actions and a footer
1142
+
1143
+ For a tile whose title goes to a page (a hospital bed, a room), pass
1144
+ `titleLink`: an element (a router `<Link to=... />`, an `<a href=...>`) that
1145
+ receives the title as its children. It is stretched over the card exactly like
1146
+ the title button and named after the title, the status and the meta. The root
1147
+ stays a plain `div`, and `onClick`, `active` and `role` are not read. `action`
1148
+ takes several controls (two menus in a fragment); `footer` (`tile` only) is a
1149
+ last row for secondary links, such as the occupant or a device, above the
1150
+ stretched link and outside its accessible name. Nothing nests, so
1151
+ `nested-interactive` stays clean; keep each footer target 44px (`min-h-11`);
1152
+ the footer wraps inside the tile at 360px.
1153
+
1154
+ ```tsx
1155
+ <ActionCard
1156
+ variant="tile"
1157
+ title="Bed 12A"
1158
+ titleLink={<Link to="/beds/12a" />}
1159
+ status={<Badge variant="soft" tone="warning">Occupied</Badge>}
1160
+ action={<><StatusMenu /><StayMenu /></>}
1161
+ footer={<Link to="/patients/9">DOE Jane</Link>}
1162
+ />;
1163
+ ```
1164
+
1165
+ | Prop | Type | Effect |
1166
+ | --- | --- | --- |
1167
+ | `titleLink` | `ReactElement` | the title becomes this link (its `className` is kept); the card is one link tab stop, then the actions, then the footer links |
1168
+ | `footer` | `ReactNode` | `tile` only; secondary links under the status and meta |
1169
+
1141
1170
  The root is `data-slot="action-card"` (it was the `Card` primitive's `data-slot="card"`): style it with `className`, not through `Card` classes or a `[data-slot=card]` selector.
1142
1171
 
1143
- A whole board built on it, with sections and an occupancy table, is the `Guides/Recipes/Status board` recipe. **Storybook:** `Buttons & actions/ActionCard`, `Buttons & actions/ActionCard/Look`, `Buttons & actions/ActionCard/Tile`, `Buttons & actions/ActionCard/Checks` · **Vue:** `ActionCard`, with the earlier look and no button role yet (#1195)
1172
+ A whole board built on it, with sections (each its own h2 and free count) and, with `linked`, link titles and an occupant footer, and an occupancy table, is the `Guides/Recipes/Status board` recipe. **Storybook:** `Buttons & actions/ActionCard`, `Buttons & actions/ActionCard/Look`, `Buttons & actions/ActionCard/Tile`, `Buttons & actions/ActionCard/Link tile`, `Buttons & actions/ActionCard/Checks` · **Vue:** `ActionCard`, with the earlier look and no button role yet (#1195)
1144
1173
 
1145
1174
  ---
1146
1175
 
@@ -95,8 +95,8 @@ table enables, for typing your own column defs.
95
95
  | `data` | `T[]` | — | rows; sorting, filtering and pagination run client-side |
96
96
  | `layout` | `'table' \| 'cards' \| 'tiles' \| { phone, wide }` | `'table'` | `'cards'` lays the rows out as a list of cards, `'tiles'` as one column in a single surface ([Cards and tiles](../guides/cards-and-tiles.md)); both ignore `virtual`, `renderExpandedRow` and `showViewOptions` |
97
97
  | `cardDensity` | `'comfortable' \| 'compact'` | `'comfortable'` | `compact` fits cards of at least `--nxgt-cards-min` (15rem), up to four a row, with a tighter padding; `layout="cards"` only ([Cards and tiles](../guides/cards-and-tiles.md#columns)) |
98
- | `renderCard` | `(row, { Title, selected, locked, reason }) => ReactNode` | — | a card's content, required with `layout="cards"`; `Title` is the card's one link and its name |
99
- | `renderTile` | `(row, { Title, selected, locked, reason }) => ReactNode` | — | a tile's content, required with `layout="tiles"`; same `Title` as a card |
98
+ | `renderCard` | `(row, { Title, Row, selected, locked, reason }) => ReactNode` | — | a card's content, required with `layout="cards"`; `Title` is the card's one link and its name; `Row` (the compact record row) is also passed, laid out inside the card's own padding, with no height of its own |
99
+ | `renderTile` | `(row, { Title, Row, selected, locked, reason }) => ReactNode` | — | a tile's content, required with `layout="tiles"`; same `Title` as a card, or `Row`, the compact record row (title, subtitle, chips, trailing facts; see the cards and tiles guide) |
100
100
  | `renderExpandedRow` | `(row) => ReactNode` | — | makes rows expandable; pair with the `expand` column |
101
101
  | `showViewOptions` | `boolean` | — | column visibility menu above the table |
102
102
  | `actions` | `(table, controls) => ReactNode` | — | toolbar content with access to the table (selected rows…); disabled while `loading`. `controls.focusTable()` moves the focus to the table |
@@ -85,6 +85,7 @@ phone's menu sheet pad clear of `env(safe-area-inset-*)`, as classes; see the
85
85
  | `titleAs` | `'h1'` … `'h6'` \| `'div'` | `'h1'` | `ActivityContent`: the title's heading level |
86
86
  | `inset` | `boolean` | `false` | `ActivityContent`: pads the content 1rem on each side. Off, it lines up with the app bar and the cards |
87
87
  | `width` | `'full' \| 'form' \| 'reading'` | `'full'` | `ActivityContent`: caps the main column at 48rem (`'form'`) or 72ch (`'reading'`), the side panel right beside it |
88
+ | `notice` | `ReactNode` | — | `ActivityContent`: a notice above the column and the side panel, at their width, taking its own height; first on a phone |
88
89
  | `aside`, `asideLabel` / `asideLabelledBy` | `ReactNode`, `string` | — | `ActivityContent`: a side panel, a named `complementary` landmark, beside the content from 1280px (CSS only), stacked below; sticky under the app bar |
89
90
  | `asideSide` / `asideWidth` / `asideFrom` / `asideSticky` / `asideStack` | `'start' \| 'end'` / `'sm' \| 'md' \| 'lg' \| string` / `'lg' \| 'xl' \| '2xl'` / `boolean` / `'before' \| 'after'` | `'end'` / `'md'` / `'xl'` / `true` / `'after'` | `ActivityContent`: the panel's side, width (16, 20, 24rem), breakpoint, stickiness and stacking order (see the [app shell guide](../guides/app-shell.md#a-side-panel-and-a-readable-width)) |
90
91
  | `align` | `'start' \| 'center'` | `'start'` | `ActivityLayout`, `AppSidebar`: the collapsed rail's entries sit right under the brand, or `'center'` for centred in the height below it |
@@ -382,7 +382,8 @@ import { LineChart } from '@nxgt/material/charts';
382
382
  ```
383
383
 
384
384
  A line chart can be read against a reference: `bands` shade a range of the
385
- value axis (a normal range), `thresholds` draw labelled lines and `pointTone`
385
+ value axis (a normal range, constant or stepping along x with a row field or
386
+ function), `thresholds` draw labelled lines and `pointTone`
386
387
  rings the values that need a word, which the tooltip, the summary and the
387
388
  "View data" table repeat
388
389
  ([guide](../guides/charts.md#reading-values-against-a-reference)).
@@ -1294,6 +1294,25 @@ const RouterLink: AppShellLinkComponent = ({ href, ...props }) => <Link to={href
1294
1294
  />;
1295
1295
  ```
1296
1296
 
1297
+ ### Compact trigger for an app bar
1298
+
1299
+ In the app bar's `titleAfter` slot the title already names the chosen record, so the full trigger repeats it. `size="compact"` renders an icon-only round button instead (32px, a 44x44 box on a coarse pointer). It is named by the label and the chosen option (`Project: Quarterly plan`, read to screen readers), the tooltip shows the option, and the panel (menu, popover or sheet) and the keyboard are unchanged. `size="default"` is the full trigger. Compact works with both variants; to compact only below `md`, pick the size with your own breakpoint hook (`size={isNarrow ? 'compact' : 'default'}`).
1300
+
1301
+ ```tsx
1302
+ export const handle = {
1303
+ title: 'Quarterly plan',
1304
+ titleAfter: (
1305
+ <Switcher
1306
+ size="compact"
1307
+ label="Project"
1308
+ value="a"
1309
+ options={projects}
1310
+ getHref={(item) => `/projects/${item.id}`}
1311
+ />
1312
+ ),
1313
+ };
1314
+ ```
1315
+
1297
1316
  ### Expressive
1298
1317
 
1299
1318
  `variant="expressive"` is a pill that opens a larger panel (always with a search). `recent` lists up to 3 option ids shown as tiles above the groups while nothing is typed; `badge` draws a soft badge after the title. On a phone the sheet is full height with a close button.
@@ -1328,6 +1347,7 @@ To put a Switcher at the start of a page header, pass it as [EntityHeader](data-
1328
1347
  | `onValueChange` | `(value: string) => void` | — | |
1329
1348
  | `label` | `ReactNode` | — | what is switched: names the trigger, the panel and the list |
1330
1349
  | `variant` | `'default' \| 'expressive'` | `'default'` | tile trigger and menu, or pill and large panel |
1350
+ | `size` | `'default' \| 'compact'` | `'default'` | full trigger, or an icon-only round button for the app bar's `titleAfter` slot |
1331
1351
  | `searchable` | `boolean` | from 8 options (always, expressive) | the search field of the panel; `true` also replaces the menu with the panel |
1332
1352
  | `recent` | `string[]` | — | up to 3 option ids shown as tiles (expressive) |
1333
1353
  | `getHref` | `(item) => string \| undefined` | — | link mode; falls back to `item.href` |
@@ -925,6 +925,7 @@ export function BoardShell({ items }: { items: MenuItem[] }) {
925
925
  | `titleAs` | `'h1'` … `'h6'` \| `'div'` | `'h1'` | The title's element: the screen's heading, `'h2'` under a heading of yours, `'div'` for none |
926
926
  | `inset` | `boolean` | `false` | Pads the content 1rem on each side. Off, it lines up with the app bar and the cards |
927
927
  | `width` | `'full'` \| `'form'` \| `'reading'` | `'full'` | Caps the main column: `'form'` 48rem, `'reading'` 72ch. The header card keeps the full width |
928
+ | `notice` | `ReactNode` | — | A notice (an `Alert`, a `Banner`) above the main column and the side panel, as wide as the two together; it follows the header card and stacks first on a phone |
928
929
  | `aside` | `ReactNode` | — | A side panel: beside the content from `asideFrom`, stacked below it |
929
930
  | `asideLabel` / `asideLabelledBy` | `string` | — | The panel's accessible name (`aria-label`), or the id of a heading inside it (`aria-labelledby`). One of them is needed |
930
931
  | `asideSide` | `'start'` \| `'end'` | `'end'` | Which side of the content the panel sits on, wide |
@@ -973,6 +974,20 @@ decides the layout.
973
974
  </ActivityContent>
974
975
  ```
975
976
 
977
+ ```tsx
978
+ import { ActivityContent, Alert } from '@nxgt/material/components';
979
+
980
+ <ActivityContent
981
+ title="New visit"
982
+ width="form"
983
+ notice={<Alert variant="warning" title="Confirm your address" />}
984
+ aside={<DuplicateHints />}
985
+ asideLabel="Possible duplicates"
986
+ >
987
+ <VisitForm />
988
+ </ActivityContent>
989
+ ```
990
+
976
991
  - **Landmark.** The panel is an `<aside>`, a `complementary` landmark: name it
977
992
  with `asideLabel`, or with `asideLabelledBy` (the id of its heading). Its
978
993
  content is the panel's own; give it one `h2` when it has a heading.
@@ -987,6 +1002,13 @@ decides the layout.
987
1002
  the main column and the pair stays start-aligned: at 1920px the panel is 1rem
988
1003
  from the fields, not at the window's far edge. The header card above keeps
989
1004
  the full width. `width` also works without a panel.
1005
+ - **A notice above both.** `notice` renders an `Alert` or a `Banner` right
1006
+ under the header card, across the main column and the panel (with
1007
+ `width="form"`, exactly their width), and it takes its own height in the
1008
+ layout, so a page that centres its content needs no guessed offset. On a
1009
+ phone it comes first, then the column, then the panel (`asideStack`
1010
+ unchanged). Without `notice` the layout is unchanged. It works without a
1011
+ panel too.
990
1012
  - **The breakpoint is the window's.** `asideFrom` follows the viewport, not the
991
1013
  content column, which the sidebar and the gutters narrow a little: pick `'lg'`
992
1014
  for a panel on a smaller window, `'2xl'` to keep it stacked on all but the
@@ -48,6 +48,41 @@ only the surface and the content differ. Let the text and the meta wrap
48
48
  `renderTile` replaces `renderCard` there (`renderCard` is not read, and the
49
49
  reverse), and `item.Title` is rendered once per tile.
50
50
 
51
+ ### A compact record row in a tile
52
+
53
+ For a list of records (a name, a note, a few tags), `item.Row` lays the tile
54
+ out as the dense row of `ListTile` (`size="sm"` with `chips`), so you do not
55
+ hand-lay two lines. The tile keeps its checkbox, its link and its states; the
56
+ title is the one link and keeps the heading level you give it with `as`.
57
+
58
+ ```tsx
59
+ <ServerTable
60
+ layout={{ phone: 'tiles', wide: 'table' }}
61
+ getRowHref={(person) => `/people/${person.id}`}
62
+ renderTile={(person, { Row }) => (
63
+ <Row
64
+ as="h2"
65
+ title={person.name}
66
+ subtitle={person.role}
67
+ chips={<Chip size="sm" label={person.status} />}
68
+ trailing={<span>{person.room}</span>}
69
+ />
70
+ )}
71
+ // ...
72
+ />;
73
+ ```
74
+
75
+ `Row` takes `title`, `as` (`h2`, `h3`, `h4` or `span`, default `span`),
76
+ `subtitle`, `chips`, `trailing`, `leading` and `chevron`. It replaces
77
+ `Title`: render one or the other, once. The row measures its own width: the
78
+ subtitle sits on the title's line from 28rem and the chips after the text from
79
+ 36rem, and below that they stack under the title. A one-line row is 36px tall
80
+ under a mouse and 44px on a coarse pointer, against 72px for a free tile.
81
+ `chips` and `trailing` are decoration, never buttons; a link or button row
82
+ (`getRowHref` or `onRowClick`) gets a decorative chevron (`chevron={false}`
83
+ removes it). `renderCard` is handed `Row` too: the same row inside the card's
84
+ own padding and border, with no height of its own.
85
+
51
86
  ## One layout on a phone, another from 768px
52
87
 
53
88
  `layout` also takes `{ phone, wide }`: the layout below 768px and the one from
@@ -94,13 +129,14 @@ only the title is interactive.
94
129
  On a coarse pointer the title link also keeps a 44px box of its own, clear of
95
130
  the row-select checkbox, however short the title.
96
131
 
97
- `item` also carries `selected`, `locked` and `reason` (a row `enableRowSelection`
98
- leaves out, and `rowSelectionDisabledReason`'s answer).
132
+ `item` also carries `Row` (the compact record row above), `selected`,
133
+ `locked` and `reason` (a row `enableRowSelection` leaves out, and
134
+ `rowSelectionDisabledReason`'s answer).
99
135
 
100
136
  The types, from `@nxgt/material/components`: `DataTableItem` (the `item`
101
137
  argument), `DataTableLayout` (`'table' | 'cards' | 'tiles'`),
102
- `DataTableResponsiveLayout` (`{ phone, wide }`), `DataTableLayoutProp` (what
103
- `layout` takes: either) and `DataTableItemTitleElement` (`Title`'s `as`).
138
+ `DataTableItemRowProps` (`Row`'s props), `DataTableResponsiveLayout`
139
+ (`{ phone, wide }`), `DataTableLayoutProp` (what `layout` takes: either) and `DataTableItemTitleElement` (`Title`'s `as`).
104
140
 
105
141
  ## In a `ServerTable`
106
142
 
@@ -240,11 +240,39 @@ const toneOf = (point: ChartPoint) =>
240
240
 
241
241
  | Prop | Effect |
242
242
  | --- | --- |
243
- | `bands` | `{ from, to, label, tone? }[]`: a wash across the plot behind the lines, with its `label` inside it at the left. `from` and `to` may come in either order. |
244
- | `thresholds` | `{ value, label, tone?, dash? }[]`: a horizontal line behind the series with its `label` above it at the right end; dashed unless `dash: false`. |
243
+ | `bands` | `{ from, to, label, tone? }[]`: a wash across the plot behind the lines, with its `label` inside it at the left. `from` and `to` may come in either order, and are a number, a row field or a function of the row (a stepped band; typed `ChartRowValue`). |
244
+ | `thresholds` | `{ value, label, tone?, dash? }[]` (`value`: a number, a row field or a function of the row): a horizontal line behind the series with its `label` above it at the right end; dashed unless `dash: false`. |
245
245
  | `pointTone` | `(point: ChartPoint, series: ChartSeries) => Tone \| undefined`: the tone of one value. It is given the point `onPointClick` gets, so `point.index` is the row of the full `data` even when the chart is zoomed. |
246
246
  | `toneLabels` | `{ [tone]: word }`: the word of each tone, in your language and domain. Unset, the library's neutral word ("Error", translated by `material.charts.tone.*`); an app tone you declared falls back to its own name. |
247
247
 
248
+ **A band or a line that changes along x.** When each row carries its own
249
+ range (a normal range copied with a measurement, which changes with an age
250
+ band or a catalogue edit), give `from` and `to` as the name of a row field, or
251
+ as a function of the row; a threshold's `value` takes the same. The band then
252
+ steps along x: each row owns the stretch up to the midpoint to its neighbours,
253
+ rows with the same range merge into one block, and a row that gives no number
254
+ leaves a gap. A number keeps the constant band, and so does a field or function
255
+ that gives every row the same range.
256
+
257
+ ```tsx
258
+ // rows: { day: 'Day 1', reading: 48, low: 40, high: 60, limit: 70 }
259
+ <LineChart
260
+ title="Daily readings"
261
+ data={rows}
262
+ x="day"
263
+ series={[{ key: 'reading', label: 'Reading' }]}
264
+ bands={[{ from: 'low', to: 'high', label: 'Normal range', tone: 'success' }]}
265
+ thresholds={[{ value: (row) => row.limit as number, label: 'Upper limit', tone: 'error' }]}
266
+ />
267
+ ```
268
+
269
+ The text names every range with the x labels it covers, in the summary and
270
+ under "View data" (`Normal range: 40 to 60 (Day 1 to Day 6), 45 to 70 (Day 7 to
271
+ Day 12)`), and the count of values outside a band uses each row's own range
272
+ (a row with no range is not counted). The axis grows to the whole span, a
273
+ label is drawn once, in the widest stretch (a threshold's at the end of the
274
+ last one), and over a `zoom` window the blocks follow the window's rows.
275
+
248
276
  **The value axis holds them.** Unless `yAxis.min` and `yAxis.max` pin it, the
249
277
  axis grows to include every band end and threshold, so none is out of view.
250
278
  With a pinned axis a band is clipped to the plot and a threshold outside it is
@@ -15,6 +15,7 @@ import { DataGrid, type DataGridHandle } from '@nxgt/material/components';
15
15
  ## Contents
16
16
 
17
17
  - [Columns and `meta`](#columns-and-meta)
18
+ - [One column, several kinds of row](#one-column-several-kinds-of-row)
18
19
  - [Locked and read-only rows](#locked-and-read-only-rows)
19
20
  - [Saving](#saving)
20
21
  - [Versions and conflicts](#versions-and-conflicts)
@@ -42,9 +43,10 @@ Columns are DataTable's TanStack v9 column definitions. A column with no
42
43
  | `frozen` | Keeps a leading column at the inline start while the rest scrolls |
43
44
  | `lockPosition` | `'start'` or `'end'`: the column keeps its edge when columns are reordered ([columns](#columns-resize-hide-and-reorder)) |
44
45
  | `editor` | `'text'`, `'number'`, `'select'`, `'date'`, `'checkbox'`, or your own component ([custom editor](#a-custom-editor)) |
45
- | `number` | `{ min, max, decimals }` for the number editor; `min` and `max` may be functions of the row. A value outside them, or with too many decimals, is an error: it is never rounded or clamped |
46
+ | `rowEditor(row)` | The built-in editor (`'text'`, `'number'`, `'select'`, `'date'`, `'checkbox'`) a row's cell has, for a column that mixes kinds ([one column, several kinds of row](#one-column-several-kinds-of-row)). It wins over `editor` |
47
+ | `number` | `{ min, max, decimals }` for the number editor; `min`, `max` and `decimals` may be functions of the row. A value outside them, or with too many decimals, is an error: it is never rounded or clamped |
46
48
  | `codes` | Symbolic values the number editor also takes ("abs", "exempt"), with the label each language shows and the aliases it accepts |
47
- | `options` | The select editor's `{ value, label }` list. The cell shows and copies the label; a typed or pasted text matches a label or a value, in any case and without accents |
49
+ | `options` | The select editor's `{ value, label }` list, or a function of the row returning it. The cell shows and copies the label; a typed or pasted text matches a label or a value, in any case and without accents |
48
50
  | `date` | `{ min, max }` as ISO dates (`'2026-09-01'`). The value is an ISO date, shown and read in the page's locale |
49
51
  | `parse(text, ctx)` | Text to value, replacing the editor's own: `{ value }` or `{ error }`. `ctx` has `lang`, `locale` and `row`. `{ value: null }` clears the cell, whatever the column's value type |
50
52
  | `format(value, ctx)` | Value to text, shown in the cell and in the editor |
@@ -129,6 +131,44 @@ answered by then refuses the paste with the same message. Set `checkTimeout` to
129
131
  <DataGrid checkTimeout={5000} /* … */ />
130
132
  ```
131
133
 
134
+ ## One column, several kinds of row
135
+
136
+ A result-entry sheet has one « Result » column whose rows differ: a quantity
137
+ (a number, held to that row's own min, max and decimals), a coded value (a
138
+ pick among that row's own options) and a free text. Give the column
139
+ `rowEditor`, and let `number` and `options` read the row:
140
+
141
+ ```tsx
142
+ type Result = {
143
+ id: string;
144
+ kind: 'quantity' | 'coded' | 'text';
145
+ value: number | string | null;
146
+ min?: number;
147
+ max?: number;
148
+ decimals?: number;
149
+ options?: { value: string; label: string }[];
150
+ };
151
+
152
+ helper.accessor('value', {
153
+ id: 'result',
154
+ header: 'Result',
155
+ meta: {
156
+ rowEditor: (row) =>
157
+ row.kind === 'quantity' ? 'number' : row.kind === 'coded' ? 'select' : 'text',
158
+ number: { min: (r) => r.min ?? 0, max: (r) => r.max ?? 0, decimals: (r) => r.decimals ?? 0 },
159
+ options: (row) => row.options ?? [],
160
+ },
161
+ });
162
+ ```
163
+
164
+ Everything reads the column for the row: typing and the editors, a pasted or
165
+ filled text, Delete, undo, copy, the phone's row panel and the conflict
166
+ panel. `rowEditor` returns a built-in name only; a custom component stays
167
+ `editor`'s. The column's `className`, `format`, `parse`, `validate` and
168
+ `schema` stay column-wide. A column with `rowEditor` copies on fill and never
169
+ continues a series, and its totals use the `decimals` given as a number. A row
170
+ that must not be edited at all is `readOnly` or `locked` as usual.
171
+
132
172
  ## Locked and read-only rows
133
173
 
134
174
  `rowReadOnly(row)` and `rowLocked(row)` return `false`, `true` or
@@ -783,6 +783,41 @@ form.setFocus('email'); // or without an error
783
783
  - If focus does not move, see
784
784
  [`setError(…, { shouldFocus: true })` does nothing](../troubleshooting.md#seterror--shouldfocus-true--or-a-failed-submit-does-not-focus-the-field).
785
785
 
786
+ ## A form that is quick to fill
787
+
788
+ `useFastForm` and `useFocusOnRefusal` (`@nxgt/material/components`) are the
789
+ two habits of a record form, in one place. `useFastForm(props, first, active?)`
790
+ is `useForm` with `mode: 'onTouched'` (an error shows once the user leaves the
791
+ field, then follows each change; a `mode` in `props` wins) that also focuses
792
+ the `first` field when the form opens, so typing starts at once. It waits a
793
+ frame and retries for about half a second, so a dialog's focus trap has
794
+ settled; it stops as soon as the focus moved. `active` (default `true`) is for
795
+ a hook that stays mounted while its dialog is closed: it focuses each time
796
+ `active` turns true.
797
+
798
+ `useFocusOnRefusal<T>(message, busy)` returns a ref for the submit control: once
799
+ a refusal shows (`message` set) and nothing is running (`busy` false), the
800
+ control takes the focus back, so it never falls to the page after the button
801
+ was disabled during the request.
802
+
803
+ ```tsx
804
+ import { Button, TextFormField, useFastForm, useFocusOnRefusal } from '@nxgt/material/components';
805
+
806
+ export function SignUp({ refusal, busy, save }: Props) {
807
+ const form = useFastForm<{ email: string }>({ defaultValues: { email: '' } }, 'email');
808
+ const submit = useFocusOnRefusal<HTMLButtonElement>(refusal, busy);
809
+ return (
810
+ <form onSubmit={form.handleSubmit(save)}>
811
+ <TextFormField control={form.control} name="email" label="Email" />
812
+ <Button ref={submit} type="submit" loading={busy}>Sign up</Button>
813
+ </form>
814
+ );
815
+ }
816
+ ```
817
+
818
+ Storybook: `Guides/Forms/useFastForm`. If the focus does not move, see
819
+ [`useFastForm` does not focus the first field](../troubleshooting.md#usefastform-does-not-focus-the-first-field).
820
+
786
821
  ## Server errors
787
822
 
788
823
  Map an API's field errors onto the form with `setError`, and put anything that
@@ -534,8 +534,12 @@ is visible only through **your** translator, and defining `material.command.titl
534
534
  does not change the title `CommandDialog` renders. Pass a stable object
535
535
  (module scope), not one built in a render. A component's `labels` prop still
536
536
  wins over both. Outside a component, `translateMaterial` takes the merged
537
- resources as a fifth argument and `zodLocaleError` takes `translate`; the
538
- toast presets and the toast close label still read the package's bundle.
537
+ resources as a fifth argument and `zodLocaleError` takes `translate`. Toasts
538
+ follow `resources` too: `toast()`, its presets and the close label run outside
539
+ React, so the mounted `I18nProvider` hands them its `resources` and language
540
+ (with several providers, the last one mounted wins). They follow that
541
+ provider's language as well as its resources, which can differ from the
542
+ language of an inner subtree.
539
543
 
540
544
  Each time `mergeResources` runs outside production, overlapping leaves are
541
545
  reported, one line per language: