@nxgt/material 3.80.0 → 3.82.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 (163) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +4 -3
  3. package/dist/{alert-dialog-DgHtXZuB.js → alert-dialog-DXLJ4AZP.js} +75 -74
  4. package/dist/{async-slice-YBQsGtQT.js → async-slice-BfVokA46.js} +1 -1
  5. package/dist/{button-_vm-xSBU.js → button-CSKUtMI3.js} +2 -2
  6. package/dist/{calculator-CTd_S23A.js → calculator-Cv00UQgR.js} +10 -10
  7. package/dist/calculator-field.js +6 -6
  8. package/dist/{calculator-segment-OI1BT3bA.js → calculator-segment-CwnC2cV_.js} +1 -1
  9. package/dist/calculator.js +1 -1
  10. package/dist/charts.js +3 -3
  11. package/dist/{chip-DumcVzUO.js → chip-BGn0V6Pt.js} +3 -3
  12. package/dist/{code-editor-CiwgfSOK.js → code-editor-B25-mWhb.js} +4 -4
  13. package/dist/code-editor.js +4 -4
  14. package/dist/{code-language-items-Dip0xnmc.js → code-language-items-oRjtO3qV.js} +2 -2
  15. package/dist/components/base-content.css +11 -0
  16. package/dist/components/components.css +2 -0
  17. package/dist/components/ui/collections/data-table/data-table-items-tiles.css +27 -1
  18. package/dist/components/ui/collections/data-table/data-table-items.css +15 -0
  19. package/dist/components/ui/display/tube-cap/tube-cap.css +87 -0
  20. package/dist/{components-CJFHhkZv.js → components-BmZk-git.js} +19258 -18882
  21. package/dist/components.js +37 -37
  22. package/dist/{conversion-variant-CqrPwPjC.js → conversion-variant-wZd-G6xR.js} +3 -3
  23. package/dist/{currency-field-CMFvctye.js → currency-field-DizBTAat.js} +4 -4
  24. package/dist/{date.utils-DUvqcjan.js → date.utils-JTEIo3mM.js} +98 -98
  25. package/dist/{dialog-B0iU3fmh.js → dialog-B-3s-oXt.js} +3 -3
  26. package/dist/dnd.js +1 -1
  27. package/dist/{dropdown-menu-CPOgcajv.js → dropdown-menu-C6rcYGB9.js} +1 -1
  28. package/dist/editor.js +987 -980
  29. package/dist/{empty-state-D3Jggupu.js → empty-state-CSkghnD9.js} +1 -1
  30. package/dist/{financial-variant-BdyfYhnp.js → financial-variant-igpGRbGw.js} +5 -5
  31. package/dist/{hooks-Z7KKbv4G.js → hooks-k8_f3B0k.js} +2 -2
  32. package/dist/hooks.js +9 -8
  33. package/dist/{hover-card-CFGIX6kg.js → hover-card-DuaVVKMb.js} +1 -1
  34. package/dist/i18n.js +3 -3
  35. package/dist/{icon-badge-kxGWUvcO.js → icon-badge-3ThP6EOl.js} +1 -1
  36. package/dist/{icon-button-BamxMwZs.js → icon-button-4PKlaI1g.js} +2 -2
  37. package/dist/lib/components/ui/actions/button/button-tones.d.ts +17 -16
  38. package/dist/lib/components/ui/collections/day-grouped-list/day-grouped-list.d.ts +6 -1
  39. package/dist/lib/components/ui/collections/filter/use-filter-field-config.d.ts +2 -0
  40. package/dist/lib/components/ui/collections/server-table/types-states.d.ts +5 -1
  41. package/dist/lib/components/ui/display/index.d.ts +1 -0
  42. package/dist/lib/components/ui/display/tube-cap/index.d.ts +2 -0
  43. package/dist/lib/components/ui/display/tube-cap/tube-cap.d.ts +27 -0
  44. package/dist/lib/components/ui/display/tube-cap/tube-cap.kinds.d.ts +7 -0
  45. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  46. package/dist/lib/components/ui/fields/inline-edit/inline-edit-display.d.ts +5 -1
  47. package/dist/lib/components/ui/fields/inline-edit/inline-edit-editor.d.ts +6 -1
  48. package/dist/lib/components/ui/fields/inline-edit/inline-edit.d.ts +14 -1
  49. package/dist/lib/components/ui/fields/inline-edit/use-inline-edit.d.ts +4 -1
  50. package/dist/lib/components/ui/fields/repeatable-group/partials/repeatable-group-row.d.ts +3 -1
  51. package/dist/lib/components/ui/fields/repeatable-group/partials/repeatable-group-rows.d.ts +2 -1
  52. package/dist/lib/components/ui/fields/repeatable-group/repeatable-group.d.ts +1 -1
  53. package/dist/lib/components/ui/fields/repeatable-group/types.d.ts +7 -0
  54. package/dist/lib/components/ui/layout/dashboard-grid/dashboard-grid.types.d.ts +6 -1
  55. package/dist/lib/components/ui/layout/dashboard-grid/dashboard-tile.d.ts +1 -1
  56. package/dist/lib/components/ui/media/galleries/gallery.d.ts +1 -1
  57. package/dist/lib/components/ui/media/galleries/gallery.utils.d.ts +14 -0
  58. package/dist/lib/components/ui/media/galleries/lightbox-gallery.d.ts +1 -1
  59. package/dist/lib/components/ui/media/galleries/mini-gallery.d.ts +1 -1
  60. package/dist/lib/components/ui/media/galleries/partials/gallery-tile-button.d.ts +12 -0
  61. package/dist/lib/components/ui/media/galleries/partials/gallery-tile.d.ts +3 -1
  62. package/dist/lib/components/ui/overlays/dialog/dialog-parts.d.ts +1 -1
  63. package/dist/lib/components/ui/overlays/sheet/sheet-parts.d.ts +5 -0
  64. package/dist/lib/components/ui/selection/choice-card-group/choice-card-balance.d.ts +4 -0
  65. package/dist/lib/components/ui/selection/choice-card-group/choice-card-group.d.ts +3 -1
  66. package/dist/lib/components/ui/selection/choice-card-group/choice-card-group.types.d.ts +7 -0
  67. package/dist/lib/components/ui/selection/sortable-list/partials/sortable-list-body.d.ts +5 -1
  68. package/dist/lib/components/ui/selection/sortable-list/partials/sortable-list-row-parts.d.ts +2 -1
  69. package/dist/lib/components/ui/selection/sortable-list/partials/sortable-list-row-props.d.ts +8 -5
  70. package/dist/lib/components/ui/selection/sortable-list/partials/use-owed-focus.d.ts +10 -0
  71. package/dist/lib/components/ui/selection/sortable-list/sortable-list-context.d.ts +2 -0
  72. package/dist/lib/components/ui/selection/sortable-list/sortable-list.keyboard.d.ts +1 -1
  73. package/dist/lib/components/ui/widgets/event-calendar/event-calendar-context.d.ts +3 -0
  74. package/dist/lib/components/ui/widgets/event-calendar/event-calendar-shell.d.ts +15 -0
  75. package/dist/lib/components/ui/widgets/event-calendar/event-calendar-template.d.ts +9 -0
  76. package/dist/lib/components/ui/widgets/event-calendar/event-calendar.d.ts +7 -3
  77. package/dist/lib/components/ui/widgets/event-calendar/event-calendar.template.d.ts +41 -0
  78. package/dist/lib/components/ui/widgets/event-calendar/index.d.ts +1 -0
  79. package/dist/lib/components/ui/widgets/event-calendar/partials/calendar-date-nav.d.ts +2 -0
  80. package/dist/lib/components/ui/widgets/event-calendar/partials/calendar-lane-toggle.d.ts +5 -0
  81. package/dist/lib/components/ui/widgets/event-calendar/partials/time-grid-header-template.d.ts +7 -0
  82. package/dist/lib/components/ui/widgets/event-calendar/template.types.d.ts +73 -0
  83. package/dist/lib/components/ui/widgets/event-calendar/types.d.ts +2 -0
  84. package/dist/lib/components/ui/widgets/event-calendar/use-chip-event.d.ts +1 -0
  85. package/dist/lib/components/ui/widgets/event-calendar/use-drawn-events.d.ts +4 -3
  86. package/dist/lib/components/ui/widgets/event-calendar/use-template-calendar.d.ts +18 -0
  87. package/dist/lib/components/ui/widgets/event-calendar/use-template-lane.d.ts +17 -0
  88. package/dist/lib/hooks/index.d.ts +1 -0
  89. package/dist/lib/i18n/resources/en/en.d.ts +2 -0
  90. package/dist/lib/i18n/resources/en/material.json.d.ts +2 -0
  91. package/dist/lib/i18n/resources/fr/fr.d.ts +2 -0
  92. package/dist/lib/i18n/resources/fr/material.json.d.ts +2 -0
  93. package/dist/lib/i18n/resources/resources.d.ts +4 -0
  94. package/dist/lib/i18n/translator.d.ts +1 -1
  95. package/dist/lib/illustrations-game/index.d.ts +4 -3
  96. package/dist/lib.js +2 -2
  97. package/dist/main.js +53 -52
  98. package/dist/map.js +7 -7
  99. package/dist/material.css +1 -1
  100. package/dist/pdf-reader.js +1 -1
  101. package/dist/{percent-field-C2LNr8NA.js → percent-field-QwU1l2Q4.js} +1 -1
  102. package/dist/{picker-surface-BUCFMoAZ.js → picker-surface-CiTycWgV.js} +4 -4
  103. package/dist/{popover-DFRwQF12.js → popover-YmbzbKQG.js} +1 -1
  104. package/dist/{pos-types-CXnEdjdy.js → pos-types-BFrapNmx.js} +1 -1
  105. package/dist/{pos-variant-DMwn_7is.js → pos-variant-CctfqieO.js} +4 -4
  106. package/dist/print.js +1 -1
  107. package/dist/{programmer-variant-Bo4eiDFi.js → programmer-variant-Dd0YOixn.js} +2 -2
  108. package/dist/{public-CyQPkkSR.js → public-CshR_aW7.js} +10 -10
  109. package/dist/{rich-code-block-iK7UcZdc.js → rich-code-block-BuJLDoBu.js} +5 -5
  110. package/dist/{scientific-variant-B9ScTgfx.js → scientific-variant-CjErQxuZ.js} +2 -2
  111. package/dist/{sortable-provider-BZwVjSVU.js → sortable-provider-DU2nvpFc.js} +2 -2
  112. package/dist/{spinner-RITOxvpH.js → spinner-DH1mm8hH.js} +1 -1
  113. package/dist/{tabs-DSo4eCUn.js → tabs-Um0SFyf1.js} +1 -1
  114. package/dist/theme-picker.js +2 -2
  115. package/dist/{theme-toggle-3gbzQX0x.js → theme-toggle-C-Ih4uQU.js} +1 -1
  116. package/dist/{toolbar-items-Bt5RK-Nx.js → toolbar-items-BoEY5bkt.js} +4 -4
  117. package/dist/{translate-material-CZTGqgen.js → translate-material-D4rn5UaQ.js} +1 -1
  118. package/dist/{translator-DTlOlIoq.js → translator-CcEgQ-H7.js} +4 -0
  119. package/dist/{use-chat-composer-DBY01IpX.js → use-chat-composer-BfFcmDua.js} +1439 -1391
  120. package/dist/{use-click-lock-BZYtTmVt.js → use-click-lock-Bwg_caHO.js} +14 -14
  121. package/dist/{use-heading-spy-Dt48bBts.js → use-heading-spy-gDNtK1sU.js} +19 -19
  122. package/dist/{use-material-translation-BJucC_xe.js → use-material-translation-DYrfV_JJ.js} +1 -1
  123. package/dist/{use-return-focus-BS1JLcnq.js → use-return-focus-C_Qz_yTk.js} +1 -1
  124. package/docs/README.md +1 -1
  125. package/docs/components/data-display.md +36 -8
  126. package/docs/components/data-table-filter-and-lists.md +7 -3
  127. package/docs/components/form-fields.md +6 -0
  128. package/docs/components/media-and-files.md +1 -0
  129. package/docs/components/navigation-and-layout.md +3 -1
  130. package/docs/components/overlays-and-menus.md +11 -5
  131. package/docs/components/rich-widgets.md +85 -1
  132. package/docs/components/selection-and-choice.md +49 -10
  133. package/docs/guides/forms.md +7 -0
  134. package/docs/guides/helpers.md +23 -0
  135. package/docs/guides/icon-motifs.md +1 -1
  136. package/docs/guides/illustrations.md +5 -5
  137. package/docs/guides/layouts.md +2 -2
  138. package/docs/guides/theming.md +7 -7
  139. package/docs/previews/data-display-tubecap.webp +0 -0
  140. package/docs/previews/feedback-status-illustration-game.webp +0 -0
  141. package/docs/previews/selection-choice-sortablelist.webp +0 -0
  142. package/docs/roadmap.md +3 -1
  143. package/docs/troubleshooting.md +50 -2
  144. package/lib/components/base-content.css +11 -0
  145. package/lib/components/components.css +2 -0
  146. package/lib/components/ui/collections/data-table/data-table-items-tiles.css +27 -1
  147. package/lib/components/ui/collections/data-table/data-table-items.css +15 -0
  148. package/lib/components/ui/display/tube-cap/tube-cap.css +87 -0
  149. package/lib/themes/ambre.css +1 -1
  150. package/lib/themes/ardoise.css +1 -1
  151. package/lib/themes/bleumono.css +1 -1
  152. package/lib/themes/bluemoon.css +1 -1
  153. package/lib/themes/corail.css +2 -2
  154. package/lib/themes/foret.css +1 -1
  155. package/lib/themes/lavande.css +1 -1
  156. package/lib/themes/menthe.css +2 -2
  157. package/lib/themes/ocean.css +1 -1
  158. package/lib/themes/pink.css +2 -2
  159. package/lib/themes/prune.css +1 -1
  160. package/lib/themes/sable.css +1 -1
  161. package/lib/themes/slate.css +1 -1
  162. package/lib/themes/teal.css +2 -2
  163. package/package.json +2 -2
@@ -839,6 +839,18 @@ export function Topics() {
839
839
  }
840
840
  ```
841
841
 
842
+ Balanced by default, and the unbalanced fill:
843
+
844
+ ```tsx
845
+ const four = ['a', 'b', 'c', 'd'].map((value) => ({ value, title: value.toUpperCase() }));
846
+
847
+ // Room for 3 columns, 4 cards: drawn 2+2, no orphan.
848
+ <ChoiceCardGroup label="Size" columns={3} options={four} value={value} onValueChange={setValue} />
849
+
850
+ // Every column that fits: 3+1.
851
+ <ChoiceCardGroup label="Size" columns={3} balance={false} options={four} value={value} onValueChange={setValue} />
852
+ ```
853
+
842
854
  | Prop | Type | Default | Effect |
843
855
  | --- | --- | --- | --- |
844
856
  | `type` | `'single' \| 'multiple'` | `'single'` | the value is `string \| null` or `string[]` |
@@ -945,8 +957,18 @@ check box at the end. `type="single"` (the default) is a radio group of
945
957
  card has a soft tint and a bar on its start edge; the mark says it too, so
946
958
  colour is never the only cue. The cards fill as many columns as fit (each at
947
959
  least 13.5rem wide), up to `columns`, so the layout follows the space the field
948
- has, not the window; there is no container query, so the group also keeps its
949
- cards inside a shrink-to-fit parent (an inline-flex box, a centred layout).
960
+ has, not the window. The rows are balanced: when the last row would hold one
961
+ orphan card (4 cards in room for 3), the group draws fewer columns and sits 2+2;
962
+ `balance={false}` fills every column that fits (3+1).
963
+
964
+ When it balances (the default, horizontal), the group is an inline-size
965
+ container (`@container`), which is how it measures its own width. A container's
966
+ width never comes from its content, so give the group a parent with a width: in
967
+ a shrink-to-fit parent (an `inline-flex` box, a float, a centred `w-fit` column)
968
+ it collapses to nothing. Add `w-full`, a `max-w-*` or a fixed width to that
969
+ parent, or pass `balance={false}`: with it (and with `orientation="vertical"`)
970
+ there is no container, and the group behaves as before in a shrink-to-fit
971
+ parent.
950
972
 
951
973
  ```tsx
952
974
  import { ChoiceCardGroup } from '@nxgt/material/components';
@@ -971,7 +993,8 @@ export function Plan() {
971
993
  | `deselectable` | `boolean` | `false` | `single` only: pressing the chosen card clears it |
972
994
  | `max` / `min` | `number` | — | `multiple` only: `max` blocks the other cards once reached and speaks "{count} of {max} chosen"; `min` is a hint only |
973
995
  | `options` | `{ value; title; description?; meta?; icon?; disabled?; disabledReason? }[]` | — | the cards |
974
- | `columns` | `1 \| 2 \| 3 \| 4` | `2` | the most columns; fewer when a card would be under 13.5rem (one on a phone) |
996
+ | `columns` | `1 \| 2 \| 3 \| 4` | `2` | the most columns; fewer when a card would be under 13.5rem (one on a phone). With `balance`, fewer also when it avoids an orphan: `columns={3}` and 4 cards draw 2+2 |
997
+ | `balance` | `boolean` | `true` | when the last row would hold one orphan card (4 cards in room for 3), draw fewer columns (2+2); by container width, CSS only. `false` fills every column that fits |
975
998
  | `density` | `'comfortable' \| 'compact'` | `'comfortable'` | the cards' padding |
976
999
  | `label`, `helperText`, `error`, `required`, `disabled`, `id` | as on `ChipGroupField` | — | the shared choice-group props: the group's name and help, its invalid and required states |
977
1000
  | `name` | `string` | — | put on every card (`ChoiceCardGroupFormField` passes its field `name`), so `document.getElementsByName(name)` finds the group's cards in order |
@@ -1113,11 +1136,19 @@ English without one.
1113
1136
 
1114
1137
  Searchable list you reorder by dragging or with the keyboard. The row under
1115
1138
  the pointer lifts into a card with a deep shadow, and its place in the list
1116
- stays as a dashed slot. From the keyboard, Space (or Enter) picks the focused
1117
- row up, the arrows move it, Space drops it and Escape puts it back; Ctrl/⌘ +
1118
- arrow moves it at once, Home / End jump. Each step is read out by a live
1119
- region ("Ada Lovelace moved to position 3 of 5."). With reduced motion the
1120
- drop does not animate.
1139
+ stays as a dashed slot. With reduced motion the drop does not animate.
1140
+
1141
+ The list is a `role="list"` of `listitem` rows, not a listbox: a row holds
1142
+ buttons of its own (grip, move, remove), which an `option` may not. Tab reaches
1143
+ the list once, on one row (a roving tab stop), and the arrows move the focus
1144
+ from row to row. A row of a disabled list is never a tab stop and carries
1145
+ `aria-disabled`. A long label keeps its full text in a `title`, and the grip is
1146
+ named after its item ("Drag: Ada Lovelace").
1147
+
1148
+ From the keyboard, Space (or Enter) on a row picks it up, the arrows move it,
1149
+ Space drops it and Escape puts it back; Ctrl/⌘ + arrow moves it at once,
1150
+ Home / End jump. Each step is read out by a live region ("Ada Lovelace moved to
1151
+ position 3 of 5."). A key pressed on a row's own button is that button's.
1121
1152
 
1122
1153
  ```tsx
1123
1154
  import { SortableList, type SortableListItem } from '@nxgt/material/components';
@@ -1144,7 +1175,7 @@ export function Ranking() {
1144
1175
  | `reorderable` | `boolean` | `false` | move up / down buttons |
1145
1176
  | `searchable` | `boolean` | `true` | filter box |
1146
1177
  | `height` | `number \| string` | `320` | list height |
1147
- | `required` | `boolean` | — | the asterisk, and `aria-required` on the `listbox`, which the label names |
1178
+ | `required` | `boolean` | — | the asterisk on the label (a `list` takes no `aria-required`) |
1148
1179
  | `hideLabel` | `boolean` | `false` | `true` keeps `label` as the list's accessible name (`aria-labelledby`) but draws nothing, for a heading that already titles it |
1149
1180
  | `numbered` | `boolean` | `false` | each row shows its rank in the whole order |
1150
1181
  | `removable` | `boolean` | `false` | a remove button on each row, and Delete: the row leaves the value |
@@ -1156,8 +1187,16 @@ export function Ranking() {
1156
1187
  where it is: no grip, not draggable, not movable from the keyboard, not
1157
1188
  removable).
1158
1189
 
1190
+ In a test, query it by role and name:
1191
+
1192
+ ```ts
1193
+ const list = screen.getByRole('list', { name: 'Ranking' });
1194
+ const rows = within(list).getAllByRole('listitem');
1195
+ await user.click(within(rows[0]).getByRole('button', { name: /^Move down/ }));
1196
+ ```
1197
+
1159
1198
  **Emits:** `string[]`, in the new order.
1160
- **Form wrapper:** `SortableListFormField` — `control` and `name` plus the same props, `hideLabel` included (the label's `required` asterisk is hidden with it; the `listbox` keeps `aria-required`), see
1199
+ **Form wrapper:** `SortableListFormField` — `control` and `name` plus the same props, `hideLabel` included (the label's `required` asterisk is hidden with it; the asterisk only: a `list` takes no `aria-required`, so a required list is described by a visually hidden "This field is required."), see
1161
1200
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
1162
1201
  **Storybook:** `Selection & choice/SortableList` · **Vue:** `SortableList`, `SortableListFormField`
1163
1202
 
@@ -262,6 +262,13 @@ in `[data-slot="form-actions"]`) and put each group of fields in a
262
262
  </form>
263
263
  ```
264
264
 
265
+ - The form grid insets its fields and its actions by 16px. In a container that
266
+ already pads its content (a settings page, a card, a sheet), set
267
+ `data-padding="none"` on the `.form-grid-layout`: the fields lose their
268
+ padding on all sides and the actions lose it left and right, so both sit
269
+ flush with that container's content edge:
270
+ `<form className="form-grid-layout" data-padding="none">`. The default is
271
+ unchanged.
265
272
  - Each section's title is an `h2` (pass `titleAs="h3"` when the form sits
266
273
  under an `h2` of your own), so a screen reader moves from section to
267
274
  section, and each section is a group named by its title.
@@ -126,6 +126,7 @@ Vue: the Redux helpers are Pinia store factories: `defineLocalStore`
126
126
  | `useIsClient()` | `false` on the server and first render, `true` after mount: a hydration guard. |
127
127
  | `useCspNonce()` | The nonce of the nearest `NonceProvider`, for a `<style>` of your own (`<style nonce={useCspNonce()}>`); `undefined` outside one, on the server and on the client alike. See [CSP](csp.md#nonceprovider). |
128
128
  | `useIsMobile()` | `true` below 768 px (`ResponsiveButton` no longer uses it: it collapses with CSS). `false` on the server and while hydrating, so SSR HTML is the desktop layout; a phone switches right after. |
129
+ | `useSsrMediaQuery(query, serverValue)` | Whether a media query matches, for a breakpoint `useIsMobile` (768 px) does not cover. The server and the hydrating client both render `serverValue` (`serverValue` is the answer the query gives on a desktop screen: `true` for `(min-width: …)`, `false` for `(max-width: …)`), and the client switches right after hydrating, so there is no hydration mismatch; a client-only render (SPA, client-side navigation) reads the real answer at once. Without `matchMedia` (jsdom) it answers `serverValue`. See [below](#usessrmediaquery). |
129
130
  | `useHeadingSpy(ids, onActive, options?)` | IntersectionObserver over heading ids; reports the topmost visible one. |
130
131
  | `useLocalizedDate()` | `{ language, locale, format }`: `DATE_UTILS.format` bound to the `I18nProvider` language (`en` without a provider). |
131
132
  | `useNumberFormatter()` | `{ formatNumber, formatCurrency }`: `Intl.NumberFormat` with the options you pass, bound to the `I18nProvider` language (`en-US` without a provider). For what `formatAmount` does not cover: percentages, units, compact notation. |
@@ -207,6 +208,28 @@ export function Sent({ email }: { email: string }) {
207
208
  `useFocusOnMount(open)` focuses again whenever `open` turns true, for content
208
209
  that stays mounted and is only shown. Storybook: `Guides/Hooks/useFocusOnMount`.
209
210
 
211
+ ### `useSsrMediaQuery`
212
+
213
+ ```tsx
214
+ import { useSsrMediaQuery } from '@nxgt/material/hooks';
215
+
216
+ // A panel that is open from 1280 px (xl, 80rem) and folded below.
217
+ function Inspector({ children }: { children: React.ReactNode }) {
218
+ const isXl = useSsrMediaQuery('(min-width: 80rem)', true);
219
+ return isXl ? <aside>{children}</aside> : null;
220
+ }
221
+ ```
222
+
223
+ The second argument is what the server renders, and what a hydrating client
224
+ renders on its first pass: it is the answer the query gives on a desktop
225
+ screen (`true` for `(min-width: …)`, `false` for `(max-width: …)`). On an SSR
226
+ page a narrower screen therefore paints the desktop markup first and switches right
227
+ after hydrating (one flash, and no hydration mismatch). A client-only render
228
+ (an SPA, a client-side navigation) reads the real answer from its first
229
+ render, with no flash. Where `matchMedia` does not exist (jsdom) the hook
230
+ answers `serverValue` and never subscribes. When CSS alone can do it
231
+ (`hidden xl:block`), prefer CSS: it has no flash at all.
232
+
210
233
  ### `useCountdown`
211
234
 
212
235
  The clock behind [`CountdownTimer`](../components/feedback-and-status.md#countdowntimer),
@@ -138,7 +138,7 @@ An `Illustration` draws a hero icon on a Material 3 Expressive plate, with two
138
138
  to four satellites. There are twelve base scenes (`empty`, `no-results`,
139
139
  `not-found`, `error`, `offline`, `success`, `welcome`, `forbidden`,
140
140
  `maintenance`, `invitation`, `sign-in`, `empty-inbox`) and two domain sets of
141
- ten, **health** and **education**, plus **game** (sixteen scenes for games and
141
+ ten, **health** and **education**, plus **game** (twenty-five scenes for games and
142
142
  events), each its own subpath so an app that does not import one does not carry it:
143
143
 
144
144
  <img src="../previews/feedback-status-illustration.webp" width="480" alt="The twelve base illustration scenes">
@@ -5,7 +5,7 @@ on a Material 3 Expressive plate, two to four satellite icons around it, a
5
5
  dashed ring and a few sparkles. There are twelve scenes: `empty`, `no-results`,
6
6
  `not-found`, `error`, `offline`, `success`, `welcome`, `forbidden`,
7
7
  `maintenance`, `invitation`, `sign-in` and `empty-inbox`, and two domain sets of
8
- ten more each, health and education, in their own entries ([health](#scene-sets-the-health-scenes) and [education](#scene-sets-the-education-scenes)), and sixteen for games ([game](#scene-sets-the-game-scenes)). The icons are
8
+ ten more each, health and education, in their own entries ([health](#scene-sets-the-health-scenes) and [education](#scene-sets-the-education-scenes)), and twenty-five for games ([game](#scene-sets-the-game-scenes)). The icons are
9
9
  [Font Awesome](https://fontawesome.com) sprites **your app serves** (see
10
10
  [Icons](../getting-started.md#icons)): the package ships no icon artwork, so it
11
11
  works with the sprites your licence covers. The [icon motifs guide](icon-motifs.md) shows them next to the icon backgrounds, the sky and the themes, and lists the suggested names that are Pro only, with Free neighbours.
@@ -148,12 +148,12 @@ icons: serve a Pro sprite, or set `edition="free"` on `IconSpritesProvider`, whi
148
148
 
149
149
  ## Scene sets: the game scenes
150
150
 
151
- `@nxgt/material/illustrations/game` holds sixteen scenes for a game, a quiz, a
151
+ `@nxgt/material/illustrations/game` holds twenty-five scenes for a game, a quiz, a
152
152
  community or an event app: `night-sky`, `dawn`, `village` (night),
153
- `day-village`, `graveyard`, seven character pictograms (`seer`, `healer`,
154
- `archer`, `cupid`, `wolf`, `villager`, `witch`), `victory`, `empty-table`,
153
+ `day-village`, `graveyard`, sixteen character pictograms (`seer`, `healer`,
154
+ `archer`, `cupid`, `wolf`, `villager`, `witch`, `protector`, `elder`, `village-idiot`, `scapegoat`, `raven`, `fox`, `siblings`, `big-bad-wolf`, `wolf-father`), `victory`, `empty-table`,
155
155
  `waiting-players` and `connection-lost`. It works exactly like the other sets,
156
- and they register side by side. Use them where a screen is empty, waiting,
156
+ and they register side by side. The `fox` and `scapegoat` heroes are drawn as vector art, because the icon sprites have no fox or goat; they follow the tone and dark mode like the sprite icons. Use them where a screen is empty, waiting,
157
157
  failed, celebrating or opening: a lobby with no players, a round that waits, a
158
158
  dropped connection, a winner, a night-phase hero. Keep them away from behind
159
159
  data.
@@ -204,8 +204,8 @@ The props of each are in
204
204
  The `Guides/Recipes` stories in Storybook are tested whole screens (the code
205
205
  ships with the `screen-recipes` skill of the `nxgt-material` plugin): a home
206
206
  dashboard, a list and detail, a form and dialog, a table and settings, a dense
207
- detail page, a status board and a landing page. Start from the closest one and
208
- replace the data.
207
+ detail page, a status board, a landing page and a search dialog with its
208
+ results page. Start from the closest one and replace the data.
209
209
 
210
210
  ## See also
211
211
 
@@ -68,7 +68,7 @@ at the top are needed is in [getting started](../getting-started.md#styles).
68
68
  | `--accent`, `--accent-foreground` | `bg-accent`, … | hover surfaces |
69
69
  | `--nxgt-accent` | `[--nxgt-accent:…]` | the mark colour of the selection controls: the fill, handle, state layer and stop dots of `Slider`, the on state of `Switch`, the box and dot of `Checkbox` and `Radio`. It defaults to `--primary` (`--primary-text` in dark) and is set per control, so override it on the control itself, not on `:root` |
70
70
  | `--error`, `--success`, `--info`, `--warning` (+ `-foreground`) | `bg-error`, `text-success`, … | the status colours of `Button`, `Alert`, `Badge`, toasts |
71
- | `--primary-text` | `text-primary-text` | primary drawn as text or an icon on a page, card or popover surface: `Typography tone="primary"`, links, the `Alert` primary icon, selected labels. In light, `--primary` mixed toward black: 96 % at most, solved lower on a tinted page so a tonal Button label on a striped row reaches 4.6:1 (94 % on teal, 89 % on corail, 94 % on pink); in dark, `--primary` mixed 60 % with white (`color-mix(in oklab, var(--primary) 40%, white)`), well over 4.5:1 on `--background` and `--card` |
71
+ | `--primary-text` | `text-primary-text` | primary drawn as text or an icon on a page, card or popover surface: `Typography tone="primary"`, links, the `Alert` primary icon, selected labels. In light, `--primary` mixed toward black: 96 % at most, solved lower on a tinted page so a tonal Button label on a striped row, or straight on the page ground, reaches 4.6:1 (92 % on teal, 88 % on corail, 92 % on pink, 95 % on menthe); in dark, `--primary` mixed 60 % with white (`color-mix(in oklab, var(--primary) 40%, white)`), well over 4.5:1 on `--background` and `--card` |
72
72
  | `--primary-indicator` | `bg-primary-indicator`, `border-primary-indicator`, `ring-primary-indicator`, `stroke-primary-indicator` | primary drawn as a non-text mark that carries a state (WCAG 1.4.11, 3:1): an active bar or dot, a selected or focused border, a ring, a spinner arc. Equal to `--primary` in light; in dark, `--primary` mixed 25 % with white (`color-mix(in oklab, var(--primary) 75%, white)`), 4.8:1 on `--background`, 4.5:1 on `--card`, 4.2:1 on `--muted` (`--primary` itself gives 2.7, 2.5 and 2.3:1) |
73
73
  | `--error-text` | `text-error-text` | error drawn as text: field messages, `HelperText` in its error state, form and dialog errors, `variant="error"` menu items |
74
74
  | `--destructive` | `bg-destructive`, `text-destructive` | deprecated alias of `--error`: the light theme sets it to `var(--error)`, the dark theme keeps its own value; use `--error` |
@@ -936,14 +936,14 @@ ones also take 10 % on focus, as on hover, so their label keeps 4.5:1 over it
936
936
 
937
937
  The `tonal`, `outlined` and `link` variants of `Button` and `IconButton` draw
938
938
  their text and icon in one tone per colour, read on `--background`, on
939
- `--card` and on the variant's own tint (15 % at most, at rest or focused). In
940
- the light theme `primary`, `secondary`, `info` and `success` use the token
941
- itself (5.8, 5.2, 5.2 and 5.0:1 at their lowest) and `warning` and `error` the
942
- token mixed with 15 % black (6.4 and 5.4:1). In the dark theme `primary` is
939
+ `--card`, on the page ground (`--ground`, a tinted page) and on the variant's own tint over each (15 % at most, at rest or focused). In
940
+ the light theme `primary` uses `--primary-text` (solved per theme), `secondary`, `info` and `success`
941
+ are mixed with 10 % black, `warning` with 15 % black and `error` with 20 % black (4.6:1 or more
942
+ on the page ground in every preset). In the dark theme `primary` is
943
943
  mixed with 60 % white, the `--primary-text` token (8.5:1 at its lowest: the token itself is a dark indigo,
944
944
  2.3:1), `success` and `warning` with 15 % white (5.3 and 6.5:1), `error` with
945
945
  30 % white (6.0:1), and `secondary` and `info` use the token (5.1 and 5.6:1).
946
- The tones are mixed from the tokens (`color-mix(in oklab, var(--color-error)
946
+ The tones are mixed from the tokens (`color-mix(in oklab, var(--color-warning)
947
947
  85%, black)`), so an override of a token moves its tone with it; a text colour
948
948
  of your own on one of those buttons (`className="text-primary-text"`) replaces the tone
949
949
  and is yours to check.
@@ -1012,7 +1012,7 @@ override on the root, or use a [theme zone](#themes), which re-declares them.
1012
1012
  for the same reason: the brand colour is a dark fill (it keeps near-white text
1013
1013
  readable on a button) and reads at only about 2:1 as text on the dark surfaces.
1014
1014
  The light value is `color-mix(in oklab, var(--primary) 96%, black)` at most (primary
1015
- 4 % toward black, 7.9:1 on the white page; the share is solved per theme and drops when a tinted page would leave a tonal Button label under 4.6:1 on a striped row, to 94 % on teal); the dark one is
1015
+ 4 % toward black, 7.9:1 on the white page; the share is solved per theme and drops when a tinted page would leave a tonal Button label under 4.6:1 on a striped row or on the page ground, to 92 % on teal); the dark one is
1016
1016
  `color-mix(in oklab, var(--primary) 40%, white)` (primary mixed 60 % with
1017
1017
  white, the recipe of the tonal `Button` labels). `Typography tone="primary"`,
1018
1018
  the `Alert` primary icon, links, selected labels and the other primary text of
package/docs/roadmap.md CHANGED
@@ -383,7 +383,9 @@ version they shipped in, and planned entries may name the major they target.
383
383
 
384
384
  ## Shipped
385
385
 
386
- - **Layouts: adaptive navigation, headers and page layouts (next minor)** — `ActivityLayout` takes `navigation` (a bottom bar with More and badges, a rail with a FAB or expanded, a standard or modal drawer, a collapsible sidebar, a top navigation with dropdowns, two-level navigation, a floating toolbar or none, chosen for phone, tablet and desktop) and `header` (small, center-aligned, medium and large collapsing on scroll, hide on scroll, a search bar that opens full screen on a phone, sticky tabs), and a route adds a back arrow with `handle.back`; each piece is also exported. Page layouts grow with it: an inspector pane and the selection in the URL for `ListDetailsLayout`, phone modes and a container breakpoint for `PaneLayout`, a centred form and wizard for `ThreadLayout`, a split `AuthCard`, spans and masonry for `ResponsiveGrid`, tabs for `Kanban` on a phone, `Doc` drawn inside the shell, and new `SettingsLayout`, `DashboardGrid` (tiles the user rearranges), `EditorLayout` and `LandingLayout` (with `LandingSection` and `SiteFooter`). Everything is opt-in; the shell's default is unchanged. [Layouts guide](guides/layouts.md) ([#2103](https://github.com/softistx/nxgt-material/issues/2103)). Follow-ups: a resize handle for `EditorLayout`'s properties panel and a separate open state for its phone sheet.
386
+ - **Search dialog and results page recipe (next patch)** — a command dialog (Ctrl/Cmd+K and a header button), debounced grouped suggestions, type chips, a conditional filter and highlighted result cards, with its code in the plugin's `screen-recipes` skill ([#2333](https://github.com/softistx/nxgt-material/issues/2333)).
387
+
388
+ - **Layouts: adaptive navigation, headers and page layouts (3.80.0)** — `ActivityLayout` takes `navigation` (a bottom bar with More and badges, a rail with a FAB or expanded, a standard or modal drawer, a collapsible sidebar, a top navigation with dropdowns, two-level navigation, a floating toolbar or none, chosen for phone, tablet and desktop) and `header` (small, center-aligned, medium and large collapsing on scroll, hide on scroll, a search bar that opens full screen on a phone, sticky tabs), and a route adds a back arrow with `handle.back`; each piece is also exported. Page layouts grow with it: an inspector pane and the selection in the URL for `ListDetailsLayout`, phone modes and a container breakpoint for `PaneLayout`, a centred form and wizard for `ThreadLayout`, a split `AuthCard`, spans and masonry for `ResponsiveGrid`, tabs for `Kanban` on a phone, `Doc` drawn inside the shell, and new `SettingsLayout`, `DashboardGrid` (tiles the user rearranges), `EditorLayout` and `LandingLayout` (with `LandingSection` and `SiteFooter`). Everything is opt-in; the shell's default is unchanged. [Layouts guide](guides/layouts.md) ([#2103](https://github.com/softistx/nxgt-material/issues/2103)). Follow-ups: a resize handle for `EditorLayout`'s properties panel and a separate open state for its phone sheet.
387
389
 
388
390
  - **Moment (3.74.0)** — 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), [#2247](https://github.com/softistx/nxgt-material/pull/2247)). A `modal={false}` variant for live events (focus stays, taps pass through, polite status) follows in the next minor ([#2313](https://github.com/softistx/nxgt-material/issues/2313)).
389
391
  - **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.
@@ -78,6 +78,8 @@ words in your error.
78
78
  - [`ActivityLayout`'s `backdrop` shows nothing, or the blobs are still there](#activitylayouts-backdrop-shows-nothing-or-the-blobs-are-still-there)
79
79
  - [Shapes of an `ActivityLayout` `backdrop` run behind the text of the chrome or a list](#shapes-of-an-activitylayout-backdrop-run-behind-the-text-of-the-chrome-or-a-list)
80
80
  - [Shaped Gallery tiles are plain rounded squares](#shaped-gallery-tiles-are-plain-rounded-squares)
81
+ - [ChoiceCardGroup shows 2+2 instead of 3+1](#choicecardgroup-shows-22-instead-of-31)
82
+ - [A `ChoiceCardGroup` collapses to nothing in an inline-flex or centred parent](#a-choicecardgroup-collapses-to-nothing-in-an-inline-flex-or-centred-parent)
81
83
  - [A component with my own tone (Shape, Fab, Avatar, StatCard, a metric card) shows no colour, or `tone` is a type error](#a-component-with-my-own-tone-shape-fab-avatar-statcard-a-metric-card-shows-no-colour-or-tone-is-a-type-error)
82
84
  - [A test expects the old `bg-muted` Avatar fallback, or a filled fallback shows a grey ring](#a-test-expects-the-old-bg-muted-avatar-fallback-or-a-filled-fallback-shows-a-grey-ring)
83
85
 
@@ -341,6 +343,8 @@ words in your error.
341
343
  - [An `Autocomplete` list covers its helper text, or a chip test no longer finds its markup](#an-autocomplete-list-covers-its-helper-text-or-a-chip-test-no-longer-finds-its-markup)
342
344
  - [A `CountryField` lost its chip, or its `renderOption` or `filter` does nothing](#a-countryfield-lost-its-chip-or-its-renderoption-or-filter-does-nothing)
343
345
  - [A `CountryField` or `TimezoneField` test no longer finds its field or its options](#a-countryfield-or-timezonefield-test-no-longer-finds-its-field-or-its-options)
346
+ - [A test that queries a `SortableList` by `listbox` or `option` finds nothing](#a-test-that-queries-a-sortablelist-by-listbox-or-option-finds-nothing)
347
+ - [axe reports `nested-interactive` on a list whose rows hold buttons](#axe-reports-nested-interactive-on-a-list-whose-rows-hold-buttons)
344
348
  - [A `SearchField` lost the height, radius or fill set through `[&_input]`](#a-searchfield-lost-the-height-radius-or-fill-set-through-_input)
345
349
  - [`ComboboxEmpty`'s text shows under the options of a `Combobox`](#comboboxemptys-text-shows-under-the-options-of-a-combobox)
346
350
  - [`ConfirmDialog` buttons stay in English](#confirmdialog-buttons-stay-in-english)
@@ -1744,6 +1748,23 @@ page.locator('[data-slot=chip]').getByRole('button', { name: 'Remove Design' });
1744
1748
  **Why:** the shapes live in an opt-in stylesheet, `@nxgt/material/galleries.css`, which `styles.css` does not import. Without it a shaped tile keeps the `xl` corner (`--nxgt-shape-corner-xl`, 28px). The same fallback shows in a browser without CSS [`shape()`](https://developer.mozilla.org/en-US/docs/Web/CSS/basic-shape/shape) (Chromium-based and Safari today), which drops the `clip-path`. Under `prefers-reduced-motion` the shapes show but do not morph, by design. Also check that nothing overrides `clip-path` on `[data-slot=gallery-tile-media]`.
1745
1749
  **Fix:** import it once, next to `styles.css`: `@import "@nxgt/material/galleries.css";` (or `import '@nxgt/material/galleries.css'`). For a browser without `shape()`, accept the rounded fallback, or use a non-shaped preset and pass `shape` only where you want it.
1746
1750
 
1751
+
1752
+ ### ChoiceCardGroup shows 2+2 instead of 3+1
1753
+
1754
+ **When:** a `ChoiceCardGroup` with `columns={3}` (or 4) and 4 cards (or any count that leaves one card alone on the last row) draws two rows of two, where it used to draw three and one.
1755
+ **Why:** rows are balanced by default (`balance`, `true`): when the last row would hold one orphan card and fewer columns avoid it, the group draws fewer columns. It is decided by CSS container queries on the group's own width, so the server and the client print the same markup.
1756
+ **Fix:** keep it, or restore the old fill with `balance={false}`:
1757
+
1758
+ ```tsx
1759
+ <ChoiceCardGroup label="Size" columns={3} balance={false} options={options} />
1760
+ ```
1761
+
1762
+ ### A `ChoiceCardGroup` collapses to nothing in an inline-flex or centred parent
1763
+
1764
+ **When:** the group sits in an `inline-flex` box, a float or a `w-fit` column and its cards are squeezed to zero width or the label shows alone.
1765
+ **Why:** a balanced group (the default, horizontal) is an inline-size container (`@container`), so that it can balance its rows; a container's width does not come from its content, so a shrink-to-fit parent gives it none. With `balance={false}`, or `orientation="vertical"`, there is no container.
1766
+ **Fix:** give the parent a width (`w-full`, a `max-w-*` or a fixed width), centring it with margins rather than shrink-to-fit, or pass `balance={false}` to restore the previous layout.
1767
+
1747
1768
  ### A component with my own tone (Shape, Fab, Avatar, StatCard, a metric card) shows no colour, or `tone` is a type error
1748
1769
 
1749
1770
  **When:** `<Shape tone="maths" />`, `<Fab tone="maths" />`, an `Avatar`, a `StatCard` or a metric card with `tone="maths"` draws no colour, or TypeScript reports:
@@ -3272,6 +3293,9 @@ only in the browser reads the real answer from its first render, as before.
3272
3293
  `Stepper` logged this line until 2.2.0, and a desktop hydrating its server
3273
3294
  HTML showed the mismatch (it asks whether the screen is wide); upgrade and
3274
3295
  drop any `clientOnly(Stepper)` you added for it.
3296
+ For a breakpoint of your own, use
3297
+ [`useSsrMediaQuery(query, serverValue)`](guides/helpers.md#usessrmediaquery)
3298
+ instead of `useMedia` or a hand-written `useSyncExternalStore`.
3275
3299
 
3276
3300
  ### `useMatches must be used within a data router`
3277
3301
 
@@ -5105,7 +5129,7 @@ append({ name: '' }, { shouldFocus: false });
5105
5129
  `aria-disabled`, still focusable, and does nothing by design; the reason, if
5106
5130
  `removeDisabledReason` gave one, is its tooltip, shown on hover, focus, tap and
5107
5131
  click, and its description), or `min` is
5108
- reached (the button is `disabled`).
5132
+ reached (the button is `disabled`, or absent with `removeAtMin="hide"`).
5109
5133
  **Fix:** return `true` from `removable` for a row that may go, and say why for
5110
5134
  the others, so a user is told instead of left guessing:
5111
5135
 
@@ -5202,7 +5226,8 @@ The `*` is now drawn by a CSS pseudo-element with an empty `content`
5202
5226
  field's own `required` / `aria-required` says it is required, and every field
5203
5227
  of this library sets it on the element a screen reader lands on (the input, the
5204
5228
  picker's combobox, the select's trigger, the slider thumbs, the rating's
5205
- radiogroup, the editor's textbox, the list's listbox, or, for a file picker,
5229
+ radiogroup, the editor's textbox, the `TransferList`'s listbox (a `SortableList` is a `list`
5230
+ and only draws the asterisk, with no `aria-required`), or, for a file picker,
5206
5231
  a group described as "This field is required."). A `SelectField`'s trigger is
5207
5232
  now named by its label (`aria-labelledby`), so `getByRole('combobox', { name:
5208
5233
  'Framework' })` finds it.
@@ -6353,6 +6378,29 @@ expect(screen.getByRole('combobox', { name: 'Time zone' })).toHaveValue('Paris')
6353
6378
  screen.getByRole('option', { name: 'Paris GMT+2' });
6354
6379
  ```
6355
6380
 
6381
+ ### A test that queries a `SortableList` by `listbox` or `option` finds nothing
6382
+
6383
+ **When:** a test queries a `SortableList` by `listbox`, `option` or `aria-selected`, after upgrading.
6384
+ **Why:** a row holds buttons of its own (grip, move, remove), which an `option`
6385
+ may not, so axe reported `nested-interactive`. The list is now a `role="list"`
6386
+ of `listitem` rows; `aria-selected`, `aria-activedescendant` and `aria-required`
6387
+ are gone (`aria-disabled` stays on a disabled row).
6388
+ **Fix:**
6389
+
6390
+ ```ts
6391
+ const list = screen.getByRole('list', { name: 'Ranking' });
6392
+ const rows = within(list).getAllByRole('listitem');
6393
+ ```
6394
+
6395
+ ### axe reports `nested-interactive` on a list whose rows hold buttons
6396
+
6397
+ **When:** axe flags a list whose rows hold buttons or other controls.
6398
+ **Why:** a row with `role="option"` (or `button`, `tab`...) may not contain
6399
+ focusable controls: assistive technology reads its children as text.
6400
+ **Fix:** make the row a `listitem` of a `list` (a roving tab stop on the row,
6401
+ the buttons keep their own names), as `SortableList` does. Do not exclude the
6402
+ list from the axe run.
6403
+
6356
6404
  ### An input inside an `InputGroup` lost a class or a style of `Input`
6357
6405
 
6358
6406
  **When:** a test or a style relies on a class of `Input` or `Textarea`
@@ -162,6 +162,17 @@
162
162
  [data-slot="form-actions"] {
163
163
  @apply sticky bottom-0 z-10 flex w-full justify-end gap-2 bg-background/80 border-t-8 border-transparent px-4 pb-[max(1rem,env(safe-area-inset-bottom))] backdrop-blur-sm max-sm:*:flex-1;
164
164
  }
165
+ /* `data-padding="none"`: the form sits flush in a container that
166
+ already pads it (a page, a card, a sheet): no inset on the fields or
167
+ the actions, so both align with the container's content edge. */
168
+ &[data-padding="none"] {
169
+ [data-slot="form-fields"] {
170
+ @apply p-0;
171
+ }
172
+ [data-slot="form-actions"] {
173
+ @apply px-0;
174
+ }
175
+ }
165
176
  }
166
177
 
167
178
  /* No height cap and no scroll of its own: the page (or the overlay) is
@@ -144,6 +144,8 @@
144
144
  @import "./ui/display/seat-ring/seat-ring-ring.css";
145
145
  /* VoteTally: ranked bars with counts, shares and voters. */
146
146
  @import "./ui/display/vote-tally/vote-tally.css";
147
+ /* TubeCap: a specimen tube chip, cap colours fixed by the tube convention. */
148
+ @import "./ui/display/tube-cap/tube-cap.css";
147
149
  /* SecretCard: a face that is not rendered until it is revealed. */
148
150
  @import "./ui/display/secret-card/secret-card.css";
149
151
  /* Alert and Banner span the row in the form grids (#1497). */
@@ -65,8 +65,34 @@
65
65
  ) {
66
66
  outline-offset: -2px;
67
67
  }
68
- /* The checkbox is centred on the tile, not pinned to its corner. */
68
+ /* The checkbox sits on the tile's title row (#2391): its centre on the
69
+ * title's first line (1rem at 1.4, so 0.7rem under the tile's padding), not
70
+ * the tile's middle, which a multi-line tile moves down to the body text.
71
+ * The 36px box (44px on a coarse pointer) then starts at the padding minus
72
+ * the half-box plus the half-line. The tile's content starts at the top so
73
+ * the title is where the box expects it. */
74
+ [data-slot="data-table-tiles"]
75
+ > [data-slot="data-table-item"]:not(:has([data-slot="data-table-record"])) {
76
+ align-content: start;
77
+ }
69
78
  [data-slot="data-table-tiles"] [data-slot="data-table-item-select"] {
79
+ inset-block-start: 0.325rem;
80
+ }
81
+ [data-slot="data-table-tiles"][data-density="compact"]
82
+ [data-slot="data-table-item-select"] {
83
+ inset-block-start: 0.075rem;
84
+ }
85
+ @media (pointer: coarse) {
86
+ [data-slot="data-table-tiles"] [data-slot="data-table-item-select"],
87
+ [data-slot="data-table-tiles"][data-density="compact"]
88
+ [data-slot="data-table-item-select"] {
89
+ inset-block-start: 0.075rem;
90
+ }
91
+ }
92
+ /* A record row is one dense line: its box stays centred on it. */
93
+ [data-slot="data-table-tiles"]
94
+ > [data-slot="data-table-item"]:has([data-slot="data-table-record"])
95
+ [data-slot="data-table-item-select"] {
70
96
  inset-block-start: 50%;
71
97
  translate: 0 -50%;
72
98
  }
@@ -48,6 +48,21 @@
48
48
  gap: var(--nxgt-cards-gap, 0.75rem);
49
49
  grid-template-columns: minmax(0, 1fr);
50
50
  }
51
+ /* The list's 4px of ring room sits inside the scroll box, which clips: the
52
+ * box takes it back on each inline side, so the cards' edges (not the
53
+ * room's) meet the page's edge and the heading above (#2472); the parent needs 4px of inline room. */
54
+ [data-slot="table-wrapper"]:has(> [data-slot="data-table-cards"]) {
55
+ margin-inline: -4px;
56
+ /* `w-full` (utilities layer) would win over this layer. */
57
+ width: calc(100% + 8px) !important;
58
+ }
59
+ /* Tiles keep their own 4px margin, except on a phone (#2292). */
60
+ @media (min-width: 768px) {
61
+ [data-slot="table-wrapper"]:has(> [data-slot="data-table-tiles"]) {
62
+ margin-inline: -4px;
63
+ width: calc(100% + 8px) !important;
64
+ }
65
+ }
51
66
  @container (min-width: 36rem) {
52
67
  [data-slot="data-table-cards"] {
53
68
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -0,0 +1,87 @@
1
+ /* TubeCap: a specimen tube's chip. The cap colours are the international tube
2
+ * convention, so they are fixed values, not theme tokens: the same in light
3
+ * and dark and in every theme. The swatch carries no text; the name beside it
4
+ * keeps the theme's foreground (AA by the theme), and a hairline in the
5
+ * foreground colour keeps a black or pale cap visible on either scheme. */
6
+ @layer components {
7
+ .nxgt-tube-cap {
8
+ --tube-cap: oklch(0.92 0 0);
9
+ --tube-cap-size: 1.75rem;
10
+ display: inline-flex;
11
+ align-items: center;
12
+ gap: 0.5rem;
13
+ max-inline-size: 100%;
14
+ min-inline-size: 0;
15
+ padding: 0.25rem 0.625rem 0.25rem 0.25rem;
16
+ border: 1px solid var(--border);
17
+ border-radius: 9999px;
18
+ background: var(--card, var(--background));
19
+ color: var(--foreground);
20
+ font-size: 0.875rem;
21
+ line-height: 1.25;
22
+ }
23
+ .nxgt-tube-cap[data-size="sm"] {
24
+ --tube-cap-size: 1.25rem;
25
+ font-size: 0.8125rem;
26
+ }
27
+ .nxgt-tube-cap[data-size="lg"] {
28
+ --tube-cap-size: 2.5rem;
29
+ font-size: 1rem;
30
+ }
31
+ .nxgt-tube-cap[data-kind="edta"] {
32
+ --tube-cap: oklch(0.62 0.13 300);
33
+ }
34
+ .nxgt-tube-cap[data-kind="heparin"] {
35
+ --tube-cap: oklch(0.62 0.17 145);
36
+ }
37
+ .nxgt-tube-cap[data-kind="citrate"] {
38
+ --tube-cap: oklch(0.8 0.1 230);
39
+ }
40
+ .nxgt-tube-cap[data-kind="esr"] {
41
+ --tube-cap: oklch(0.22 0 0);
42
+ }
43
+ .nxgt-tube-cap[data-kind="serum"] {
44
+ --tube-cap: oklch(0.55 0.21 27);
45
+ }
46
+ .nxgt-tube-cap[data-kind="gel"] {
47
+ --tube-cap: oklch(0.82 0.15 85);
48
+ }
49
+ .nxgt-tube-cap[data-kind="fluoride"] {
50
+ --tube-cap: oklch(0.68 0.01 260);
51
+ }
52
+ .nxgt-tube-cap[data-kind="trace"] {
53
+ --tube-cap: oklch(0.45 0.18 262);
54
+ }
55
+ .nxgt-tube-cap [data-slot="tube-cap-swatch"] {
56
+ flex: none;
57
+ box-sizing: border-box;
58
+ inline-size: var(--tube-cap-size);
59
+ block-size: var(--tube-cap-size);
60
+ border-radius: 9999px;
61
+ background: var(--tube-cap);
62
+ box-shadow: 0 0 0 1px
63
+ color-mix(in oklab, var(--foreground) 45%, transparent);
64
+ forced-color-adjust: none;
65
+ }
66
+ .nxgt-tube-cap [data-slot="tube-cap-text"] {
67
+ display: inline-flex;
68
+ flex-wrap: wrap;
69
+ align-items: baseline;
70
+ column-gap: 0.5rem;
71
+ min-inline-size: 0;
72
+ overflow-wrap: anywhere;
73
+ }
74
+ .nxgt-tube-cap [data-slot="tube-cap-label"] {
75
+ font-weight: 600;
76
+ }
77
+ .nxgt-tube-cap [data-slot="tube-cap-volume"],
78
+ .nxgt-tube-cap [data-slot="tube-cap-accession"] {
79
+ color: var(--muted-foreground);
80
+ font-variant-numeric: tabular-nums;
81
+ }
82
+ .nxgt-tube-cap [data-slot="tube-cap-action"] {
83
+ display: inline-flex;
84
+ flex: none;
85
+ margin-inline-start: 0.125rem;
86
+ }
87
+ }
@@ -1,6 +1,6 @@
1
1
  /* Theme "ambre", generated.
2
2
  * Seeds: primary oklch(0.52 0.13 48), secondary oklch(0.5 0.09 95), tertiary oklch(0.5 0.14 350), neutral hue 65, tint 0.85.
3
- * Contrast: 66 pairs, 0 failing.
3
+ * Contrast: 67 pairs, 0 failing.
4
4
  * hue-near-status: The primary's hue 48 is 14° from warning (62): they may read alike. Keep an icon and a word with every status.
5
5
  */
6
6
  [data-nxgt-theme="ambre"] {
@@ -1,6 +1,6 @@
1
1
  /* Theme "ardoise", generated.
2
2
  * Seeds: primary oklch(0.45 0.065 252), secondary oklch(0.48 0.05 215), tertiary oklch(0.48 0.1 290), neutral hue 250, tint 0.7.
3
- * Contrast: 66 pairs, 0 failing.
3
+ * Contrast: 67 pairs, 0 failing.
4
4
  */
5
5
  [data-nxgt-theme="ardoise"] {
6
6
  --radius: 0.375rem;
@@ -1,6 +1,6 @@
1
1
  /* Theme "bleumono", generated.
2
2
  * Seeds: primary oklch(0.46 0.13 255), secondary oklch(0.55 0.06 255), tertiary oklch(0.38 0.17 255), neutral hue 255, tint 1.1.
3
- * Contrast: 66 pairs, 0 failing.
3
+ * Contrast: 67 pairs, 0 failing.
4
4
  * lightness-moved: secondary: lightness 0.550 to 0.520 for AA.
5
5
  * chroma-lowered: tertiary: chroma 0.170 to 0.125 (lowered to fit sRGB or AA).
6
6
  * hue-near-status: The primary's hue 255 is 0° from info (254.624): they may read alike. Keep an icon and a word with every status.
@@ -1,6 +1,6 @@
1
1
  /* Theme "bluemoon", generated.
2
2
  * Seeds: primary oklch(0.45 0.12 265), secondary oklch(0.55 0.02 250), tertiary oklch(0.62 0.08 290), neutral hue 265, tint 1.
3
- * Contrast: 66 pairs, 0 failing.
3
+ * Contrast: 67 pairs, 0 failing.
4
4
  * lightness-moved: secondary: lightness 0.550 to 0.520 for AA.
5
5
  * lightness-moved: tertiary: lightness 0.620 to 0.563 for AA.
6
6
  * hue-near-status: The primary's hue 265 is 10° from info (254.624): they may read alike. Keep an icon and a word with every status.
@@ -1,6 +1,6 @@
1
1
  /* Theme "corail", generated.
2
2
  * Seeds: primary oklch(0.58 0.17 35), secondary oklch(0.52 0.1 70), tertiary oklch(0.5 0.1 200), neutral hue 30, tint 0.8.
3
- * Contrast: 66 pairs, 0 failing.
3
+ * Contrast: 67 pairs, 0 failing.
4
4
  * lightness-moved: primary: lightness 0.580 to 0.575 for AA.
5
5
  * chroma-lowered: tertiary: chroma 0.100 to 0.085 (lowered to fit sRGB or AA).
6
6
  * hue-near-status: The primary's hue 35 is 8° from error (27.325): they may read alike. Keep an icon and a word with every status.
@@ -17,7 +17,7 @@
17
17
  --popover-foreground: oklch(0.227 0.0136 30);
18
18
  --primary: oklch(0.575 0.17 35);
19
19
  --primary-foreground: oklch(0.984 0.003 30);
20
- --primary-text: color-mix(in oklab, var(--primary) 89%, black);
20
+ --primary-text: color-mix(in oklab, var(--primary) 88%, black);
21
21
  --primary-indicator: var(--primary);
22
22
  --secondary: oklch(0.52 0.1 70);
23
23
  --secondary-foreground: oklch(0.984 0.003 30);
@@ -1,6 +1,6 @@
1
1
  /* Theme "foret", generated.
2
2
  * Seeds: primary oklch(0.48 0.1 152), secondary oklch(0.5 0.08 118), tertiary oklch(0.5 0.1 55), neutral hue 140, tint 0.8.
3
- * Contrast: 66 pairs, 0 failing.
3
+ * Contrast: 67 pairs, 0 failing.
4
4
  */
5
5
  [data-nxgt-theme="foret"] {
6
6
  --radius: 0.5rem;
@@ -1,6 +1,6 @@
1
1
  /* Theme "lavande", generated.
2
2
  * Seeds: primary oklch(46% 0.1135 276.35), secondary oklch(48% 0.1135 245.65), tertiary oklch(45.91% 0.1864 310.75), neutral hue 292, tint 1.
3
- * Contrast: 66 pairs, 0 failing.
3
+ * Contrast: 67 pairs, 0 failing.
4
4
  */
5
5
  [data-nxgt-theme="lavande"] {
6
6
  --radius: 0.625rem;