@nxgt/material 3.51.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 (133) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +5 -4
  3. package/dist/{async-slice-BGtmF3K5.js → async-slice-DDSsekfm.js} +2 -2
  4. package/dist/{button-BWcr2b49.js → button-Ct8DSLVv.js} +1 -1
  5. package/dist/charts.js +1064 -932
  6. package/dist/{chip-CmLW6ZrT.js → chip-BzoNsd-W.js} +2 -2
  7. package/dist/{code-editor-BKMaeIF6.js → code-editor-DsLQeiZc.js} +6 -6
  8. package/dist/code-editor.js +5 -5
  9. package/dist/{code-language-items-Cs1Pwd1h.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-CuwU9Sux.js → components-DZPMvc1w.js} +16110 -15921
  15. package/dist/components.js +26 -26
  16. package/dist/{dialog-TdT_5iUc.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-BozV6Ikw.js → dropdown-menu-CDN6Nqx6.js} +1 -1
  20. package/dist/editor.js +9 -9
  21. package/dist/{empty-state-gzBPimAo.js → empty-state-Bb9W9did.js} +2 -2
  22. package/dist/{escape-owner-Bu-aDEEO.js → escape-owner-DI_lg2ID.js} +1 -1
  23. package/dist/{field-blur-47zmuDkH.js → field-blur-BcJH8H9B.js} +1 -1
  24. package/dist/{field-blur-context-6McYOnM8.js → field-blur-context-Dg1nwWOV.js} +1 -1
  25. package/dist/hooks.js +2 -2
  26. package/dist/{hover-card-zHdZJpBG.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-CRFGIQPX.js → icon-badge-DVPBQOKe.js} +3 -3
  30. package/dist/{icon-button-CtG3Snfm.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/filtered-server-table.d.ts +1 -0
  47. package/dist/lib/components/ui/collections/server-table/partials/server-table-band-classes.d.ts +5 -0
  48. package/dist/lib/components/ui/collections/server-table/partials/server-table-band-view.d.ts +18 -0
  49. package/dist/lib/components/ui/collections/server-table/partials/use-server-table-list-root.d.ts +10 -0
  50. package/dist/lib/components/ui/collections/server-table/partials/use-server-table-phone-row.d.ts +3 -2
  51. package/dist/lib/components/ui/display/change-history/types.d.ts +2 -1
  52. package/dist/lib/components/ui/display/description/description.d.ts +2 -2
  53. package/dist/lib/components/ui/display/list-tile/internal.d.ts +1 -0
  54. package/dist/lib/components/ui/display/list-tile/partials/list-tile-dense-text.d.ts +28 -0
  55. package/dist/lib/components/ui/feedback/toast/toast-text.d.ts +7 -0
  56. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  57. package/dist/lib/components/ui/fields/verification-code/use-resend-focus.d.ts +4 -1
  58. package/dist/lib/components/ui/forms/index.d.ts +2 -0
  59. package/dist/lib/components/ui/forms/use-fast-form.d.ts +25 -0
  60. package/dist/lib/components/ui/forms/use-focus-on-refusal.d.ts +14 -0
  61. package/dist/lib/components/ui/layout/app-shell/activity-content-aside.d.ts +3 -1
  62. package/dist/lib/components/ui/layout/app-shell/activity-content.d.ts +10 -1
  63. package/dist/lib/components/ui/primitives/tabs/tabs-overflow-classes.d.ts +8 -5
  64. package/dist/lib/components/ui/primitives/tabs/use-tabs-overflow.d.ts +4 -3
  65. package/dist/lib/components/ui/selection/switcher/partials/switcher-trigger.d.ts +3 -1
  66. package/dist/lib/components/ui/selection/switcher/switcher.d.ts +1 -1
  67. package/dist/lib/components/ui/selection/switcher/types.d.ts +7 -0
  68. package/dist/lib/components/ui/widgets/native-charts/core/reference-text.d.ts +14 -6
  69. package/dist/lib/components/ui/widgets/native-charts/core/reference.d.ts +57 -10
  70. package/dist/lib/components/ui/widgets/native-charts/core/types.d.ts +13 -5
  71. package/dist/lib/components/ui/widgets/native-charts/hooks/chart-model-types.d.ts +3 -2
  72. package/dist/lib/components/ui/widgets/native-charts/index.d.ts +1 -1
  73. package/dist/lib/components/ui/widgets/native-charts/line-chart/line-chart-types.d.ts +6 -1
  74. package/dist/lib/components/ui/widgets/native-charts/line-chart/partials/reference-marks.d.ts +1 -1
  75. package/dist/lib/i18n/i18n-provider.d.ts +14 -3
  76. package/dist/lib/i18n/material-resources-registry.d.ts +21 -0
  77. package/dist/lib/i18n/resources/en/en.d.ts +4 -0
  78. package/dist/lib/i18n/resources/en/material.json.d.ts +5 -1
  79. package/dist/lib/i18n/resources/fr/fr.d.ts +4 -0
  80. package/dist/lib/i18n/resources/fr/material.json.d.ts +5 -1
  81. package/dist/lib/i18n/resources/resources.d.ts +8 -0
  82. package/dist/lib/i18n/translate-material.d.ts +7 -1
  83. package/dist/lib/i18n/translator.d.ts +1 -1
  84. package/dist/lib/i18n/types.d.ts +6 -0
  85. package/dist/lib/i18n/use-material-translation.d.ts +4 -0
  86. package/dist/lib.js +1 -1
  87. package/dist/main.js +43 -43
  88. package/dist/map.js +8 -8
  89. package/dist/material.css +1 -1
  90. package/dist/pdf-reader.js +1 -1
  91. package/dist/print.js +5 -4
  92. package/dist/{public-CTZ5RK_Y.js → public-BoVE0ffh.js} +9 -9
  93. package/dist/{rich-code-block-oTgL-s4W.js → rich-code-block-DAUM7Yh_.js} +5 -5
  94. package/dist/{sortable-provider-CpA5NYgD.js → sortable-provider-BLXsGE_0.js} +2 -2
  95. package/dist/{spinner-UvqlDsV8.js → spinner-Rg4PnBPH.js} +1 -1
  96. package/dist/{stored-language-Cb9FMEwk.js → stored-language-B1lJMPbR.js} +10 -2
  97. package/dist/theme-picker.js +2 -2
  98. package/dist/{theme-toggle-BZVz6O5q.js → theme-toggle-jVdxF_mc.js} +1 -1
  99. package/dist/{toolbar-items-C09bJphe.js → toolbar-items-BFY_hns5.js} +4 -4
  100. package/dist/translate-material-q-JNxP1m.js +20 -0
  101. package/dist/{translator-BnQXtpSZ.js → translator-BU1o3_l9.js} +1 -1
  102. package/dist/{use-chat-composer-AZAj4tFJ.js → use-chat-composer-DGbDuIUa.js} +3783 -3719
  103. package/dist/{use-heading-spy-C9-A2md2.js → use-heading-spy-CP63sAcS.js} +1 -1
  104. package/dist/use-material-translation-nNF8fUXz.js +14 -0
  105. package/dist/use-shape-morph-DXdy2X5g.js +35 -0
  106. package/docs/components/buttons-and-actions.md +30 -1
  107. package/docs/components/data-display.md +3 -3
  108. package/docs/components/data-table-filter-and-lists.md +2 -2
  109. package/docs/components/navigation-and-layout.md +1 -0
  110. package/docs/components/primitives.md +1 -1
  111. package/docs/components/rich-widgets.md +2 -1
  112. package/docs/components/selection-and-choice.md +20 -0
  113. package/docs/guides/app-shell.md +22 -0
  114. package/docs/guides/cards-and-tiles.md +40 -4
  115. package/docs/guides/charts.md +30 -2
  116. package/docs/guides/data-grid.md +42 -2
  117. package/docs/guides/forms.md +35 -0
  118. package/docs/guides/helpers.md +1 -1
  119. package/docs/guides/i18n.md +34 -8
  120. package/docs/guides/two-factor.md +2 -1
  121. package/docs/previews/guides-i18n-i18nprovider.webp +0 -0
  122. package/docs/previews/guides-i18n-translated-components.webp +0 -0
  123. package/docs/roadmap.md +84 -684
  124. package/docs/troubleshooting.md +24 -2
  125. package/lib/components/components.css +1 -5
  126. package/lib/components/ui/actions/action-card/action-card-tile.css +11 -2
  127. package/lib/components/ui/collections/data-table/data-table-items-record.css +27 -0
  128. package/lib/components/ui/collections/data-table/index.css +14 -0
  129. package/package.json +1 -1
  130. package/dist/i18n-provider-Ang2f31m.js +0 -43
  131. package/dist/translate-material-D6KCK21i.js +0 -12
  132. package/dist/use-material-translation-pLgSqqLw.js +0 -14
  133. package/dist/use-shape-morph-BKA_R5P5.js +0 -35
@@ -1,4 +1,4 @@
1
- import { r as e } from "./i18n-provider-Ang2f31m.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";
@@ -0,0 +1,14 @@
1
+ import { r as e } from "./i18n-provider-DlVPS58N.js";
2
+ import { t } from "./translate-material-q-JNxP1m.js";
3
+ import { useCallback as n } from "react";
4
+ //#region lib/i18n/use-material-translation.ts
5
+ function r() {
6
+ let r = e(), i = r?.language, a = r?.resources;
7
+ return {
8
+ t: n((e, n, r) => t(i, e, n, r, a), [i, a]),
9
+ language: i,
10
+ hasProvider: !!r
11
+ };
12
+ }
13
+ //#endregion
14
+ export { r as t };
@@ -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
 
@@ -642,7 +642,7 @@ import { Description } from '@nxgt/material/components';
642
642
  </div>
643
643
  ```
644
644
 
645
- `label` and `value` are its main props; the rest go to the `div`. A `value` of `null`, `undefined`, `false` or `''` renders nothing (`0` is shown), so optional fields need no guard. Inside a `variant="panels"` list a `Description` must sit in a `DescriptionList.Group`; otherwise it renders its stand-alone markup. The label is a small
645
+ `label` and `value` are its main props; the rest go to the `div`. A `value` of `null`, `undefined`, `false` or `''` with no `lines` and no `children` renders nothing (`0` is shown), so optional fields need no guard. `children` alone are the value (`<Description label="Note">text</Description>`); after a `value` they follow it. Inside a `variant="panels"` list a `Description` must sit in a `DescriptionList.Group`; otherwise it renders its stand-alone markup. The label is a small
646
646
  muted caption above the value (not a title). `Description` takes one cell of
647
647
  the grid it is in; pass `className="col-start-full"` for a full-width row. It takes `lines` anywhere; inside a [`DescriptionList`](#descriptionlist) also `span` and `labelPosition`.
648
648
 
@@ -686,7 +686,7 @@ row) or a number of columns (used once that many fit; at most `maxColumns`, and
686
686
  field uses dense packing: the fields after it fill the cells before it, so they
687
687
  may be drawn above it while the DOM and reading order stay the source order.
688
688
  Put the full-width field where that reads fine, or last. A multi-line value
689
- (an address) takes `lines={[…]}` or `children`, never a string to split. Entries of `lines` that are `null`, `false` or `""` are skipped, and a `Description` whose `lines` are all empty and that has no `value` renders nothing.
689
+ (an address) takes `lines={[…]}` or `children`, never a string to split. Entries of `lines` that are `null`, `false` or `""` are skipped, and a `Description` whose `lines` are all empty and that has no `value` or `children` renders nothing.
690
690
 
691
691
  ```tsx
692
692
  <DescriptionList variant="panels">
@@ -1087,7 +1087,7 @@ yours, the component names no domain.
1087
1087
 
1088
1088
  | Prop | Type | Default | Effect |
1089
1089
  | --- | --- | --- | --- |
1090
- | `entries` | `ChangeHistoryEntry[]` | — | `id`, `kind`, `at` (`Date`, string or number), `author` (`name`, `src`), `changes` (`ChangeHistoryChange[]`: `field`, `before`, `after`, `redacted`) |
1090
+ | `entries` | `ChangeHistoryEntry[]` | — | `id`, `kind`, `at` (`Date`, string or number), `author` (`name`, `src`), `changes` (optional, `ChangeHistoryChange[]`: `field`, `before`, `after`, `redacted`; left out or `[]`, the entry shows its sentence, author and time with no change list) |
1091
1091
  | `kinds` | `Record<string, ChangeHistoryKindOverride>` | built-ins | `label`, `verb`, `icon` (`null` removes it), `tone` per kind; `creation` (default true for `created`) makes an entry list values only |
1092
1092
  | `variant` | `'rail' \| 'cards'` | `'rail'` | the look |
1093
1093
  | `groupBy` | `'day' \| 'none'` | `'day'` | a heading per day (sticky on the rail), or one list in the order given |
@@ -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 |
@@ -735,7 +735,7 @@ reached with the arrow keys. The list scrolls sideways when the tabs do not
735
735
  fit (its scrollbar hidden), which suits a phone or a narrow popover. While it
736
736
  overflows, each side with hidden tabs fades and gets a small scroll button
737
737
  ("Scroll tabs left" / "Scroll tabs right", from `material.tabs.*`; out of the
738
- tab order, 44px on a touch screen); the selected and the focused tab are
738
+ tab order; on a touch screen 44px wide and as tall as the list, so it never covers content); the selected and the focused tab are
739
739
  scrolled into view, instantly under reduced motion. A list that fits has
740
740
  neither, and neither is in the server markup. `segmented` does the same.
741
741
 
@@ -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
@@ -265,7 +265,7 @@ flag. Storybook: `Guides/Hooks/useSubmitOnce`.
265
265
  | `formatMessage(message, language, values?, { onError }?)` | Formats one ICU message (`{name}`, plurals, selects) in any BCP 47 `language` with the translator's engine, for a message that is not in a bundle (a server-side catalogue). Cached per message; one that does not format is logged and returned unformatted, or passed to `onError` as a `FormatMessageError` (reason, key, source) without logging. |
266
266
  | `createTypedTranslation(translate)` | Turns a translator into a typed `useTranslation()` hook. |
267
267
  | `translate`, `useMaterialTranslation()` | Translator / hook bound to the package's own `material.*` keys. `useMaterialTranslation().t(key, fallback)` returns `fallback` without a provider. |
268
- | `translateMaterial(language, key, fallback, values?)` | The same as `useMaterialTranslation().t`, with the language passed as a value: no provider, no hook. For a PDF `footer` function or a server. `'fr-CA'` reads `fr`; an unknown language formats `fallback`. |
268
+ | `translateMaterial(language, key, fallback, values?, resources?)` | The same as `useMaterialTranslation().t`, with the language passed as a value: no provider, no hook. `resources` is your merged bundle (stable object), read before the base one. For a PDF `footer` function or a server. `'fr-CA'` reads `fr`; an unknown language formats `fallback`. |
269
269
  | `getLanguage()` | Language read from `localStorage`, else `FALLBACK_LANGUAGE`. |
270
270
  | `LANGUAGE_KEY`, `FALLBACK_LANGUAGE`, `SUPPORTED_LANGUAGES` | Storage key (`'language'`), fallback (`'en'`) and supported languages (`en`, `fr`). |
271
271
 
@@ -63,6 +63,7 @@ function translateMaterial(
63
63
  key: MaterialLocaleKey,
64
64
  fallback: string,
65
65
  values?: TranslationContext,
66
+ resources?: ResourceBundle, // your merged bundle, a stable object (module scope)
66
67
  ): string;
67
68
  ```
68
69
 
@@ -189,6 +190,7 @@ if (failure) {
189
190
  | `initialLanguage` | `Language` | — | Used as-is for the first render when supported, and `localStorage` is not read. Set it in SSR. See [below](#server-rendering) |
190
191
  | `storageKey` | `string` | `'language'` | `localStorage` key the choice is read from and written to |
191
192
  | `supportedLanguages` | `Language[]` | `['en', 'fr']` | What `changeLanguage` accepts and `LanguageSwitcher` lists |
193
+ | `resources` | `ResourceBundle` | — | Your `mergeResources(...)` result: the components read their `material.*` words from it before the base bundle. Pass a stable object (module scope) |
192
194
  | `onLanguageChange` | `(language) => void` | — | Called after a successful change: save it to a cookie, a profile, analytics |
193
195
 
194
196
  `changeLanguage` with an unsupported value logs
@@ -208,7 +210,8 @@ const context = useOptionalI18nContext(); // null outside the provider
208
210
  third language is not in the type today, so adding one to
209
211
  `supportedLanguages` takes a cast. The runtime accepts it: your translator
210
212
  uses your strings for it, and the components fall back to their English copy,
211
- formatted.
213
+ formatted. A third language you define under `material.*` and give through
214
+ `resources` now reaches the components.
212
215
 
213
216
  ```tsx
214
217
  const LANGUAGES = ['en', 'fr', 'de'] as Language[];
@@ -498,8 +501,8 @@ material.emoji.groups.flags; // "Flags" / "Drapeaux"
498
501
 
499
502
  A group your `EmojiItem`s name that is not in this list (`hand_signs`) reads
500
503
  as its words in English (`Hand signs`). As for every `material.*` key above,
501
- the components read their own bundle: your own translator sees a key you add,
502
- the tab does not.
504
+ your own translator sees a key you add; the components see it too once you hand
505
+ them the merged resources (see "Overriding a component's words" below).
503
506
 
504
507
  ```tsx
505
508
  // Under <I18nProvider defaultLanguage="fr">
@@ -509,11 +512,34 @@ the tab does not.
509
512
  <Spinner variant="4" /> // role="img" aria-label="Chargement..."
510
513
  ```
511
514
 
512
- That override is visible only through **your** translator. The components
513
- translate through the package's own bundle, so defining
514
- `material.command.title` in your resources changes what
515
- `t('material.command.title')` returns in your code, but not the title
516
- `CommandDialog` renders.
515
+ ### Overriding a component's words
516
+
517
+ Give the provider the merged resources and every component translates through
518
+ them, so an override under `material.*` changes what it renders:
519
+
520
+ ```tsx
521
+ import { mergeResources, resources as materialResources } from '@nxgt/material/i18n';
522
+
523
+ const resources = mergeResources(materialResources, {
524
+ en: { material: { 'recovery-codes-status': { title: 'Backup codes' } } },
525
+ });
526
+
527
+ <I18nProvider defaultLanguage="en" resources={resources}>
528
+ <RecoveryCodesStatus remaining={3} onRegenerate={regenerate} /> {/* title: "Backup codes" */}
529
+ </I18nProvider>
530
+ ```
531
+
532
+ Without `resources`, the components read the package's own bundle: an override
533
+ is visible only through **your** translator, and defining `material.command.title`
534
+ does not change the title `CommandDialog` renders. Pass a stable object
535
+ (module scope), not one built in a render. A component's `labels` prop still
536
+ wins over both. Outside a component, `translateMaterial` takes the merged
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.
517
543
 
518
544
  Each time `mergeResources` runs outside production, overlapping leaves are
519
545
  reported, one line per language:
@@ -62,7 +62,8 @@ in `@nxgt/material-core/two-factor`.
62
62
  What stays in the app: every request, the mapping of a server error to the
63
63
  props above, where the challenge is kept, the replay of the action a step-up
64
64
  unblocked, the leave guard around the new recovery codes, and all wording
65
- (`labels`, or your translations under `material.*`).
65
+ (`labels`, or your translations under `material.*`, given to
66
+ `I18nProvider` as `resources`, see [i18n](i18n.md#overriding-a-components-words)).
66
67
 
67
68
  ## On a phone and for assistive technology
68
69