@nxgt/material 3.103.0 → 3.104.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 -3
  3. package/dist/{alert-dialog-NFyAiSqc.js → alert-dialog-CK_47jfm.js} +1 -1
  4. package/dist/{async-slice-D35bX79S.js → async-slice-1ZNMiwyJ.js} +1 -1
  5. package/dist/{button-Be7GZzF9.js → button-YfEMZbRO.js} +1 -1
  6. package/dist/{calculator-BGy1Q7rn.js → calculator-BmYZId4c.js} +11 -11
  7. package/dist/calculator-field.js +6 -6
  8. package/dist/{calculator-segment-uixk6lBg.js → calculator-segment-DJ08DJ-n.js} +1 -1
  9. package/dist/calculator.js +1 -1
  10. package/dist/charts.js +819 -764
  11. package/dist/{chip-3rWebZEM.js → chip-DPj6e-3v.js} +2 -2
  12. package/dist/{code-editor-BqSVMfy-.js → code-editor-CZg3nLZO.js} +4 -4
  13. package/dist/code-editor.js +4 -4
  14. package/dist/{code-language-items-Cf2LJVg2.js → code-language-items-Nq1iOGId.js} +2 -2
  15. package/dist/components/ui/display/chip/chip-soft.css +27 -2
  16. package/dist/components/ui/primitives/collapsible/collapsible.css +9 -0
  17. package/dist/{components-D2liJ9Yu.js → components-B2wrH_W0.js} +16884 -16603
  18. package/dist/components.js +38 -38
  19. package/dist/{conversion-variant-clFZ6ZMJ.js → conversion-variant-C-LPx5ek.js} +4 -4
  20. package/dist/{currency-field-CBFEVNM1.js → currency-field-DnsRIA80.js} +4 -4
  21. package/dist/{dialog-CrIIINTk.js → dialog-BKBsDGFc.js} +3 -3
  22. package/dist/dnd.js +1 -1
  23. package/dist/{dropdown-menu-BnfLWu8n.js → dropdown-menu-QMDbRidH.js} +1 -1
  24. package/dist/editor.js +8 -8
  25. package/dist/{empty-state-CuUaCPY7.js → empty-state-BLIJ6znY.js} +1 -1
  26. package/dist/{financial-variant-BY_VCUuk.js → financial-variant-Ws0Jp01H.js} +5 -5
  27. package/dist/{hooks-hHoZbL-4.js → hooks-C3vkQmzh.js} +2 -2
  28. package/dist/hooks.js +2 -2
  29. package/dist/{hover-card-Do2-uzlF.js → hover-card-Df649U6I.js} +1 -1
  30. package/dist/i18n.js +3 -3
  31. package/dist/{icon-badge-CHaVNEmb.js → icon-badge-Cma1qOOJ.js} +1 -1
  32. package/dist/{icon-button-BIAubLeJ.js → icon-button-CpKxV264.js} +1 -1
  33. package/dist/keypad.js +3 -3
  34. package/dist/lib/components/ui/actions/index.d.ts +1 -0
  35. package/dist/lib/components/ui/actions/product-tile/index.d.ts +1 -0
  36. package/dist/lib/components/ui/actions/product-tile/product-tile-link.d.ts +16 -0
  37. package/dist/lib/components/ui/actions/product-tile/product-tile.d.ts +41 -0
  38. package/dist/lib/components/ui/collections/data-table/data-table-density.d.ts +1 -1
  39. package/dist/lib/components/ui/collections/data-table/partials/data-table-scroll-region.d.ts +4 -1
  40. package/dist/lib/components/ui/collections/data-table/partials/layouts/data-table-list-bar.d.ts +6 -3
  41. package/dist/lib/components/ui/collections/data-table/partials/layouts/list-bar-in-row.d.ts +2 -1
  42. package/dist/lib/components/ui/collections/data-table/types.d.ts +11 -8
  43. package/dist/lib/components/ui/collections/data-table/virtual-option.d.ts +9 -3
  44. package/dist/lib/components/ui/collections/filter/use-filter-field-config.d.ts +2 -0
  45. package/dist/lib/components/ui/collections/server-table/partials/server-table-passed-through.d.ts +1 -0
  46. package/dist/lib/components/ui/collections/server-table/types.d.ts +1 -1
  47. package/dist/lib/components/ui/display/chip/chip.variants.d.ts +5 -4
  48. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  49. package/dist/lib/components/ui/fields/index.d.ts +1 -0
  50. package/dist/lib/components/ui/fields/quantity-field/types.d.ts +1 -1
  51. package/dist/lib/components/ui/fields/quantity-stepper/index.d.ts +2 -0
  52. package/dist/lib/components/ui/fields/quantity-stepper/quantity-stepper-model.d.ts +9 -0
  53. package/dist/lib/components/ui/fields/quantity-stepper/quantity-stepper-value.d.ts +15 -0
  54. package/dist/lib/components/ui/fields/quantity-stepper/quantity-stepper.d.ts +9 -0
  55. package/dist/lib/components/ui/fields/quantity-stepper/quantity-stepper.types.d.ts +35 -0
  56. package/dist/lib/components/ui/fields/quantity-stepper/use-quantity-stepper.d.ts +28 -0
  57. package/dist/lib/components/ui/layout/pane-layout/pane-layout.d.ts +8 -3
  58. package/dist/lib/components/ui/layout/pane-layout/partials/pane-layout-classes.d.ts +2 -2
  59. package/dist/lib/components/ui/layout/pane-layout/partials/pane-layout-context.d.ts +9 -0
  60. package/dist/lib/components/ui/layout/pane-layout/partials/use-pane-layout-state.d.ts +7 -2
  61. package/dist/lib/components/ui/layout/panel-card/panel-card.d.ts +3 -1
  62. package/dist/lib/components/ui/primitives/collapsible/collapsible.d.ts +9 -1
  63. package/dist/lib/components/ui/primitives/tabs/partials/tabs-list.d.ts +6 -1
  64. package/dist/lib/components/ui/primitives/tabs/tabs-classes.d.ts +1 -0
  65. package/dist/lib/components/ui/selection/chip-group-field/chip-group-field.d.ts +8 -0
  66. package/dist/lib/components/ui/selection/chip-group-field/chip-group-field.types.d.ts +6 -0
  67. package/dist/lib/components/ui/selection/chip-group-field/partials/chip-group-reasons.d.ts +10 -0
  68. package/dist/lib/components/ui/selection/chip-group-field/partials/use-chip-rail.d.ts +12 -0
  69. package/dist/lib/components/ui/selection/choice-group/choice-group-root.d.ts +5 -2
  70. package/dist/lib/components/ui/widgets/native-charts/core/label-placement.d.ts +25 -0
  71. package/dist/lib/components/ui/widgets/native-charts/core/reference-geometry.d.ts +3 -2
  72. package/dist/lib/i18n/resources/en/en.d.ts +3 -0
  73. package/dist/lib/i18n/resources/en/material.json.d.ts +4 -1
  74. package/dist/lib/i18n/resources/fr/fr.d.ts +3 -0
  75. package/dist/lib/i18n/resources/fr/material.json.d.ts +4 -1
  76. package/dist/lib/i18n/resources/resources.d.ts +6 -0
  77. package/dist/lib/i18n/translator.d.ts +1 -1
  78. package/dist/lib/print/output/csv.d.ts +37 -0
  79. package/dist/lib/print/output/index.d.ts +1 -0
  80. package/dist/lib.js +1 -1
  81. package/dist/main.js +54 -54
  82. package/dist/map.js +8 -8
  83. package/dist/material.css +1 -1
  84. package/dist/pdf-reader.js +1 -1
  85. package/dist/{percent-field-DU8eIMD3.js → percent-field-sAq7BaxN.js} +2 -2
  86. package/dist/{picker-surface-XmA--JVI.js → picker-surface--6E-nvpU.js} +4 -4
  87. package/dist/{popover-D8iHiAbl.js → popover-bEB8I_-w.js} +1 -1
  88. package/dist/{pos-types-BEAY27Rm.js → pos-types-Cpdc4E33.js} +1 -1
  89. package/dist/{pos-variant-ClGI92Lt.js → pos-variant-ClMGu59I.js} +5 -5
  90. package/dist/print.js +146 -127
  91. package/dist/{programmer-variant-Boq2O8hB.js → programmer-variant-ChzgyA7x.js} +2 -2
  92. package/dist/{public-DOrXKmcL.js → public-BY8N5NDT.js} +9 -9
  93. package/dist/{rich-code-block-Djj2XE_u.js → rich-code-block-D2GiWuqD.js} +5 -5
  94. package/dist/{scientific-variant-BzF_aMQP.js → scientific-variant-31_93gcZ.js} +2 -2
  95. package/dist/{sortable-provider-DCZjsIIH.js → sortable-provider-DGiVn72t.js} +2 -2
  96. package/dist/{spinner-Cg0y_E_K.js → spinner-CEVomQ5K.js} +1 -1
  97. package/dist/{tabs-DpessV5s.js → tabs-B8MXBbPW.js} +14 -14
  98. package/dist/{text-field-Dxk9lAKM.js → text-field-DN88qD0Q.js} +1 -1
  99. package/dist/theme-picker.js +2 -2
  100. package/dist/{theme-toggle-B6Jl5Kfn.js → theme-toggle-C7pnu1Xh.js} +1 -1
  101. package/dist/{toolbar-items-Cs1XrZsH.js → toolbar-items-BDfoh8QB.js} +4 -4
  102. package/dist/{translate-material-Du0j0t97.js → translate-material-s_lxIV43.js} +1 -1
  103. package/dist/{translator-BVs4n0Fz.js → translator-Cgql9srO.js} +4 -2
  104. package/dist/{use-calculator-keyboard-BZ1CA4Ut.js → use-calculator-keyboard-DysnFK8m.js} +1 -1
  105. package/dist/{use-chat-composer-B4m3OEm1.js → use-chat-composer-xCcp9fVc.js} +2050 -1996
  106. package/dist/{use-heading-spy-CO3_AtY9.js → use-heading-spy-FIZlf9qj.js} +4 -4
  107. package/dist/{use-material-translation-uNc1FyC8.js → use-material-translation-xNzbSVgS.js} +1 -1
  108. package/dist/{use-return-focus-AfLRrUm-.js → use-return-focus-CW9KkDkp.js} +1 -1
  109. package/docs/README.md +1 -1
  110. package/docs/components/README.md +1 -0
  111. package/docs/components/buttons-and-actions.md +47 -0
  112. package/docs/components/data-display.md +2 -2
  113. package/docs/components/data-table-filter-and-lists.md +19 -6
  114. package/docs/components/form-fields.md +60 -7
  115. package/docs/components/navigation-and-layout.md +20 -2
  116. package/docs/components/primitives.md +44 -2
  117. package/docs/components/selection-and-choice.md +56 -1
  118. package/docs/guides/cards-and-tiles.md +10 -0
  119. package/docs/guides/csp.md +4 -3
  120. package/docs/guides/data-grid.md +5 -4
  121. package/docs/guides/field-look-refined.md +2 -2
  122. package/docs/guides/keypad.md +9 -1
  123. package/docs/guides/printing.md +51 -1
  124. package/docs/previews/buttons-actions-producttile.webp +0 -0
  125. package/docs/previews/form-fields-quantitystepper.webp +0 -0
  126. package/docs/previews/navigation-layout-panelcard.webp +0 -0
  127. package/docs/roadmap.md +10 -10
  128. package/docs/troubleshooting.md +5 -4
  129. package/lib/components/ui/display/chip/chip-soft.css +27 -2
  130. package/lib/components/ui/primitives/collapsible/collapsible.css +9 -0
  131. package/lib/components/ui/widgets/calculator/calculator-keys.css +3 -0
  132. package/lib/components/ui/widgets/calculator/calculator.css +38 -7
  133. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  import { t as e } from "./utils-BLfntITJ.js";
2
- import { a as t, c as n, d as r, i, n as a, r as o, t as s, u as c } from "./alert-dialog-NFyAiSqc.js";
2
+ import { a as t, c as n, d as r, i, n as a, r as o, t as s, u as c } from "./alert-dialog-CK_47jfm.js";
3
3
  import { t as l } from "./label-DR-zZyGQ.js";
4
- import { a as u } from "./button-Be7GZzF9.js";
5
- import { i as d } from "./translate-material-Du0j0t97.js";
6
- import { t as f } from "./use-material-translation-uNc1FyC8.js";
4
+ import { a as u } from "./button-YfEMZbRO.js";
5
+ import { i as d } from "./translate-material-s_lxIV43.js";
6
+ import { t as f } from "./use-material-translation-xNzbSVgS.js";
7
7
  import { _ as p, g as m, h, t as g, v as _, y as v } from "./date.utils-JTEIo3mM.js";
8
8
  import { t as y } from "./input-CC2joTjB.js";
9
9
  import { useCallback as b, useEffect as x, useId as S, useMemo as C, useRef as w, useState as T } from "react";
@@ -1,4 +1,4 @@
1
- import { i as e, t } from "./translate-material-Du0j0t97.js";
1
+ import { i as e, t } from "./translate-material-s_lxIV43.js";
2
2
  import { useCallback as n } from "react";
3
3
  //#region lib/i18n/use-material-translation.ts
4
4
  function r() {
@@ -1,6 +1,6 @@
1
1
  import { o as e, r as t } from "./dist-DVRQkaTK.js";
2
2
  import { i as n, n as r, r as i, t as a } from "./dist-DWZXlCZR.js";
3
- import { C as o, S as s, x as c } from "./spinner-Cg0y_E_K.js";
3
+ import { C as o, S as s, x as c } from "./spinner-CEVomQ5K.js";
4
4
  import { a as l, i as u, r as d } from "./dist-DHdUUM1w.js";
5
5
  import { c as f, i as p, l as m, o as h, r as g, s as ee } from "./in-dialog-D61Yeaol.js";
6
6
  import { r as te } from "./escape-owner-DpaP0lm1.js";
package/docs/README.md CHANGED
@@ -22,7 +22,7 @@ detail: every option, the second example, and the traps.
22
22
  | [Layouts](guides/layouts.md) | You are choosing a navigation preset, an app bar variant or a page layout, and want one working example of each |
23
23
  | [Animations](guides/animations.md) | You animate with `animate-*` classes: setup next to `tw-animate-css`, every animation with its defaults, the modifiers, stagger, text splitting, scroll-driven animation, appear/disappear, reduced motion, recipes and browser support |
24
24
  | [Theming](guides/theming.md) | You want to change colours or radius, use a shipped theme (`@nxgt/material/themes/<name>.css`) or a themed zone, add dark mode without a flash, use tones (the `data-tone` channel and app tones), or use the compiled CSS without Tailwind |
25
- | [Printing](guides/printing.md) | You render PDF documents with `@nxgt/material/print`, turn them into a download, a new tab or a server response, write them as plain data, or bundle them into an SSR server |
25
+ | [Printing](guides/printing.md) | You render PDF documents with `@nxgt/material/print`, turn them into a download, a new tab or a server response, write them as plain data, bundle them into an SSR server, or export a list as CSV with `toCsv` and `csvResponse` |
26
26
  | [PDF reader](guides/pdf-reader.md) | You render PDFs with `@nxgt/material/pdf-reader`: the pdf.js assets and worker, `configurePdfReader`, server rendering, the Content-Security-Policy and moving from the classic reader (removed in 3.0) |
27
27
  | [Media players](guides/media.md) | You play video or audio: embedded, audio cards, playlists, chapters and transcript, sound across pages with `MediaPlayerHost`, the lightbox, CSP and keyboard |
28
28
  | [Streaming (HLS)](guides/media-streaming.md) | You play `.m3u8` streams in `MediaPlayer` with `@nxgt/material/media-hls` |
@@ -34,6 +34,7 @@ Anything you press that is not a field: buttons, split buttons, floating action
34
34
  [<img src="../previews/buttons-actions-toolbar.webp" height="72" alt="Toolbar">](buttons-and-actions.md#toolbar)
35
35
  [<img src="../previews/buttons-actions-iconbutton.webp" height="72" alt="IconButton">](buttons-and-actions.md#iconbutton)
36
36
  [<img src="../previews/buttons-actions-actioncard.webp" height="72" alt="ActionCard">](buttons-and-actions.md#actioncard)
37
+ [<img src="../previews/buttons-actions-producttile.webp" height="72" alt="ProductTile">](buttons-and-actions.md#producttile)
37
38
 
38
39
  ## [Form fields](form-fields.md)
39
40
 
@@ -24,6 +24,7 @@ and language switches. Everything is imported from `@nxgt/material/components`.
24
24
  | <img src="../previews/buttons-actions-scrolltotop.webp" width="96" alt=""> | [ScrollToTop](#scrolltotop) | Appears past a scroll threshold |
25
25
  | <img src="../previews/buttons-actions-scrolltolatestbutton.webp" width="96" alt=""> | [ScrollToLatestButton](#scrolltolatestbutton) | "Jump to latest" over a scroll box, counting what arrived |
26
26
  | <img src="../previews/buttons-actions-actioncard.webp" width="96" alt=""> | [ActionCard](#actioncard) | Clickable card, a button (`aria-pressed`) when it chooses or reacts, with an active indicator; `status`, `meta`, a trailing `action` and a `tile` variant |
27
+ | <img src="../previews/buttons-actions-producttile.webp" width="96" alt=""> | [ProductTile](#producttile) | Large tap tile for a till or kiosk catalogue: picture or icon, name, price, stock state |
27
28
  | <img src="../previews/buttons-actions-themetoggle.webp" width="96" alt=""> | [ThemeToggle](#themetoggle) | Light / dark / system switch |
28
29
  | <img src="../previews/buttons-actions-languageswitcher.webp" width="96" alt=""> | [LanguageSwitcher](#languageswitcher) | Menu of the `I18nProvider` languages |
29
30
 
@@ -1335,6 +1336,52 @@ A whole board built on it, with sections (each its own h2 and free count) and, w
1335
1336
 
1336
1337
  ---
1337
1338
 
1339
+ ## ProductTile
1340
+
1341
+ ![ProductTile](../previews/buttons-actions-producttile.webp)
1342
+
1343
+ A large, even tap tile for a point-of-sale or kiosk catalogue: a picture (or a toned shape with an icon), the item's name, a prominent price and a stock state on its own line. `ActionCard` has no media or price slot; this is the tile for a catalogue.
1344
+
1345
+ ```tsx
1346
+ import { Badge, ProductTile, ResponsiveGrid } from '@nxgt/material/components';
1347
+
1348
+ <ResponsiveGrid minItem="9rem" gap="md">
1349
+ <ProductTile
1350
+ title="Orange juice 1 L"
1351
+ price="1,500"
1352
+ icon="bottle-water"
1353
+ selected={basket.has(id)}
1354
+ onClick={() => add(id)}
1355
+ />
1356
+ <ProductTile
1357
+ title="Free-range eggs, tray of thirty"
1358
+ price="4,800"
1359
+ image="/eggs.jpg"
1360
+ disabled
1361
+ status={<Badge variant="soft" tone="error">Out of stock</Badge>}
1362
+ />
1363
+ </ResponsiveGrid>
1364
+ ```
1365
+
1366
+ | Prop | Type | Default | Effect |
1367
+ | --- | --- | --- | --- |
1368
+ | `title` | `ReactNode` | required | The name, up to two lines |
1369
+ | `price` | `ReactNode` | none | Large and tabular (`tabular-nums`), at the foot of the tile; format it with the app's locale |
1370
+ | `status` | `ReactNode` | none | The stock state, on its own line under the price (a `Badge`) |
1371
+ | `image` | `string` | none | The photo's URL (a square one fits best), in a tinted 4:3 well, lazy loaded, with an empty `alt` (the title names the tile). If it fails to load, the toned `icon` shape takes its place |
1372
+ | `imageFit` | `'contain' \| 'cover'` | `contain` | `contain` for a cut-out PNG or a packshot on white, `cover` for a full-bleed photo |
1373
+ | `icon`, `tone`, `shape` | `IconName`, `Tone`, `ShapeName` | `primary`, `squircle` | Without an `image` (or when it fails), a toned shape with this icon, in the same 4:3 well, so tiles with and without a photo share one height |
1374
+ | `media` | `ReactNode` | none | Your own media in place of the box; replaces `image` and `icon` |
1375
+ | `selected` | `boolean` | `false` | `aria-pressed`, a tint and a bar at the start (the item is in the basket) |
1376
+ | `disabled` | `boolean` | `false` | Dimmed and not tappable; its text, status included, is still read |
1377
+ | `asChild` | `boolean` | `false` | Render inside the one child (a router `Link`): `<ProductTile asChild title="…"><Link to="…" /></ProductTile>`. `selected` is then `aria-current`, `disabled` is `aria-disabled`, `tabIndex={-1}` and a cancelled click, so a link leaves the Tab order and Enter does not follow it; `onClick` is not called |
1378
+
1379
+ The tile is one `button` named by its text: `getByRole('button', { name: /Orange juice.*1,500/ })`. It is a hairline card, at least 44 px high under a coarse pointer, and fills its grid cell (`h-full`): the name takes two lines and the status line is always reserved, so tiles of a row share one height. `minItem="9rem"` gives two columns at 360 px. It has a pressed state, a focus ring and a dark mode. It takes the `button`'s props, so `className` adds to it.
1380
+
1381
+ **Storybook:** `Buttons & actions/ProductTile` · **Vue:** later
1382
+
1383
+ ---
1384
+
1338
1385
  ## ThemeToggle
1339
1386
 
1340
1387
  ![ThemeToggle](../previews/buttons-actions-themetoggle.webp)
@@ -601,7 +601,7 @@ text colour, 4.5:1 or more on the page and in a card; the `outlined` hairline
601
601
  and the `tag` dot still read `--chip-tone`, not the tone channel); `tag`
602
602
  a smaller tag led by a colour dot; `dashed` a muted "add" pill. 28px tall,
603
603
  24px with `size="sm"`, 44px with `size="lg"`. Selected (`active`, or `aria-pressed` on a toggle
604
- chip) takes a primary tint and a primary edge, and a pressed toggle with
604
+ chip) takes a primary tint and a primary edge (a pressed toggle chip with a `color` shows the filled tone under its on-colour instead), and a pressed toggle with
605
605
  nothing leading draws a check. An unselected neutral toggle chip keeps a
606
606
  boundary of at least 3:1 against the page and the card. The label is truncated as one piece; the ✕
607
607
  and `trailing` stay whole.
@@ -631,7 +631,7 @@ import { Avatar, Chip } from '@nxgt/material/components';
631
631
  | `active` | `boolean` | — | selected: primary tint and edge |
632
632
  | `aria-pressed` | `boolean` | — | a toggle chip: selected while pressed, with a check when nothing leads |
633
633
  | `size` | `'default' \| 'sm' \| 'lg'` | `'default'` | 28px, 24px, or `lg`, a 44px box at every pointer (the touch target); every target stays at least 24px |
634
- | `color` | `'default' \| 'primary' \| 'secondary' \| 'info' \| 'success' \| 'warning' \| 'error'` | neutral | tone: the edge (outlined), the fill and label (tonal, `filled`: a status look) or the dot and tint (tag). A `tag` without `color` (or with `default`) draws no dot |
634
+ | `color` | `'default'` or any `Tone` (the eight named tones, or one declared in `AppTones`) | neutral | tone: the edge (outlined), the fill and label (tonal, `filled`: a status look) or the dot and tint (tag). A `tag` without `color` (or with `default`) draws no dot |
635
635
  | `onDismiss` | `() => void` | — | adds the ✕ button, named "Remove {label}" (`material.chip.dismiss-label`), `{label}` being the text `label` shows, a node's included; "Remove" when it shows none; the ✕ is drawn 18px (16px small) in its own 24x24 box (44x44 on a coarse pointer) |
636
636
  | `dismissLabel` | `string \| ((label: string) => string)` | — | names the ✕ instead of "Remove {label}": a string as is, or a function of the text the chip shows |
637
637
  | `onClick` | `MouseEventHandler` | — | makes the chip (or its body, when dismissible) a button; without it the chip is a `<span>` |
@@ -107,9 +107,11 @@ table enables, for typing your own column defs.
107
107
  | `onLoadMore` / `isLoadingMore` | `() => void` / `boolean` | — | adds a "load more" button under the rows |
108
108
  | `options` | `{ showPage?: boolean; showSelected?: boolean }` | both `true` | parts of the footer |
109
109
  | `showHeader` | `boolean` | shown | header row (column names, sorting, "Select all") at every width; `false` hides it |
110
+ | `showSort` | `boolean` | shown | `false` drops the sort control of the `cards` and `tiles` list bar (on a phone too); columns stay sortable and a `table` keeps its header sorting ([Cards and tiles](../guides/cards-and-tiles.md#sorting)) |
110
111
  | `maxHeight` | `number \| string` | none | caps the table's height (`400`, `'60vh'`); the rows scroll inside it |
112
+ | `maxHeightClassName` | class | none | the same cap as a Tailwind class (`'max-h-[28rem]'`), with no inline `style` on the scroll region, server included; wins over `maxHeight` |
111
113
  | `scrollLabel` | `string` | "Scrollable table" | the name of the scroll region the table becomes while it overflows |
112
- | `virtual` | `boolean \| { estimateSize?, overscan?, ssrRows? }` | off | renders only the rows in view, for long lists; needs `maxHeight`. Client mode shows every filtered and sorted row with no paging and no pagination footer; `manual` virtualizes the rows given. Defaults: `estimateSize` 36 (px, a comfortable row without the gap between rows), `overscan` 8, `ssrRows` 50. `renderExpandedRow` is ignored while it is on ([long lists](#long-lists)) |
114
+ | `virtual` | `boolean \| { estimateSize?, overscan?, ssrRows? }` | off | renders only the rows in view, for long lists; needs `maxHeight` or `maxHeightClassName`. Client mode shows every filtered and sorted row with no paging and no pagination footer; `manual` virtualizes the rows given. Defaults: `estimateSize` 36 (px, a comfortable row without the gap between rows), `overscan` 8, `ssrRows` 50. `renderExpandedRow` is ignored while it is on ([long lists](#long-lists)) |
113
115
  | `loading` | `boolean` | `false` | skeleton rows, or the current rows dimmed; `aria-busy`; controls disabled |
114
116
  | `loadingRows` | `number` | the page size | how many skeleton rows |
115
117
  | `density` | `'comfortable' \| 'compact'` | `'comfortable'` | `compact` tightens the cell padding, the row and header heights and the gap between rows, the selection column included; it is the table's `data-density` attribute, styled with classes (no inline style). The toolbar band and the empty state keep their size; the print variant is unchanged |
@@ -527,12 +529,22 @@ Rows stay rows on a phone unless you ask for `layout="cards"`, `layout="tiles"`
527
529
  virtual
528
530
  maxHeight="70vh"
529
531
  />
532
+
533
+ // Under a strict CSP (`style-src-attr 'none'`): a class, no inline style.
534
+ <DataTable
535
+ data={people}
536
+ columns={columns}
537
+ getRowId={(person) => person.id}
538
+ virtual
539
+ maxHeightClassName="max-h-[28rem]"
540
+ />
530
541
  ```
531
542
 
532
543
  `virtual` renders only the rows in view and a few around them (`overscan`),
533
544
  between two `aria-hidden` spacer rows that stand for the rest. It needs
534
- `maxHeight`, since the rows scroll inside the table: without it, `virtual`
535
- is off and a development warning says so. In client mode every filtered and
545
+ `maxHeight` or `maxHeightClassName`, since the rows scroll inside the table
546
+ (the virtualizer measures the scroll region, so the class needs no number):
547
+ without either, `virtual` is off and a development warning says so. In client mode every filtered and
536
548
  sorted row is in one scrolling list: no paging and no pagination footer
537
549
  (`onLoadMore` still shows its button). In `manual` mode the rows given are
538
550
  virtualized as they are, with the server footer.
@@ -556,8 +568,9 @@ virtualized as they are, with the server footer.
556
568
  takes over after mount. The spacers' heights are custom properties
557
569
  (`--vr-before`, `--vr-after`) written on the `<tbody>` through CSSOM, which
558
570
  `style-src-attr` does not govern: the rows add no `style` attribute to the
559
- server markup. The scroll region's own `maxHeight` is still an inline
560
- `style` there ([csp.md](../guides/csp.md)).
571
+ server markup. The scroll region's `maxHeight` is an inline
572
+ `style` there; pass `maxHeightClassName` instead and it has none
573
+ ([csp.md](../guides/csp.md)).
561
574
 
562
575
  `@nxgt/material-vue`'s `DataTable` has no `virtual` yet.
563
576
 
@@ -1070,7 +1083,7 @@ export default function Students() {
1070
1083
  and its "Clear search and filters" button
1071
1084
  (`onParamsChange(withoutFilters(params))`, then `onClearFilters`, or the
1072
1085
  table, or the list of cards or tiles, takes the focus). Your search box and switches stay outside.
1073
- - **Passed through** to `DataTable`: `columns`, `maxHeight`, `scrollLabel`,
1086
+ - **Passed through** to `DataTable`: `columns`, `maxHeight`, `maxHeightClassName`, `scrollLabel`,
1074
1087
  `showViewOptions`, `renderExpandedRow`, `options`, `loading`,
1075
1088
  `loadingRows`, `getRowHref`, `onRowClick`, `linkComponent`, `density`,
1076
1089
  `layout`, `enableRowSelection`, `rowSelectionDisabledReason`,
@@ -27,6 +27,7 @@ order and roles) and the tokens that tune it.
27
27
  | <img src="../previews/form-fields-numberfield.webp" width="96" alt=""> | [NumberField](#numberfield) | `number \| null` with clamping and +/− buttons |
28
28
  | <img src="../previews/form-fields-percentfield.webp" width="96" alt=""> | [PercentField](#percentfield) | `NumberField` with a `%` suffix |
29
29
  | <img src="../previews/form-fields-quantityfield.webp" width="96" alt=""> | [QuantityField](#quantityfield) | One labelled field holding an amount and its unit |
30
+ | <img src="../previews/form-fields-quantitystepper.webp" width="96" alt=""> | [QuantityStepper](#quantitystepper) | Inline −/value/+ control for a cart or ticket line |
30
31
  | <img src="../previews/form-fields-currencyfield.webp" width="96" alt=""> | [CurrencyField](#currencyfield) | Locale-formatted amount with a currency suffix |
31
32
  | <img src="../previews/form-fields-phonefield.webp" width="96" alt=""> | [PhoneField](#phonefield) | International number, emits E.164 |
32
33
  | <img src="../previews/form-fields-urlfield.webp" width="96" alt=""> | [UrlField](#urlfield) | Validates on blur, open and copy buttons |
@@ -802,7 +803,7 @@ phone: the chip truncates before the value does (see The unit chip below).
802
803
  | `control`, `name` | `Control<T>`, `Path<T>` | — | the form and its `{ value, unit }` (`QuantityFormField`) |
803
804
  | `value`, `defaultValue`, `onChange` | `QuantityValue`, `(value) => void` | empty | the amount and its unit; `onChange` gets both on each change |
804
805
  | `label` | `ReactNode` | none | the one label; it names the group |
805
- | `units` | `{ value, label }[]` | none | the units offered |
806
+ | `units` | `{ value, label }[]` | none | the units offered; without `units` and `allowCustomUnit` there is no unit to pick, so no chip is drawn (a plain count) |
806
807
  | `allowCustomUnit` | `boolean` | `false` | the unit menu gets a "Search or type a unit" input and a "Use “x”" item, so a unit outside `units` can be typed; otherwise it is a plain listbox of `units` |
807
808
  | `min`, `max`, `step`, `hideSteppers` | | `step` 1 | as `NumberField` |
808
809
  | `unitLabel`, `unitPlaceholder` | `string` | "<label> unit" | `unitLabel` names the unit part (the menu's name; the chip reads "<unitLabel>: mg"); `unitPlaceholder` shows while no unit is chosen |
@@ -812,15 +813,16 @@ phone: the chip truncates before the value does (see The unit chip below).
812
813
  | `required`, `disabled`, `placeholder`, `id`, `ref` | | | `required` applies to the number only; to require a unit, validate `unit` in the form schema. `placeholder`, `id` and `ref` reach the number input (React Hook Form's `field.ref` focuses it on an error) |
813
814
 
814
815
  - **Semantics:** a `<fieldset>` (role `group`) named by the label; the number
815
- is a textbox named after the label, the unit chip a button named "Unit: mg"
816
- (`aria-expanded`; it opens a popover), the menu a listbox named
817
- "Dose unit". The message describes the group, the number and the chip, and
818
- the number is `aria-invalid` on an error.
819
- - **Keyboard:** Tab goes value, unit chip, Decrease, Increase. Enter, Space or
816
+ is a textbox named after the label, the unit chip (when drawn) a button
817
+ named "Unit: mg" (`aria-expanded`; it opens a popover), the menu a listbox
818
+ named "Dose unit". The message describes the group, the number and the chip,
819
+ and the number is `aria-invalid` on an error. Without a chip there is no
820
+ unit button or listbox.
821
+ - **Keyboard:** Tab goes value, unit chip (when drawn), Decrease, Increase. Enter, Space or
820
822
  ArrowDown on the chip opens the menu (focus lands on the list, or on the
821
823
  search with `allowCustomUnit`); the arrows move, Enter picks, Esc closes and
822
824
  returns focus to the chip.
823
- - **The unit chip:** one focus ring for the whole box, whichever part has
825
+ - **The unit chip** (drawn only with `units` or `allowCustomUnit`; a plain `<QuantityField label="Count" />` has none, and Tab goes value, Decrease, Increase): one focus ring for the whole box, whichever part has
824
826
  focus. The chip is capped at 52% of the box and truncates a long label with
825
827
  an ellipsis (the full label is its `title`); the value keeps room for about
826
828
  4 digits at 360px. A coarse pointer gets 44px targets. Disabled, the chip
@@ -836,6 +838,57 @@ phone: the chip truncates before the value does (see The unit chip below).
836
838
 
837
839
  ---
838
840
 
841
+ ## QuantityStepper
842
+
843
+ ![QuantityStepper](../previews/form-fields-quantitystepper.webp)
844
+
845
+ A compact inline −/value/+ control for a cart or ticket line, where a
846
+ [`NumberField`](#numberfield) or [`QuantityField`](#quantityfield) (a labelled
847
+ box) is too heavy. It has no label and no border: a `group` named by your
848
+ `aria-label` ("Quantity of Orange juice") holding a minus button, the value and
849
+ a plus button. It is an uncontrolled or controlled number, not a form field.
850
+
851
+ ```tsx
852
+ import { QuantityStepper } from '@nxgt/material/components';
853
+ import { useState } from 'react';
854
+
855
+ export function Line() {
856
+ const [qty, setQty] = useState(2);
857
+ return (
858
+ <QuantityStepper
859
+ aria-label="Quantity of Orange juice"
860
+ value={qty}
861
+ onValueChange={setQty}
862
+ min={1}
863
+ max={20}
864
+ onRemove={() => removeLine()}
865
+ />
866
+ );
867
+ }
868
+ ```
869
+
870
+ | Prop | Type | Default | What it does |
871
+ | --- | --- | --- | --- |
872
+ | `aria-label` | `string` | required | Names the group and the value; the buttons are "Decrease …", "Increase …" and "Remove …" followed by it |
873
+ | `value` / `defaultValue` | `number` | `min` | Controlled / uncontrolled value |
874
+ | `onValueChange` | `(value: number) => void` | none | Called with the new value, always within `min` and `max` |
875
+ | `min`, `max` | `number` | `0`, none | The limits; the minus (or plus) button is `aria-disabled` at its limit |
876
+ | `step` | `number` | `1` | What a button or an arrow key adds; Page Up/Down use ten steps, Home/End jump to `min`/`max` |
877
+ | `size` | `'sm' \| 'md'` | `'md'` | 32px or 36px buttons; both are 44px under a coarse pointer |
878
+ | `editable` | `boolean` | `false` | The value is a text input: committed on Enter or blur (clamped; text that is not a number is dropped), Escape cancels; only Up/Down and Page Up/Down step it, Left/Right/Home/End move the caret |
879
+ | `onRemove` | `() => void` | none | At `min`, the minus button becomes a trash button named "Remove …" that calls this instead of stepping |
880
+ | `disabled` | `boolean` | `false` | Disables every control |
881
+
882
+ The value is a `spinbutton` (`aria-valuenow`, `aria-valuemin`, `aria-valuemax`)
883
+ in tabular figures, so the row does not shift as digits change. The stepper is
884
+ inline (`inline-flex`): put it in a `ListTile`'s `trailing` or `content`. The
885
+ text is translated through `material.number-field.*` and
886
+ `material.quantity-stepper.remove`.
887
+
888
+ **Storybook:** `Form fields/QuantityStepper` · **Vue:** later
889
+
890
+ ---
891
+
839
892
  ## CurrencyField
840
893
 
841
894
  ![CurrencyField](../previews/form-fields-currencyfield.webp)
@@ -670,7 +670,7 @@ import { ListDetailsLayout, PaneLayout, ThreadLayout } from '@nxgt/material/comp
670
670
  </ListDetailsLayout.Details>
671
671
  </ListDetailsLayout>
672
672
 
673
- <PaneLayout defaultOpen paneSide="right">
673
+ <PaneLayout defaultOpen="desktop" paneSide="right">
674
674
  <PaneLayout.Content>
675
675
  <PaneLayout.Trigger>Folders</PaneLayout.Trigger>
676
676
  {messages}
@@ -716,10 +716,25 @@ takes over. A page rendered only in the browser starts on the real layout.
716
716
  container and the flex row moves to an inner `data-slot="pane-layout-body"`.
717
717
  - `paneWidth`: `'sm'` (16rem), `'md'` (20rem, default) or `'lg'` (24rem) for
718
718
  the inline pane; no style attribute, so it holds under a strict CSP.
719
+ - `defaultOpen`: `true` / `false` for every viewport, `'desktop'` (open wide,
720
+ shut on a phone) or `{ desktop, phone }` (a missing key is shut). The state
721
+ follows the viewport until the user opens or closes the pane, so a cart that
722
+ starts open on desktop never opens its sheet over a phone, with no effect
723
+ and no flash: the server renders the desktop value and the inline pane is
724
+ hidden by CSS on a phone. With `phone="stack"` the pane has no overlay to
725
+ hide, so a stacked pane that is open on desktop only shows for one frame
726
+ before closing on a phone.
727
+ - `paneSide`: `'left'` (default) or `'right'`. The pane takes its side with a
728
+ CSS `order`, never by reversing the row, so the DOM and tab order are the
729
+ ones you write: put `PaneLayout.Content` first and the pane after it and
730
+ Tab reaches the content first, whichever side the pane is on.
731
+ Before this change a right pane written after the content sat on the left; check
732
+ screens that relied on it.
719
733
  - With `phone` or `breakpoint` set, the inline pane is an `<aside>` named by
720
734
  `sheetTitle` (default `Side Panel`), a landmark; the historical markup is a
721
735
  `div`. `usePaneLayout()` and the function children also get `phone`,
722
736
  `breakpoint` and `paneWidth`.
737
+
723
738
  `ThreadLayout` is a centred column over the app
724
739
  [background](#background) on the page ground (`bg-ground`, so a `bg-card` card stands apart) for focused flows (sign-in, onboarding), with no
725
740
  app bar. `width`, `stepperPlacement` and a sticky `footer` make it a centred form or a wizard with a side rail (see "A centred form or a wizard" below).
@@ -736,7 +751,8 @@ app bar. `width`, `stepperPlacement` and a sticky `footer` make it a centred for
736
751
  | `history` / `historyParam` | `'push' \| 'replace'` / `string` | — / `'view'` | `ListDetailsLayout`: keep the view in the URL (`?view=details`), through React Router |
737
752
  | `listWidth` / `resizable` / `onListWidthChange` | `number` / `boolean` / `(width) => void` | `320` / `false` | `ListDetailsLayout`: the list pane's width in px; `resizable` adds a keyboard and pointer separator, 240 to 480 px |
738
753
  | `back` | `boolean \| ReactNode` | — | `ListDetailsLayout.Details`: a bar with the way back to the list, on a phone only; `true` is the translated "Back to the list" (`material['list-details'].back`), a node is its label |
739
- | `open` / `defaultOpen` / `onOpenChange` | `boolean` | `false` | `PaneLayout`: pane shown |
754
+ | `open` / `onOpenChange` | `boolean` | `false` | `PaneLayout`: pane shown |
755
+ | `defaultOpen` | `boolean \| 'desktop' \| { desktop?, phone? }` | `false` | `PaneLayout`: initial pane state per viewport |
740
756
  | `paneSide` | `'left' \| 'right'` | `'left'` | `PaneLayout` |
741
757
  | `sheetTitle` | `string` | "Side Panel" (translated, `material['pane-layout']['sheet-title']`) | `PaneLayout.Pane`: screen-reader title of the sheet on phones |
742
758
  | `header` | `ReactNode` | — | `ThreadLayout`: above the column |
@@ -1223,6 +1239,8 @@ import { PanelCard } from '@nxgt/material/components';
1223
1239
 
1224
1240
  The card is a `role="region"` labelled by its title, so `getByRole('region', { name })` finds it. Children are the card's content (`CardContent`); with none, the card is its header only. It takes the `div`'s props, so `className` adds to it.
1225
1241
 
1242
+ The title is set in the card-title step (`title-sm` emphasized) whatever its heading level, one step below an `ActivityContent` page title (`title-md` emphasized), so an aside never outranks the page. The level only sets the document outline, not the size.
1243
+
1226
1244
  **Storybook:** `Navigation & layout/PanelCard` · **Vue:** later, [#2866](https://github.com/softistx/nxgt-material/issues/2866)
1227
1245
 
1228
1246
  ---
@@ -83,7 +83,7 @@ opens at once.
83
83
 
84
84
  ![Collapsible](../previews/primitives-collapsible.webp)
85
85
 
86
- A single show / hide region and its trigger. The content springs open to its height and folds away (clipped only while it moves, so a focus ring inside is never cut). Give `CollapsibleTrigger` a `variant` for a styled trigger with a turning chevron: `tonal`, a pill that squares its corners while open, or `text`.
86
+ A single show / hide region and its trigger. The content springs open to its height and folds away (clipped only while it moves, so a focus ring inside is never cut). Give `CollapsibleTrigger` a `variant` for a styled trigger with a turning chevron: `tonal`, a pill that squares its corners while open, or `text`. Add `flush` to a `text` trigger to drop its inline padding and take the parent's font, so a section disclosure inside `<Typography as="h3">` lines up with the headings around it.
87
87
 
88
88
  ```tsx
89
89
  import {
@@ -110,6 +110,33 @@ import {
110
110
  </Collapsible>;
111
111
  ```
112
112
 
113
+ A flush trigger inside a heading, aligned with the headings around it:
114
+
115
+ ```tsx
116
+ import {
117
+ Collapsible,
118
+ CollapsibleContent,
119
+ CollapsibleTrigger,
120
+ Typography,
121
+ } from '@nxgt/material/components';
122
+
123
+ <div className="flex w-80 flex-col gap-3">
124
+ <Typography as="h3" variant="title-sm" emphasized>
125
+ Question
126
+ </Typography>
127
+ <Collapsible>
128
+ <Typography as="h3" variant="title-sm" emphasized>
129
+ <CollapsibleTrigger variant="text" flush>
130
+ Details
131
+ </CollapsibleTrigger>
132
+ </Typography>
133
+ <CollapsibleContent className="text-muted-foreground text-sm">
134
+ Hidden text that opens under the heading.
135
+ </CollapsibleContent>
136
+ </Collapsible>
137
+ </div>;
138
+ ```
139
+
113
140
  Parts: `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent`.
114
141
 
115
142
  | Prop | Type | Default | Effect |
@@ -117,6 +144,7 @@ Parts: `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent`.
117
144
  | `defaultOpen` / `open` | `boolean` | `false` | whether the content shows, uncontrolled / controlled |
118
145
  | `onOpenChange` | `(open: boolean) => void` | — | called when the trigger toggles it |
119
146
  | `disabled` | `boolean` | `false` | the trigger does nothing |
147
+ | `flush` (`CollapsibleTrigger`) | `boolean` | `false` | with `variant="text"`, no inline padding and the parent's font: the label starts at the left edge and can sit in a heading; ignored on other variants |
120
148
  | `variant` (`CollapsibleTrigger`) | `'tonal' \| 'text'` | — | a styled trigger with a chevron; unset, it is unstyled as before (with `asChild`, the child takes the look and brings its own icon) |
121
149
 
122
150
  To keep the former instant toggle, pass `className="animate-none"` to
@@ -705,7 +733,7 @@ For sorting, paging and selection use `DataTable` on the
705
733
 
706
734
  ![Tabs](../previews/primitives-tabs.webp)
707
735
 
708
- Tabs whose triggers can take an icon, in two looks: `underline` (the default) and the `segmented` pill.
736
+ Tabs whose triggers can take an icon, in two looks: `underline` (the default) and the `segmented` pill. `<TabsList fullWidth>` on a `segmented` list fills its container and shares the width between the tabs in equal parts (a payment-method switch); `underline` ignores it.
709
737
 
710
738
  ```tsx
711
739
  import {
@@ -767,6 +795,19 @@ few short choices, such as a view switch.
767
795
  </Tabs>
768
796
  ```
769
797
 
798
+ **`fullWidth`** on a `segmented` `TabsList` makes the track fill its container,
799
+ the tabs sharing the width in equal parts, whatever their labels. It suits a
800
+ two-way switch such as a payment method.
801
+
802
+ ```tsx
803
+ <Tabs defaultValue="cash" variant="segmented">
804
+ <TabsList fullWidth aria-label="Payment method">
805
+ <TabsTrigger value="cash">Cash</TabsTrigger>
806
+ <TabsTrigger value="card">Card</TabsTrigger>
807
+ </TabsList>
808
+ </Tabs>
809
+ ```
810
+
770
811
  `orientation="vertical"` lays the tabs in a column, the indicator on their
771
812
  left side; the arrow keys become Up and Down.
772
813
 
@@ -825,6 +866,7 @@ as `segmented`. Both still type-check and render; write `underline` or
825
866
  | `variant` (`Tabs`) | `'underline' \| 'segmented' \| 'bar' \| 'line'` | `'underline'` | the look; `bar` and `line` are deprecated aliases of `underline` and `segmented` |
826
867
  | `defaultValue` / `value` (`Tabs`) | `string` | — | the selected tab, uncontrolled / controlled |
827
868
  | `onValueChange` (`Tabs`) | `(value: string) => void` | — | called when a trigger is selected |
869
+ | `fullWidth` (`TabsList`) | `boolean` | `false` | `segmented` only: the track fills its container and the tabs share the width in equal parts; `underline` ignores it |
828
870
  | `orientation` (`Tabs`) | `'horizontal' \| 'vertical'` | `'horizontal'` | a row or a column, and the arrow keys' direction |
829
871
  | `dir` (`Tabs`) | `'ltr' \| 'rtl'` | `'ltr'` | the reading direction: which way the tabs run and the arrow keys move |
830
872
  | `icon` (`TabsTrigger`) | `ReactNode` | — | shown before the label |
@@ -840,6 +840,61 @@ export function Topics() {
840
840
  }
841
841
  ```
842
842
 
843
+ <a id="chipgroupfield-tones"></a>
844
+
845
+ An option's `tone` colours its chip with the tones `Badge` takes (`primary`,
846
+ `secondary`, `tertiary`, `success`, `info`, `warning`, `error`, `neutral`, and
847
+ an app's own from `AppTones`), so a category filter can match the toned tiles
848
+ below it. An unchosen chip shows a soft tint of the tone with the tone's text
849
+ colour; a chosen chip shows the filled tone under its on-colour, both at AA
850
+ contrast in light and dark. The leading check, `aria-checked`, the roving
851
+ focus and the boundary of an unchosen chip are unchanged. No `tone` keeps the
852
+ neutral chip.
853
+
854
+ ```tsx
855
+ <ChipGroupField
856
+ label="Categories"
857
+ options={[
858
+ { value: 'drinks', label: 'Drinks', icon: 'glass', tone: 'info' },
859
+ { value: 'bakery', label: 'Bakery', tone: 'warning' },
860
+ { value: 'other', label: 'Other' },
861
+ ]}
862
+ value={value}
863
+ onValueChange={setValue}
864
+ />
865
+ ```
866
+
867
+ `overflow="scroll"` keeps the chips on one line that scrolls sideways (touch,
868
+ trackpad, the wheel; a focused chip scrolls into view), with a fade at each
869
+ edge that hides more chips and no page scroll. The roving focus and the
870
+ selection are the same. The default `overflow="wrap"` lets the chips flow onto
871
+ several lines; `columns` and `groups` draw a grid and ignore `overflow`.
872
+
873
+ `overflow="scroll"` for one sideways-scrolling line:
874
+
875
+ ```tsx
876
+ import { ChipGroupField } from '@nxgt/material/components';
877
+ import { useState } from 'react';
878
+
879
+ const subjects = ['Algebra', 'Biology', 'Chemistry', 'Drama', 'Economics', 'French'].map(
880
+ (label) => ({ value: label.toLowerCase(), label }),
881
+ );
882
+
883
+ export function Subjects() {
884
+ const [value, setValue] = useState<string[]>([]);
885
+ return (
886
+ <ChipGroupField
887
+ label="Subjects"
888
+ type="multiple"
889
+ overflow="scroll"
890
+ options={subjects}
891
+ value={value}
892
+ onValueChange={setValue}
893
+ />
894
+ );
895
+ }
896
+ ```
897
+
843
898
  Balanced by default, and the unbalanced fill:
844
899
 
845
900
  ```tsx
@@ -858,7 +913,7 @@ const four = ['a', 'b', 'c', 'd'].map((value) => ({ value, title: value.toUpperC
858
913
  | `value` / `onValueChange` | `string \| null` or `string[]` | — | the chosen value, by `type` |
859
914
  | `deselectable` | `boolean` | `false` | `single` only: pressing the chosen chip clears it |
860
915
  | `max` / `min` | `number` | — | `multiple` only: `max` blocks the other chips once reached and speaks "{count} of {max} chosen"; `min` is a hint only |
861
- | `options` | `{ value; label; icon?; count?; description?; disabled?; disabledReason? }[]` | — | the chips, in one flow; `count`: see [option counts](#option-counts) |
916
+ | `options` | `{ value; label; icon?; tone?; count?; description?; disabled?; disabledReason? }[]` | — | the chips, in one flow; `count`: see [option counts](#option-counts); `tone`: see [tones](#chipgroupfield-tones) |
862
917
  | `groups` | `{ label: string; options }[]` | — | titled sections instead of `options` (it wins when both are given): see [slot picker](#slot-picker-columns-groups-and-reasons) |
863
918
  | `columns` | `number \| { base?; sm?; md?; lg? }` | — | equal-width columns (1 to 12; one count per breakpoint, each holding to the next); Up and Down then move by row. Unset, the chips flow and wrap |
864
919
  | `showDisabledReason` | `boolean` | `true` | prints a disabled option's `disabledReason` below the whole group ("Label: reason"), so the chip row keeps its wrapping; `false` leaves it to the tooltip and the accessible description |
@@ -265,6 +265,16 @@ focus. Below 30rem of list width the "Sort:" prefix is visually hidden (the name
265
265
  stays, and the button's accessible name is still "Sort: Name, Ascending").
266
266
  With no sortable column there is no control, and with neither a sortable column nor a `select` column there is no list bar.
267
267
 
268
+ For a short list the band is noise: `showSort={false}` drops the button (on a
269
+ phone too) and leaves every column sortable, so a
270
+ `layout={{ phone: 'cards', wide: 'table' }}` keeps its header sorting on desktop.
271
+ A list with a `select` column keeps its bar for "Select all". `DataTable` only:
272
+ `ServerTable` does not pass it through.
273
+
274
+ ```tsx
275
+ <DataTable layout="cards" showSort={false} renderCard={…} … />
276
+ ```
277
+
268
278
  ## Columns
269
279
 
270
280
  Tiles are always one column. For cards, the list's own width, not the window's, picks the columns: one, two from 36rem,
@@ -84,9 +84,10 @@ A virtual `DataTable` (`virtual`) adds no `style` attribute either: the
84
84
  server renders its first rows as plain rows, and the spacer rows' heights
85
85
  are custom properties written through `style.setProperty` after mount,
86
86
  which `style-src-attr` does not govern (`StrictCspVirtual` renders one under
87
- `style-src-attr 'none'`). Its scroll region's `maxHeight` is still an inline
88
- `style` on the server, one of #596's (a `maxHeightClassName` for
89
- `DataTable`, as `DataGrid` has, is [#800](https://github.com/softistx/nxgt-material/issues/800)).
87
+ `style-src-attr 'none'`). Its scroll region's `maxHeight` is an inline
88
+ `style` on the server, one of #596's: give `maxHeightClassName`
89
+ (`maxHeightClassName="max-h-[28rem]"`, as `DataGrid` has) instead, and the
90
+ region has no `style` attribute.
90
91
 
91
92
  A keyline `Carousel` (`multi-browse`, `hero`) renders no `style` attribute
92
93
  either: its first frame comes from container-query CSS, and the masks that
@@ -1123,10 +1123,11 @@ A strict policy works with no `style-src-attr 'unsafe-inline'` for the grid:
1123
1123
  the CSSOM, which `style-src-attr` does not govern. A frozen column's drag
1124
1124
  rewrites `--dg-start-*` the same way.
1125
1125
  - A custom editor must follow the same rule: classes only.
1126
- - `DataTable`'s own `maxHeight` (which a virtual `DataTable` needs) is an
1127
- inline `style={{ maxHeight }}` on its scroll region, so it is not covered
1128
- by this list; the grid takes a class instead. A `maxHeightClassName` on
1129
- `DataTable` is proposed in [#800](https://github.com/softistx/nxgt-material/issues/800).
1126
+ - `DataTable`'s own `maxHeight` is an inline `style={{ maxHeight }}` on its
1127
+ scroll region, so it is not covered by this list; the grid takes a class
1128
+ instead. A virtual `DataTable` under a strict CSP takes
1129
+ `maxHeightClassName` (a class such as `max-h-[28rem]`, no inline style;
1130
+ [#800](https://github.com/softistx/nxgt-material/issues/800)).
1130
1131
 
1131
1132
  The grid's popups (the select's list, the date's calendar, the menus) are
1132
1133
  library components and add nothing to the policy: see the
@@ -41,7 +41,7 @@ import { TextField } from '@nxgt/material/components';
41
41
  | --- | --- |
42
42
  | `TextField` and the `*Field` text wrappers | `leading` / `trailing` icons and actions are muted with no well fill; a text unit (`"/ 20"`, a currency code) sits inline at the end in muted text. `CurrencyField` follows |
43
43
  | `NumberField`, `PercentField`, `CalculatorField` | one `role="group"` box; the value is end-aligned in tabular figures, the suffix hugs it, the Decrease and Increase buttons are a tinted round pair at the end (the `DurationField` look), no dividers |
44
- | `QuantityField`, `AgeField` | the value, the unit and the pair share one box; the unit is a chip that opens a menu |
44
+ | `QuantityField`, `AgeField` | the value, the unit and the pair share one box; the unit is a chip that opens a menu (no chip without `units` or `allowCustomUnit`) |
45
45
  | `NumberField variant="score"` | a big value with its maximum, no box at rest; see [the form fields page](../components/form-fields.md#numberfield) |
46
46
  | Date, time and duration fields | the shared focus ring, a dashed hairline when read-only, the recessed surface when disabled |
47
47
 
@@ -49,7 +49,7 @@ import { TextField } from '@nxgt/material/components';
49
49
 
50
50
  - The `NumberField` order is the input, Decrease, Increase (it was Decrease,
51
51
  input, Increase). With a `CalculatorField`: input, Open calculator,
52
- Decrease, Increase. A `QuantityField` is value, unit, Decrease, Increase.
52
+ Decrease, Increase. A `QuantityField` is value, unit (when it has one), Decrease, Increase.
53
53
  - The number box is a `group`. A test that queries an unnamed
54
54
  `getByRole('group')` now also matches it: name the one you mean.
55
55
  - A `QuantityField` unit is a button named "Unit: mg", not a `combobox`:
@@ -170,6 +170,14 @@ written at `=`.
170
170
  is not called). `settlement` is `{ status: 'paid', change }` (change may be
171
171
  `'0'`) or `{ status: 'short', due }`, as exact decimal text. A short payment is a
172
172
  settlement, not an error. **The app owns the charge**: the keypad only reports.
173
+ - The settlement row carries a tone: **success** while the payment covers the
174
+ total (change may be `0`), **warning** while an amount is still due, with the
175
+ figure emphasised (AA text on a tinted band, light and dark).
176
+ - The keys follow one scheme: digits neutral, the editing keys (C, backspace)
177
+ one tone, the commit key filled primary, the quick amounts tonal chips in a
178
+ two-column grid (an odd last chip takes the full row; amounts never
179
+ truncate). Without a decimal key the 0 key spans nine of the twelve columns,
180
+ the commit key the other three.
173
181
  - `total` must be a valid, non-negative amount (a number or exact decimal text);
174
182
  without one the commit key does nothing.
175
183
 
@@ -211,7 +219,7 @@ taller keys. Colours and shapes follow the theme and dark mode through
211
219
 
212
220
  - The keys are native buttons in one `role="group"` (one tab stop, arrow keys
213
221
  over the grid), at least 44px on a coarse pointer at 360px and 320px.
214
- - Each key's glyph is decoration; its name is spoken (Plus, Minus, Double zero,
222
+ - Each key's glyph is decoration; its name is spoken (Plus, Minus, "00, double zero",
215
223
  Clear all, Enter, "Amount €20.00" in English, "Amount 20,00 €" in French). `commitLabel` replaces the commit key's name.
216
224
  - Results and, in cash, the change or amount still due are announced in a
217
225
  polite live region; the display itself is not live, so typing is not read digit by digit.