@nxgt/material 3.72.0 → 3.74.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 (99) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +3 -1
  3. package/dist/{alert-dialog-A8K8zai1.js → alert-dialog-BfO_ri-N.js} +1 -1
  4. package/dist/{async-slice-B8933IHY.js → async-slice-roMAJVzG.js} +1 -1
  5. package/dist/{button-C8moFnIT.js → button-DgenqmpS.js} +1 -1
  6. package/dist/charts.js +3 -3
  7. package/dist/{chip-B6AY1E91.js → chip-CiDczNxO.js} +2 -2
  8. package/dist/{code-editor-DQZIKL54.js → code-editor-CohlhFsA.js} +5 -5
  9. package/dist/code-editor.js +5 -5
  10. package/dist/{code-language-items-8VL0aMKG.js → code-language-items-Cs8GLYDp.js} +2 -2
  11. package/dist/components/ui/display/seat-ring/seat-ring.css +26 -0
  12. package/dist/components/ui/feedback/feedback.css +2 -0
  13. package/dist/components/ui/feedback/moment/moment.css +22 -0
  14. package/dist/{components-EHdmrvV4.js → components-y18qSFzl.js} +14184 -13894
  15. package/dist/components.js +28 -28
  16. package/dist/{dialog-WuLTwxkr.js → dialog-AGsB3bv4.js} +4 -4
  17. package/dist/dnd.js +1 -1
  18. package/dist/{dropdown-menu-tadySeGH.js → dropdown-menu-CzR6JP31.js} +1 -1
  19. package/dist/editor.js +10 -10
  20. package/dist/{empty-state-ZYiFSSBQ.js → empty-state-DjHGMp5F.js} +2 -2
  21. package/dist/{field-blur-BYR0yE5L.js → field-blur-CdASxjL7.js} +1 -1
  22. package/dist/{field-blur-context-bEhByoK-.js → field-blur-context-kZ4SD7xu.js} +1 -1
  23. package/dist/{hooks-GpwD8iDu.js → hooks-Dqq8O3Az.js} +2 -2
  24. package/dist/hooks.js +2 -2
  25. package/dist/{hover-card-BowaI_ra.js → hover-card-BPKqte4a.js} +1 -1
  26. package/dist/i18n.js +3 -3
  27. package/dist/{icon-badge-B1B0Lg02.js → icon-badge-B9uvDoyY.js} +3 -3
  28. package/dist/{icon-button-BbGU17cb.js → icon-button-Cr-DyDGg.js} +1 -1
  29. package/dist/lib/components/ui/collections/filter/bar/filter-bar-classes.d.ts +2 -2
  30. package/dist/lib/components/ui/collections/filter/bar/filter-bar-view.d.ts +1 -1
  31. package/dist/lib/components/ui/collections/filter/facets/filter-facets.d.ts +1 -1
  32. package/dist/lib/components/ui/collections/filter/query/types.d.ts +6 -0
  33. package/dist/lib/components/ui/collections/filter/query/view-types.d.ts +1 -0
  34. package/dist/lib/components/ui/collections/filter/sections/chips/filter-chip-body.d.ts +1 -1
  35. package/dist/lib/components/ui/collections/filter/sections/filter-search.d.ts +1 -0
  36. package/dist/lib/components/ui/collections/filter/sections/filter-sections-classes.d.ts +8 -0
  37. package/dist/lib/components/ui/collections/server-table/partials/server-table-empty-page.d.ts +4 -0
  38. package/dist/lib/components/ui/collections/server-table/types.d.ts +7 -1
  39. package/dist/lib/components/ui/display/highlighted-text/highlight-ranges.d.ts +11 -0
  40. package/dist/lib/components/ui/display/highlighted-text/highlighted-text.d.ts +16 -0
  41. package/dist/lib/components/ui/display/highlighted-text/index.d.ts +2 -0
  42. package/dist/lib/components/ui/display/index.d.ts +1 -0
  43. package/dist/lib/components/ui/display/list-tile/list-tile.d.ts +10 -1
  44. package/dist/lib/components/ui/display/list-tile/partials/list-tile-box.d.ts +16 -0
  45. package/dist/lib/components/ui/display/list-tile/partials/list-tile-classes.d.ts +3 -1
  46. package/dist/lib/components/ui/display/list-tile/partials/list-tile-line.d.ts +1 -1
  47. package/dist/lib/components/ui/display/list-tile/partials/list-tile-row.d.ts +1 -1
  48. package/dist/lib/components/ui/display/seat-ring/types.d.ts +6 -0
  49. package/dist/lib/components/ui/feedback/index.d.ts +1 -0
  50. package/dist/lib/components/ui/feedback/moment/index.d.ts +1 -0
  51. package/dist/lib/components/ui/feedback/moment/moment-body.d.ts +9 -0
  52. package/dist/lib/components/ui/feedback/moment/moment.d.ts +33 -0
  53. package/dist/lib/components/ui/fields/choice-list/choice-list-control.d.ts +17 -0
  54. package/dist/lib/components/ui/fields/choice-list/choice-list-focus.d.ts +49 -0
  55. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  56. package/dist/lib/components/ui/overlays/command/command-dialog.d.ts +8 -1
  57. package/dist/lib/components/ui/selection/autocomplete/autocomplete-enter.d.ts +7 -3
  58. package/dist/lib/i18n/resources/en/en.d.ts +4 -0
  59. package/dist/lib/i18n/resources/en/material.json.d.ts +5 -1
  60. package/dist/lib/i18n/resources/fr/fr.d.ts +4 -0
  61. package/dist/lib/i18n/resources/fr/material.json.d.ts +5 -1
  62. package/dist/lib/i18n/resources/resources.d.ts +8 -0
  63. package/dist/lib/i18n/translator.d.ts +1 -1
  64. package/dist/lib.js +1 -1
  65. package/dist/main.js +42 -42
  66. package/dist/map.js +8 -8
  67. package/dist/material.css +1 -1
  68. package/dist/pdf-reader.js +1 -1
  69. package/dist/print.js +1 -1
  70. package/dist/{public-CFj2qn7l.js → public-DC9VQw4Z.js} +9 -9
  71. package/dist/{rich-code-block-B7Teg7-e.js → rich-code-block-DBSL9zEN.js} +5 -5
  72. package/dist/{sortable-provider-y5sFhRDj.js → sortable-provider-C4tk5esY.js} +19 -13
  73. package/dist/{spinner-C5Kb6MWu.js → spinner-B2MubVWb.js} +1 -1
  74. package/dist/theme-picker.js +2 -2
  75. package/dist/{theme-toggle-B1JZg7mU.js → theme-toggle-BInlu21G.js} +1 -1
  76. package/dist/{toolbar-items-m_1FTFPQ.js → toolbar-items-CRJSx1MU.js} +3 -3
  77. package/dist/{translate-material-D1ndrd7a.js → translate-material-pVlPJ1v4.js} +1 -1
  78. package/dist/{translator-CtEC_AQa.js → translator-BoYCward.js} +10 -2
  79. package/dist/{typography-prQHb3gI.js → typography-B6YMbCge.js} +1 -1
  80. package/dist/{use-chat-composer-DBiyNXvL.js → use-chat-composer-Cn6yaeO0.js} +1924 -1911
  81. package/dist/{use-heading-spy-Bzj5mIUZ.js → use-heading-spy-B_Z4ArDh.js} +4 -4
  82. package/dist/{use-material-translation-C6H0B4kE.js → use-material-translation-CIS2n3RB.js} +1 -1
  83. package/dist/{use-return-focus-CkUX1HFl.js → use-return-focus-DNQtADAR.js} +1 -1
  84. package/docs/components/data-display.md +58 -5
  85. package/docs/components/data-table-filter-and-lists.md +8 -3
  86. package/docs/components/feedback-and-status.md +40 -0
  87. package/docs/components/form-fields.md +16 -0
  88. package/docs/components/overlays-and-menus.md +2 -1
  89. package/docs/components/primitives.md +2 -1
  90. package/docs/components/selection-and-choice.md +4 -2
  91. package/docs/guides/i18n.md +4 -1
  92. package/docs/guides/theming.md +4 -0
  93. package/docs/previews/data-display-highlightedtext.webp +0 -0
  94. package/docs/roadmap.md +1 -0
  95. package/docs/troubleshooting.md +37 -0
  96. package/lib/components/ui/display/seat-ring/seat-ring.css +26 -0
  97. package/lib/components/ui/feedback/feedback.css +2 -0
  98. package/lib/components/ui/feedback/moment/moment.css +22 -0
  99. package/package.json +3 -3
@@ -1,8 +1,8 @@
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-A8K8zai1.js";
3
- import { a as l } from "./button-C8moFnIT.js";
4
- import { i as u } from "./translate-material-D1ndrd7a.js";
5
- import { t as d } from "./use-material-translation-C6H0B4kE.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-BfO_ri-N.js";
3
+ import { a as l } from "./button-DgenqmpS.js";
4
+ import { i as u } from "./translate-material-pVlPJ1v4.js";
5
+ import { t as d } from "./use-material-translation-CIS2n3RB.js";
6
6
  import { _ as f, g as p, h as m, n as h, v as g, y as _ } from "./number.utils-BG_eEt5g.js";
7
7
  import { useCallback as v, useEffect as y, useMemo as b, useRef as x, useState as S } from "react";
8
8
  import { Fragment as C, jsx as w, jsxs as T } from "react/jsx-runtime";
@@ -1,4 +1,4 @@
1
- import { i as e, t } from "./translate-material-D1ndrd7a.js";
1
+ import { i as e, t } from "./translate-material-pVlPJ1v4.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 { a as n, c as r, i, l as a, o, r as s, s as c } from "./dist-lmHmz_bu.js";
3
- import { C as l, S as u, w as d } from "./spinner-C5Kb6MWu.js";
3
+ import { C as l, S as u, w as d } from "./spinner-B2MubVWb.js";
4
4
  import { c as f, i as p, l as m, o as h, r as g, s as ee } from "./in-dialog-Vwkyp-M5.js";
5
5
  import { r as _ } from "./escape-owner-DpaP0lm1.js";
6
6
  import * as v from "react";
@@ -13,6 +13,7 @@ from `@nxgt/material/components`.
13
13
  | <img src="../previews/data-display-shape.webp" width="96" alt=""> | [Shape](#shape) | Material 3 Expressive's shapes as a backdrop, an icon container or a mask for a photo |
14
14
  | <img src="../previews/data-display-badge.webp" width="96" alt=""> | [Badge](#badge) | Status pill, 20 px, in seven tones, soft or outlined, with an optional decorative `icon` before the words |
15
15
  | <img src="../previews/data-display-countbadge.webp" width="96" alt=""> | [CountBadge](#countbadge) | Count in the corner of its children |
16
+ | <img src="../previews/data-display-highlightedtext.webp" width="96" alt=""> | [HighlightedText](#highlightedtext) | A string with the words of a search query in a themed `<mark>` |
16
17
  | <img src="../previews/data-display-dayseparator.webp" width="96" alt=""> | [DaySeparator](#dayseparator) | "Today", "Yesterday" or the date between two hairlines |
17
18
  | <img src="../previews/data-display-periodcards.webp" width="96" alt=""> | [PeriodCards](#periodcards) | Consecutive date ranges (terms, sprints) as cards, with a current marker and an optional link per card |
18
19
  | <img src="../previews/data-display-chip.webp" width="96" alt=""> | [Chip](#chip) | Three looks and a dashed add pill, with slots, a selected state and a dismiss button |
@@ -147,6 +148,7 @@ import { SeatRing } from '@nxgt/material/components';
147
148
  seats={[
148
149
  { id: 'ada', label: 'Ada Example', status: { label: 'Ready', tone: 'success' }, badge: 'Host' },
149
150
  { id: 'bo', label: 'Bo Sample', status: { label: 'Away', tone: 'warning' } },
151
+ { id: 'cy', label: 'Cy Demo', out: true },
150
152
  { id: 'free', label: 'Free', empty: true },
151
153
  ]}
152
154
  highlightedId="ada"
@@ -159,7 +161,7 @@ import { SeatRing } from '@nxgt/material/components';
159
161
 
160
162
  | Prop | Type | Default | Notes |
161
163
  | --- | --- | --- | --- |
162
- | `seats` | `Seat[]` | required | `{ id, label, avatar?: { src?, fallback? }, status?: { label, tone? }, badge?, disabled?, empty? }`; clockwise from the top |
164
+ | `seats` | `Seat[]` | required | `{ id, label, avatar?: { src?, fallback? }, status?: { label, tone? }, badge?, disabled?, out?, empty? }`; clockwise from the top |
163
165
  | `layout` | `'ring' \| 'grid'` | `'ring'` | `ring` falls back to the grid in a narrow box and past the seat limit (10 seats) |
164
166
  | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | seat width 4.75, 6 or 8rem; ring from a box of 32, 38 or 46rem; at most 10 seats on the circle |
165
167
  | `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'none'` | `none`: a list; otherwise a listbox |
@@ -183,6 +185,7 @@ no `style` attribute and a strict CSP is fine.
183
185
  keys, Home and End move the focus (an empty seat is skipped); Space or Enter
184
186
  picks, and pressing the chosen seat again clears it.
185
187
  - A seat is named by its label and its status text, never by colour alone.
188
+ - `out: true` marks an eliminated seat: it stays visible, dimmed, with the name struck through, the avatar greyed and the word "Out" (`material.seat-ring.out`, "Éliminé" in French) written in it, so the word is part of the seat's name (label, status, "Out", then the badge); it stays pickable unless `disabled`. An empty seat ignores it. Seat status words ("Eliminated") remain the app's own.
186
189
  - In a listbox, empty and `disabled` seats are `aria-disabled`, and a disabled
187
190
  seat can be reached but not picked (in a list, `disabled` only dims the seat).
188
191
  - Put no button in a selectable seat: interactive content (an "Invite" action
@@ -417,6 +420,35 @@ import { CountBadge, IconButton } from '@nxgt/material/components';
417
420
 
418
421
  ---
419
422
 
423
+ ## HighlightedText
424
+
425
+ ![HighlightedText](../previews/data-display-highlightedtext.webp)
426
+
427
+ A string with the words of a search query in a themed `<mark>`. Case, accents and ligatures are folded (`œ` is `oe`, `æ` is `ae`, `ß` is `ss`), and a query word matches at the start of a word, as a search engine's normaliser does.
428
+
429
+ ```tsx
430
+ import { HighlightedText, highlightRanges } from '@nxgt/material/components';
431
+
432
+ <HighlightedText text="Œuvres d'été" query="oeuvre ete" />
433
+ // <mark>Œuvre</mark>s d'<mark>été</mark>
434
+
435
+ // For a table or a custom renderer: offsets into the original text.
436
+ highlightRanges("Œuvres d'été", 'oeuvre ete'); // [[0, 5], [9, 12]]
437
+ ```
438
+
439
+ | Prop | Type | Default | Effect |
440
+ | --- | --- | --- | --- |
441
+ | `text` | `string` | — | the string to show |
442
+ | `query` | `string` | — | search words; a blank query marks nothing |
443
+ | `locale` | `string` | runtime's | language for lowercasing |
444
+ | span props | | | `className` and the other `span` props go to the wrapping `span` |
445
+
446
+ `highlightRanges(text, query, locale?)` is pure and returns sorted, non-overlapping `[start, end)` offsets. The marks use theme tokens (readable in dark mode) and the system `Mark` colours in forced colours.
447
+
448
+ **Storybook:** `Data display/HighlightedText`
449
+
450
+ ---
451
+
420
452
  ## DaySeparator
421
453
 
422
454
  ![DaySeparator](../previews/data-display-dayseparator.webp)
@@ -974,15 +1006,16 @@ A tile with an `href` ends with a discreet chevron after `trailing`: decorative
974
1006
  | `linkComponent` | `AppShellLinkComponent` | plain `<a>` | the link element, as in the app shell: takes `href`, spreads every other prop, forwards `ref`. Defaults to the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s link when one is set. |
975
1007
  | `onClick` | `MouseEventHandler` | — | renders a `<button>` |
976
1008
  | `selected` | `boolean` | `false` | the current row: drawn selected; a link gets `aria-current="page"` (your own `aria-current` replaces it), a button `aria-pressed` |
977
- | `toggle` | `Switch` props | — | makes the tile a switch row (see below): the whole row toggles a Switch at its end; `href`, `onClick`, `actions`, `chips`, `chevron` and `linkComponent` are refused beside it by type |
1009
+ | `toggle` | `Switch` props | — | makes the tile a switch row (see below): the whole row toggles a Switch at its end; `href`, `onClick`, `actions`, `chips`, `content`, `chevron` and `linkComponent` are refused beside it by type |
978
1010
  | `size` | `'sm' \| 'md'` | `'md'` | `sm` is the dense row (with `chips`, the record row, see below) |
979
1011
  | `leading`, `trailing`, `subtitle` | `ReactNode` | — | slots; `trailing` sits inside the link, so it holds decoration only |
980
1012
  | `actions` | `ReactNode` | — | buttons at the end of the tile, outside the link, vertically centred (`data-slot="item-actions"`) |
981
1013
  | `chips` | `ReactNode` | — | tags after the text, in the compact record row (see below); inside the link, so decoration only; part of the row's accessible name, keep labels short |
1014
+ | `content` | `ReactNode` | — | full-width content under the title row, inside the tile's box and outside its link or button: it may hold inputs and buttons (an expected value beside an editable field), and its clicks do not reach `onClick`. Not available on a `toggle` row |
982
1015
  | `variant` | `'tonal' \| 'divided'` | `'tonal'` | `divided`: flat rows with no tonal surface, a hover tint and the record-row layout; the hairline comes with `as="li"` |
983
1016
  | `inset` | `boolean` | `false` | With `variant="divided"`: 16px of inline padding (M3's list-item inset) in place of 4px, for a list inside a `Card`; the hairlines on the `<li>` still run edge to edge. Ignored by `tonal` |
984
1017
  | `as` | `'li'` | — | renders the tile inside a `<li>`, for a `<ul>` you own |
985
- | `disabled` | `boolean` | — | disables the link or button, and the `actions` with it |
1018
+ | `disabled` | `boolean` | — | disables the link or button, and the `actions` and `content` with it (both become inert) |
986
1019
 
987
1020
  With a router, pass `linkComponent` so a click navigates on the client instead of reloading the page (React Router's `Link` takes `to`, so wrap it):
988
1021
 
@@ -999,7 +1032,27 @@ export function SettingsLink() {
999
1032
  }
1000
1033
  ```
1001
1034
 
1002
- A link tile's `trailing` sits inside its `<a>`, which cannot hold buttons. Put edit / remove / menu buttons in `actions`: they render outside the link, in the tile's box, while the link still covers the whole tile (a click on the text navigates, a click on a button does not) and hover and selected styles stay on the tile. Tab goes to the link, then to the buttons. A tile without `href` renders `actions` the same way. With `actions`, `className`, `style` and `data-*` go on the tile's box and every other prop (`aria-label`, `id`, handlers…) on the link or button; `actions={null}` (or `false`) is no actions. and the text of a link tile with actions cannot be selected (the link is stretched over the tile).
1035
+ A link tile's `trailing` sits inside its `<a>`, which cannot hold buttons. Put edit / remove / menu buttons in `actions`: they render outside the link, in the tile's box, while the link still covers the whole tile (a click on the text navigates, a click on a button does not) and hover and selected styles stay on the tile. Tab goes to the link, then to the buttons, then to the controls of `content`. A tile without `href` renders `actions` the same way. With `actions`, `className`, `style` and `data-*` go on the tile's box and every other prop (`aria-label`, `id`, handlers…) on the link or button; `actions={null}` (or `false`) is no actions. and the text of a link tile with actions cannot be selected (the link is stretched over the tile).
1036
+
1037
+ **Content under the row.** `content` renders full-width under the title row, inside the tile's box and outside its link or button, so it may hold inputs and buttons (an expected value beside an editable field). Its clicks never reach the tile's `onClick`.
1038
+
1039
+ ```tsx
1040
+ <ul className="m-0 list-none p-0">
1041
+ <ListTile
1042
+ as="li"
1043
+ variant="divided"
1044
+ title="Item"
1045
+ subtitle="SKU-1 · LOT-2"
1046
+ actions={<IconButton label="Remove" onClick={remove}><Trash2 /></IconButton>}
1047
+ content={
1048
+ <label className="flex items-center justify-between gap-2">
1049
+ Expected 12
1050
+ <NumberField aria-label="Counted" value={counted} onChange={setCounted} />
1051
+ </label>
1052
+ }
1053
+ />
1054
+ </ul>
1055
+ ```
1003
1056
 
1004
1057
  ```tsx
1005
1058
  import { Pencil } from 'lucide-react';
@@ -1070,7 +1123,7 @@ function GuideList({ guides }: { guides: { to: string; title: string }[] }) {
1070
1123
  }
1071
1124
  ```
1072
1125
 
1073
- **A switch row.** `toggle` takes a Switch's props (`checked`, `defaultChecked`, `onCheckedChange`, `name`, `thumbIcons`…). The tile is then a `<label>` for the switch, so a press anywhere on the row toggles it; the switch is the one tab stop (Space or Enter), named by `title` and described by `subtitle`, and `disabled` on the tile disables it. A switch row is neither a link nor a button: `href`, `onClick`, `actions` and `chevron` do not apply, and `trailing` sits before the switch.
1126
+ **A switch row.** `toggle` takes a Switch's props (`checked`, `defaultChecked`, `onCheckedChange`, `name`, `thumbIcons`…). The tile is then a `<label>` for the switch, so a press anywhere on the row toggles it; the switch is the one tab stop (Space or Enter), named by `title` and described by `subtitle`, and `disabled` on the tile disables it. A switch row is neither a link nor a button: `href`, `onClick`, `actions`, `content` and `chevron` do not apply, and `trailing` sits before the switch.
1074
1127
 
1075
1128
  ```tsx
1076
1129
  const [on, setOn] = useState(true);
@@ -1018,7 +1018,7 @@ export default function Students() {
1018
1018
  table: [Cards and tiles](../guides/cards-and-tiles.md)).
1019
1019
 
1020
1020
  Its props are `ServerTableProps<T>`; `filter` is a `ServerTableFilter`,
1021
- `empty` a `ServerTableEmpty`, and `bulkActions` gets a
1021
+ `empty` an optional `ServerTableEmpty`, and `bulkActions` gets a
1022
1022
  `ServerTableSelection`.
1023
1023
 
1024
1024
  | Prop | Type | Default | Effect |
@@ -1045,7 +1045,7 @@ Its props are `ServerTableProps<T>`; `filter` is a `ServerTableFilter`,
1045
1045
  | `filterRef` | `Ref<ServerTableFilterHandle>` | — | with `filter`: `filterRef.current.focus()` focuses the Filter's first control, the search, else the first quick filter, "Add filter" or "Filters (N)" the width shows |
1046
1046
  | `sorting` | `createSorting(…)` | — | headers sorted from the URL; without it, no column is sortable (the server sorts, so pass `sorting` to let people sort), and the phone list bar shows only for selection |
1047
1047
  | `filters` | `tableFilters(model)` | — | without `filter`: "nothing matches" and its clear |
1048
- | `empty` | `ServerTableEmpty`: `{ none, noMatch, clear?, headingLevel?, iconStyle? }` | — | the empty page: `none` and `noMatch` are a title or a `ListEmptyPage` (`{ title, description?, illustration?, action? }`); `headingLevel` (default 2) is the level of its title and of the error's; `iconStyle` styles the scenes (see below) |
1048
+ | `empty` | `ServerTableEmpty`: `{ none, noMatch, clear?, headingLevel?, iconStyle? }` | the `DataTable` default ("No data available", or "No results" when filtered) | the empty page. Leave it out when your page answers "nothing yet" and "nothing matches" itself and renders the table only when there are rows; the failure with Retry and the page past the last show either way. When given: `none` and `noMatch` are a title or a `ListEmptyPage` (`{ title, description?, illustration?, action? }`); `headingLevel` (default 2) is the level of its title and of the error's; `iconStyle` styles the scenes (see below) |
1049
1049
  | `error` | `ServerTableError \| null` | — | the fetch failed: the failure and a Retry replace the rows |
1050
1050
  | `onRetry` | `() => unknown` | — | the error's Retry; a returned promise keeps it loading |
1051
1051
  | `onClearFilters` | `() => void` | focus the table | without `filter`, after "clear" |
@@ -1936,8 +1936,13 @@ function Notifications({ items }: { items: Notification[] }) {
1936
1936
  the loader's own; see [A field's default](#a-fields-default)),
1937
1937
  `resultCount`, `loading`; every other prop of `Filter variant="bar"`
1938
1938
  (`quick`, `counts`, `previewCount`, `fieldProps`, `density`, `shortcuts`,
1939
- `searchApply`, `searchMinLength`, `errors`, `label`...) passes through. There is no `query`,
1939
+ `searchApply`, `searchMinLength`, `searchAutoFocus`, `errors`, `label`...) passes through. There is no `query`,
1940
1940
  `defaultQuery` nor `onQueryChange`: the URL is the query.
1941
+ - **Focus on arrival.** `searchAutoFocus` puts focus in the search field when
1942
+ the bar mounts (React's `autoFocus`, so it is right on the server too). Use it
1943
+ on a search page reached with an empty query; leave it off on a list page,
1944
+ where it would take focus from the page. `/` still focuses the field. `ServerTable`'s `filter` does not take
1945
+ `searchAutoFocus`, on purpose: a list page should not steal focus.
1941
1946
  - **One write.** An edit writes the search, every `f=` and the aliased params,
1942
1947
  drops the `reset` ones and keeps the page's others (a tab, a sort).
1943
1948
  `onParamsChange` receives the next params for your router; the bar then
@@ -14,6 +14,7 @@ centre.
14
14
  | <img src="../previews/feedback-status-stepupdialog.webp" width="96" alt=""> | [StepUpDialog](#stepupdialog) | "Confirm it's you": a dialog that asks for a one-time code, driven by props |
15
15
  | <img src="../previews/feedback-status-unsaveddialog.webp" width="96" alt=""> | [UnsavedDialog](#unsaveddialog) | "Leave without saving?": a destructive confirm with Stay focused, used by the unsaved-changes guards |
16
16
  | <img src="../previews/feedback-status-stalenotice.webp" width="96" alt=""> | [StaleNotice](#stalenotice) | "This record changed": a warning that another write came first, with one Reload action and no overwrite |
17
+ | | [Moment](#moment) | A full-screen one-off announcement (a player is eliminated, a role revealed): illustration, title, description and action, dismissed by Escape, a tap, its action or a timer |
17
18
  | <img src="../previews/feedback-status-emptystate.webp" width="96" alt=""> | [EmptyState](#emptystate) | Illustration, title, description and action, by kind (empty, search, not-found, unauthorized) |
18
19
  | <img src="../previews/feedback-status-pageloadfailed.webp" width="96" alt=""> | [PageLoadFailed](#pageloadfailed) | An empty state for a page opened at a cursor that failed, with a link back to the start |
19
20
  | <img src="../previews/feedback-status-errorstate.webp" width="96" alt=""> | [ErrorState](#errorstate) | Failure block with Retry and technical details |
@@ -522,6 +523,45 @@ import { UnsavedDialog } from '@nxgt/material/components';
522
523
 
523
524
  ---
524
525
 
526
+ ## Moment
527
+
528
+ A transient full-screen announcement for a dramatic one-off event: a player is eliminated, a role is revealed. It is a modal dialog named by its `title` (and described by its `description`), so a screen reader announces it as it opens. Focus moves into it and goes back to the element that had it when it closes; the page behind is inert. Escape, a tap anywhere, the close button and the `action` dismiss it, and so does `duration`. It fades in and its picture pops in; under `prefers-reduced-motion` both stop. The surface is the tone's container with the foreground colour on it, so it holds AA in light and dark, and it fills a phone.
529
+
530
+ ```tsx
531
+ import { Moment } from '@nxgt/material/components';
532
+ import { Illustration } from '@nxgt/material/illustrations';
533
+
534
+ <Moment
535
+ open={eliminated != null}
536
+ onOpenChange={(open) => !open && setEliminated(null)}
537
+ duration={6000}
538
+ tone="error"
539
+ illustration={<Illustration scene="error" />}
540
+ title={`${eliminated?.name} was eliminated`}
541
+ description="The village chose her. She was the seer."
542
+ action={<Button onClick={nextNight}>Next night</Button>}
543
+ />
544
+ ```
545
+
546
+ | Prop | Type | Default | Effect |
547
+ | --- | --- | --- | --- |
548
+ | `open` | `boolean` | — | shown or not (controlled) |
549
+ | `onOpenChange` | `(open: boolean) => void` | — | called with `false` on every dismissal |
550
+ | `duration` | `number` (ms) | — | dismisses by itself after this long; unset: stays until dismissed |
551
+ | `illustration` | `ReactNode` | — | the picture (a scene, a pictogram, an icon); decorative, hidden from assistive technology |
552
+ | `title` | `ReactNode` | — | what happened; the accessible name |
553
+ | `description` | `ReactNode` | — | the line under it, in a `<p>` (phrasing content only) |
554
+ | `action` | `ReactNode` | — | a button or link; its click dismisses after its own handler, unless that handler calls `event.preventDefault()` |
555
+ | `tone` | `Tone` | `neutral` | the colour of the meaning: a named tone or one of your `AppTones` |
556
+ | `closeLabel` | `string` | translated | name of the close button |
557
+ | `className` | `string` | — | on the full-screen surface |
558
+
559
+ A tap anywhere dismisses, so keep text a reader must act on out of a short `duration`. Without a `description` the dialog is described by the hint `material.moment.hint` ("Press Escape or tap to dismiss."), read by assistive technology only. The close button is named by `material.buttons.close`.
560
+
561
+ **Storybook:** `Feedback & status/Moment` · **Vue:** not yet (a `vue` issue)
562
+
563
+ ---
564
+
525
565
  ## StaleNotice
526
566
 
527
567
  ![StaleNotice](../previews/feedback-status-stalenotice.webp)
@@ -375,6 +375,22 @@ up / down buttons, `min` and `max` rows (Remove is disabled at `min`, Add is
375
375
  announcements. Without a form library, `ChoiceListField` takes `value` and
376
376
  `onChange`.
377
377
 
378
+ **Focus.** `ChoiceListFormField` gives React Hook Form's `field.ref` a handle
379
+ that focuses the first refused row's text input: the row of the first
380
+ `choices.<i>.text` error (a resolver's), or the first blank row when the
381
+ built-in blank rule refused the list. An error on the list itself (too few
382
+ rows, no row marked) focuses its first control. So `shouldFocusError`,
383
+ `setFocus('choices')` and `setError('choices', …, { shouldFocus: true })` land
384
+ on the refused row when there is an error, and on the first control otherwise.
385
+ A row path works too: `setFocus('choices.1.text')` and
386
+ `setError('choices.1.text', …, { shouldFocus: true })` (a server's refusal)
387
+ focus that row's text input. The form needs no `[aria-invalid="true"]` lookup.
388
+
389
+ ```tsx
390
+ // After the server refused the second row:
391
+ form.setError('choices.1.text', { message }, { shouldFocus: true });
392
+ ```
393
+
378
394
  | Prop | Type | Default | What it does |
379
395
  | --- | --- | --- | --- |
380
396
  | `control`, `name` | `Control<T>`, `Path<T>` | — | the form and its array (`ChoiceListFormField`) |
@@ -641,7 +641,7 @@ in a desktop application.
641
641
 
642
642
  ![Command](../previews/overlays-menus-command.webp)
643
643
 
644
- Command palette with filtering, groups and shortcuts. `CommandDialog` shows it in a modal.
644
+ Command palette with filtering, groups and shortcuts. `CommandDialog` shows it in a modal, with a visible close button (`showCloseButton`, on by default; `false` removes it) at the end of the input row: named "Close" (`material.buttons.close`) and 44px on a coarse pointer, since a phone has no Escape key.
645
645
 
646
646
  ```tsx
647
647
  import {
@@ -700,6 +700,7 @@ children and they replace that, as before.
700
700
  | `shouldFilter` (`Command`) | `boolean` | `true` | `false` when you filter the items yourself (server search) |
701
701
  | `title` / `description` (`CommandDialog`) | `string` | `"Command Palette"` / `"Search for a command to run..."` (translated) | the dialog's title and description, inside the dialog and visually hidden |
702
702
  | `mobilePresentation` (`CommandDialog`) | `'dialog' \| 'top' \| 'fullscreen'` | `'top'` | below 640 px: pinned under the top edge (so the input and the results stay above the on-screen keyboard), the whole screen, or the centred modal it was before |
703
+ | `showCloseButton` (`CommandDialog`) | `boolean` | `true` | a visible close button at the end of the input row, named `material.buttons.close` and 44 px on a coarse pointer; `false` leaves Escape and the backdrop |
703
704
  | `commandProps` (`CommandDialog`) | `Pick<CommandProps, 'shouldFilter' \| 'value' \| 'onValueChange' \| 'loop' \| 'filter' \| 'label'>` | — | reaches the inner `Command`: `{ shouldFilter: false }` for a palette whose results come from a server (cmdk then does not hide them again by the typed text), `value` / `onValueChange` for the highlighted item, `label` for the input's accessible name. The input's sizing, the dialog's title and its focus stay as they are |
704
705
  | `open`, `onOpenChange` (`CommandDialog`) | as `Dialog` | — | every other prop goes to the `Dialog` |
705
706
 
@@ -163,7 +163,8 @@ import {
163
163
  Parts: `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`,
164
164
  `CardContent`, `CardFooter` — each a styled `<div>` taking its element's props,
165
165
  except `CardTitle`, a heading: an `h3` by default, another level or a `div`
166
- with `as` (`<CardTitle as="h2">`), so `getByRole('heading', { name })` finds it.
166
+ with `as` (`<CardTitle as="h2">`), so `getByRole('heading', { name })` finds it. The title takes its text size's
167
+ line height; pass a `leading-*` class to override.
167
168
  `CardAction` sits top-right of the header.
168
169
 
169
170
  **Storybook:** `Primitives/Card` · **Vue:** `Card` (same parts)
@@ -436,8 +436,10 @@ disabled with the field; Backspace in the empty input removes the last one.
436
436
  Enter in a form: it acts on the list and does not submit the form while it has
437
437
  something to do (WAI-ARIA combobox practice). It picks the highlighted option;
438
438
  with `freeSolo` and no option highlighted it adds the typed text; with the list
439
- open it is only default-prevented. On a closed list with nothing typed it
440
- submits, as a plain input does.
439
+ open it is only default-prevented. In `mode="multiple"` (a tags box, `freeSolo`
440
+ or not) Enter never submits the form, closed list and empty input included, so
441
+ the Enter after a pick does not save the form. In `single` mode, on a closed
442
+ list with nothing typed it submits, as a plain input does.
441
443
 
442
444
  An avatar in the rows and the chips:
443
445
  `getOptionLeading={(person) => <Avatar src={person.photo} alt="" />}`; see
@@ -312,7 +312,7 @@ title), `show` "Show results", `show-count` (an ICU plural over `count`:
312
312
  filters" (the list atop it); its Close is `material.buttons.close`.
313
313
 
314
314
  Smaller labels follow the language the same way: `CommandDialog`'s default
315
- title and description (`material.command.title` / `description`) and
315
+ title and description (`material.command.title` / `description`), its close button (`material.buttons.close`) and
316
316
  `CommandList`'s accessible name (`material.command.list`); the filter's empty state and
317
317
  `+N more` chip (`material.filter.empty` / `more`) and the Yes/No, From, Until
318
318
  and Up to of its chips (`material.filter.value.*`; `formatFilterValue` takes
@@ -578,6 +578,9 @@ places that use them read:
578
578
  | `material.table.labels.select-page` / `select-item` | Select all on this page / Select | Tout sélectionner sur cette page / Sélectionner | The cards list bar's select-all (its accessible name), and a card's checkbox (named "Select <title>"). Inside a `ServerTable`'s two-row phone header the select-all also shows `material.table.labels.select-all` ("Select all") as its short visible label |
579
579
  | `material.table.labels.clear-selection` | Clear selection | Effacer la sélection | The cards list bar's button, shown while rows are selected; on a narrow list it is an icon button, and this string is its accessible name and tooltip |
580
580
  | `material.table.labels.scroll-list` | Scrollable list | Liste défilante | The region of a cards or tiles list with `maxHeight` (`scrollLabel` overrides it) |
581
+ | `material.seat-ring.empty` | Empty seat | Place libre | `SeatRing`'s default empty seat (`renderEmptySeat` replaces it) |
582
+ | `material.seat-ring.out` | Out | Éliminé | the word written in a `SeatRing` seat with `out: true`, part of its accessible name |
583
+ | `material.moment.hint` | Press Escape or tap to dismiss. | Appuyez sur Échap ou touchez l'écran pour fermer. | `Moment`'s description for assistive technology when it has no `description` |
581
584
  | `material.command.empty` | No results | Aucun résultat | `CommandEmpty` with no children |
582
585
  | `material.pdf-reader.retry` | Retry | Réessayer | the `PdfReader` button |
583
586
 
@@ -1171,6 +1171,10 @@ are, without growing what is painted (nothing changes for a mouse):
1171
1171
  visible label (only an `aria-label`) is still its drawn size plus the
1172
1172
  `.nxgt-touch-target` hit area: wrap it in a `<label>` of 44px, or give it a
1173
1173
  label.
1174
+ - **A `FilterBar` chip is 44px tall and at least 44px wide**: the quick pills and toggles, the
1175
+ scope pill, an active condition's chip and the default hint (`height` and
1176
+ `min-width`, so a long label still grows it; a mouse keeps 24-28px), and
1177
+ "Add filter" and "Clear all" are 44px tall.
1174
1178
 
1175
1179
  A toast covering a control is out of scope: it physically hides what is under
1176
1180
  it.
package/docs/roadmap.md CHANGED
@@ -371,6 +371,7 @@ version they shipped in, and planned entries may name the major they target.
371
371
 
372
372
  ## Shipped
373
373
 
374
+ - **Moment (next minor)** — a transient full-screen announcement for a one-off event (a player is eliminated, a role is revealed): a modal dialog named by its title, with an illustration, description, action and tone, dismissed by Escape, a tap, its action or a timer, and no motion under reduced motion ([#2220](https://github.com/softistx/nxgt-material/issues/2220)).
374
375
  - **RecordActionsMenu (next minor)** — one menu for a record's lifecycle: archive with an Undo toast (or confirm first with `archivePolicy="confirm"`), restore, and delete that always confirms and shows the backend's refusal, with `ConfirmDialog` gaining `details` and `refusal` slots.
375
376
  - **Assistant approval card (next minor)** — `ChatActionProposal` shows an action an assistant proposes, with its status, deadline and Confirm / Reject, and `ChatToolActivity` is a compact row for a tool step in a chat thread ([#2134](https://github.com/softistx/nxgt-material/issues/2134)).
376
377
 
@@ -313,6 +313,7 @@ words in your error.
313
313
  - [A click on the next field right after an `Autocomplete` pick is lost](#a-click-on-the-next-field-right-after-an-autocomplete-pick-is-lost)
314
314
  - [An older search's results replace the newer ones in an `Autocomplete`](#an-older-searchs-results-replace-the-newer-ones-in-an-autocomplete)
315
315
  - [An `Autocomplete` option does nothing on click or Enter](#an-autocomplete-option-does-nothing-on-click-or-enter)
316
+ - [Enter in a multiple `Autocomplete` no longer submits the form](#enter-in-a-multiple-autocomplete-no-longer-submits-the-form)
316
317
  - ["The search failed. Try again." no longer shows in an `Autocomplete`](#the-search-failed-try-again-no-longer-shows-in-an-autocomplete)
317
318
  - [A test no longer finds `aria-invalid` on an `OtpField` slot](#a-test-no-longer-finds-aria-invalid-on-an-otpfield-slot)
318
319
  - [A test no longer finds `aria-invalid` on a `CodeEditor`](#a-test-no-longer-finds-aria-invalid-on-a-codeeditor)
@@ -4880,6 +4881,21 @@ on a desktop and on a phone alike: closing the picker with OK hands focus back
4880
4881
  to the trigger, and the next Tab or click elsewhere touches it. See the next
4881
4882
  entry.
4882
4883
 
4884
+ ### A failed submit focuses a `ChoiceListFormField` but not the refused row
4885
+
4886
+ **When:** a form with a `ChoiceListFormField` fails validation (a row's text
4887
+ is blank, `choices.2.text` in a resolver) and `document.activeElement` is the
4888
+ list's first control, or nothing, instead of the refused row. In versions
4889
+ before this fix.
4890
+ **Why:** the field gave React Hook Form a handle that focused the first
4891
+ control of the list, whichever row was refused.
4892
+ **Fix:** upgrade to the release that lists this fix in its changelog:
4893
+ `shouldFocusError` and `setFocus(name)` focus the first refused row's text
4894
+ input, and so do `setFocus('choices.1.text')` and
4895
+ `setError('choices.1.text', …, { shouldFocus: true })` on a row path, as a
4896
+ server's refusal sets. Remove a workaround that queries `[aria-invalid="true"]`
4897
+ after submit. For the other fields, see the next entry.
4898
+
4883
4899
  ### `setError(…, { shouldFocus: true })` or a failed submit does not focus the field
4884
4900
 
4885
4901
  **When:** a form wired with `*FormField`s: `handleSubmit` fails validation, or
@@ -5712,6 +5728,27 @@ may be chosen:
5712
5728
  />
5713
5729
  ```
5714
5730
 
5731
+ ### Enter in a multiple `Autocomplete` no longer submits the form
5732
+
5733
+ **When:** after upgrading, Enter in an `Autocomplete` (or `AutocompleteFormField`,
5734
+ a tags field) with `mode="multiple"` no longer submits the surrounding form
5735
+ when the list is closed and the input is empty.
5736
+ **Why:** the field is a tags box: the Enter that follows a pick used to save the
5737
+ whole form by surprise (#2199). Enter in `mode="multiple"` now only picks or
5738
+ adds. `single` mode is unchanged.
5739
+ **Fix:** submit with the form's button, or with a key handler of your own on the
5740
+ form if a keyboard shortcut is wanted (Ctrl/Cmd + Enter); drop any
5741
+ `onKeyDown` that prevented Enter on the combobox.
5742
+
5743
+ ```tsx
5744
+ <form
5745
+ onSubmit={save}
5746
+ onKeyDown={(e) => {
5747
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) e.currentTarget.requestSubmit();
5748
+ }}
5749
+ >
5750
+ ```
5751
+
5715
5752
  ### "The search failed. Try again." no longer shows in an `Autocomplete`
5716
5753
 
5717
5754
  **When:** after upgrading, an `Autocomplete` whose `onSearch` rejects shows
@@ -101,6 +101,32 @@
101
101
  border-style: dashed;
102
102
  background: transparent;
103
103
  }
104
+ /* Out: dimmed, the name struck through, the avatar greyed, and the word
105
+ * "Out" written under the name, so colour and opacity are never the only cue. */
106
+ .nxgt-seat-ring [data-out] {
107
+ background: color-mix(in oklab, var(--muted) 60%, transparent);
108
+ border-color: var(--border);
109
+ }
110
+ .nxgt-seat-ring [data-out] [data-slot="seat-ring-avatar"] {
111
+ opacity: 0.55;
112
+ filter: grayscale(1);
113
+ }
114
+ .nxgt-seat-ring [data-out] [data-slot="seat-ring-label"] {
115
+ color: var(--muted-foreground);
116
+ text-decoration: line-through;
117
+ text-decoration-thickness: 2px;
118
+ }
119
+ .nxgt-seat-ring [data-slot="seat-ring-out"] {
120
+ padding-inline: 0.375rem;
121
+ border: 1px solid currentColor;
122
+ border-radius: 9999px;
123
+ color: var(--muted-foreground);
124
+ font-size: 0.6875rem;
125
+ font-weight: 600;
126
+ line-height: 1.25rem;
127
+ text-transform: uppercase;
128
+ letter-spacing: 0.04em;
129
+ }
104
130
  .nxgt-seat-ring [data-highlighted] {
105
131
  border-color: var(--primary-indicator, var(--primary));
106
132
  box-shadow: 0 0 0 2px var(--primary-indicator, var(--primary));
@@ -1,4 +1,6 @@
1
1
  @import "./spinner/spinner.css";
2
+ /* Moment: the full-screen announcement. */
3
+ @import "./moment/moment.css";
2
4
  /* Illustration: the classes of the state scenes (EmptyState, ErrorState, @nxgt/material/illustrations). */
3
5
  @import "./state-frame/illustration.css";
4
6
  @import "./progress/progress.css";
@@ -0,0 +1,22 @@
1
+ /* Moment: a full-screen one-off announcement. The surface is the tone's
2
+ * container (the `data-tone` channel), the text the foreground colour, so it
3
+ * reads at AA in light and dark. It fades in and its picture pops in (the
4
+ * keyframes are dialog.css's `nxgt-fade-in` and the `nxgt-pop` of styles.css);
5
+ * under prefers-reduced-motion reduced-motion.css ends both at once (the
6
+ * content carries `data-slot`). */
7
+ @layer components {
8
+ .nxgt-moment {
9
+ background-color: var(--nxgt-tone-container, var(--background));
10
+ color: var(--foreground);
11
+ }
12
+ .nxgt-moment[data-state="open"] {
13
+ animation: nxgt-fade-in 240ms var(--nxgt-ease-effects) backwards;
14
+ }
15
+ .nxgt-moment[data-state="closed"] {
16
+ animation: nxgt-fade-out 160ms var(--nxgt-ease-emphasized-accelerate) both;
17
+ }
18
+ .nxgt-moment[data-state="open"] [data-slot="moment-illustration"] {
19
+ animation: nxgt-pop 520ms var(--nxgt-ease-emphasized-decelerate) 80ms
20
+ backwards;
21
+ }
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "3.72.0",
3
+ "version": "3.74.0",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"
@@ -372,7 +372,7 @@
372
372
  "docs:previews": "rm -rf node_modules/.cache/nxgt-preview-free/missing && NXGT_PREVIEW_FREE=1 vitest run --project previews && bun .storybook/preview-free-report.ts"
373
373
  },
374
374
  "dependencies": {
375
- "@nxgt/material-core": "^0.47.0",
375
+ "@nxgt/material-core": "^0.47.1",
376
376
  "@base-ui/react": "^1.6.0",
377
377
  "@dnd-kit/core": "^6.3.1",
378
378
  "@dnd-kit/sortable": "^10.0.0",
@@ -459,7 +459,7 @@
459
459
  "@fontsource-variable/inter": "^5",
460
460
  "@fortawesome/fontawesome-free": "7.3.1",
461
461
  "@nxgt/material-testing": "0.0.0",
462
- "@nxgt/playwright": "^0.9.7",
462
+ "@nxgt/playwright": "^0.11.0",
463
463
  "@react-leaflet/core": "^3.0.0",
464
464
  "@react-pdf/renderer": "^4.9.0",
465
465
  "@storybook/addon-a11y": "10.6.0",