@nxgt/material 3.101.0 → 3.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +37 -5
  3. package/dist/{alert-dialog-DYzxchiS.js → alert-dialog-CK_47jfm.js} +1 -1
  4. package/dist/{async-slice-drJyPvuM.js → async-slice-1ZNMiwyJ.js} +1 -1
  5. package/dist/{bubble-input-CQwVSoU3.js → bubble-input-BxI1WmNs.js} +17 -5
  6. package/dist/{button-GeIsUekU.js → button-YfEMZbRO.js} +2 -2
  7. package/dist/{calculator-BS4PAm6l.js → calculator-BmYZId4c.js} +261 -460
  8. package/dist/calculator-field.js +7 -7
  9. package/dist/{calculator-segment-CEdwE24M.js → calculator-segment-DJ08DJ-n.js} +1 -1
  10. package/dist/calculator.js +1 -1
  11. package/dist/charts.js +819 -764
  12. package/dist/{chip-BdEtFQ85.js → chip-DPj6e-3v.js} +2 -2
  13. package/dist/{code-editor-BByXZOGy.js → code-editor-CZg3nLZO.js} +178 -177
  14. package/dist/code-editor.js +5 -5
  15. package/dist/{code-language-items-CSZX5zxD.js → code-language-items-Nq1iOGId.js} +14 -5
  16. package/dist/components/ui/actions/action-card/action-card-tile.css +47 -0
  17. package/dist/components/ui/display/chip/chip-soft.css +27 -2
  18. package/dist/components/ui/display/code/code-moonlight.css +10 -10
  19. package/dist/components/ui/primitives/collapsible/collapsible.css +9 -0
  20. package/dist/{components-DQ4MljCr.js → components-B2wrH_W0.js} +19302 -18583
  21. package/dist/components.js +38 -37
  22. package/dist/{conversion-variant-DTQqAyDt.js → conversion-variant-C-LPx5ek.js} +5 -5
  23. package/dist/{currency-field-Dnju7ss2.js → currency-field-DnsRIA80.js} +24 -23
  24. package/dist/{dialog-5_2__tu8.js → dialog-BKBsDGFc.js} +3 -3
  25. package/dist/dnd.js +1 -1
  26. package/dist/{dropdown-menu-FdGC5kIP.js → dropdown-menu-QMDbRidH.js} +1 -1
  27. package/dist/editor.js +9 -9
  28. package/dist/{empty-state-BoepPG5v.js → empty-state-BLIJ6znY.js} +1 -1
  29. package/dist/{field-blur-DkiZOVNV.js → field-blur-DMJwPLsg.js} +9 -9
  30. package/dist/field-blur-context-3rKussBQ.js +29 -0
  31. package/dist/{financial-variant-BhqJp5C-.js → financial-variant-Ws0Jp01H.js} +5 -5
  32. package/dist/{hooks-DjcbJZiO.js → hooks-C3vkQmzh.js} +106 -61
  33. package/dist/hooks.js +3 -3
  34. package/dist/{hover-card-CmHJeBeY.js → hover-card-Df649U6I.js} +1 -1
  35. package/dist/i18n.js +3 -3
  36. package/dist/{icon-badge-LGUPQh_c.js → icon-badge-Cma1qOOJ.js} +1 -1
  37. package/dist/{icon-button-BSVpO1jP.js → icon-button-CpKxV264.js} +1 -1
  38. package/dist/key-layout-D2goCDkD.js +31 -0
  39. package/dist/keypad.js +551 -0
  40. package/dist/label-DR-zZyGQ.js +47 -0
  41. package/dist/lib/components/ui/actions/index.d.ts +1 -0
  42. package/dist/lib/components/ui/actions/product-tile/index.d.ts +1 -0
  43. package/dist/lib/components/ui/actions/product-tile/product-tile-link.d.ts +16 -0
  44. package/dist/lib/components/ui/actions/product-tile/product-tile.d.ts +41 -0
  45. package/dist/lib/components/ui/collections/data-table/data-table-density.d.ts +1 -1
  46. package/dist/lib/components/ui/collections/data-table/partials/data-table-scroll-region.d.ts +4 -1
  47. package/dist/lib/components/ui/collections/data-table/partials/layouts/data-table-list-bar.d.ts +6 -3
  48. package/dist/lib/components/ui/collections/data-table/partials/layouts/list-bar-in-row.d.ts +2 -1
  49. package/dist/lib/components/ui/collections/data-table/types.d.ts +11 -8
  50. package/dist/lib/components/ui/collections/data-table/virtual-option.d.ts +9 -3
  51. package/dist/lib/components/ui/collections/filter/use-filter-field-config.d.ts +2 -0
  52. package/dist/lib/components/ui/collections/server-table/partials/server-table-passed-through.d.ts +1 -0
  53. package/dist/lib/components/ui/collections/server-table/types.d.ts +1 -1
  54. package/dist/lib/components/ui/display/chip/chip.variants.d.ts +5 -4
  55. package/dist/lib/components/ui/display/code/code-block-parts.d.ts +1 -1
  56. package/dist/lib/components/ui/display/code/code-block.d.ts +4 -1
  57. package/dist/lib/components/ui/display/code/languages.d.ts +2 -0
  58. package/dist/lib/components/ui/display/code/moonlight-tokens.d.ts +10 -10
  59. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-dialog-props.d.ts +73 -0
  60. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-dialog-rules.d.ts +12 -0
  61. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-dialog.d.ts +5 -61
  62. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-footer.d.ts +12 -0
  63. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-phrase.d.ts +26 -0
  64. package/dist/lib/components/ui/feedback/confirm-dialog/use-confirm-action.d.ts +1 -1
  65. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  66. package/dist/lib/components/ui/fields/index.d.ts +1 -0
  67. package/dist/lib/components/ui/fields/quantity-field/types.d.ts +1 -1
  68. package/dist/lib/components/ui/fields/quantity-stepper/index.d.ts +2 -0
  69. package/dist/lib/components/ui/fields/quantity-stepper/quantity-stepper-model.d.ts +9 -0
  70. package/dist/lib/components/ui/fields/quantity-stepper/quantity-stepper-value.d.ts +15 -0
  71. package/dist/lib/components/ui/fields/quantity-stepper/quantity-stepper.d.ts +9 -0
  72. package/dist/lib/components/ui/fields/quantity-stepper/quantity-stepper.types.d.ts +35 -0
  73. package/dist/lib/components/ui/fields/quantity-stepper/use-quantity-stepper.d.ts +28 -0
  74. package/dist/lib/components/ui/layout/index.d.ts +1 -0
  75. package/dist/lib/components/ui/layout/pane-layout/pane-layout.d.ts +8 -3
  76. package/dist/lib/components/ui/layout/pane-layout/partials/pane-layout-classes.d.ts +2 -2
  77. package/dist/lib/components/ui/layout/pane-layout/partials/pane-layout-context.d.ts +9 -0
  78. package/dist/lib/components/ui/layout/pane-layout/partials/use-pane-layout-state.d.ts +7 -2
  79. package/dist/lib/components/ui/layout/panel-card/index.d.ts +1 -0
  80. package/dist/lib/components/ui/layout/panel-card/panel-card.d.ts +23 -0
  81. package/dist/lib/components/ui/primitives/collapsible/collapsible.d.ts +9 -1
  82. package/dist/lib/components/ui/primitives/tabs/partials/tabs-list.d.ts +6 -1
  83. package/dist/lib/components/ui/primitives/tabs/tabs-classes.d.ts +1 -0
  84. package/dist/lib/components/ui/selection/chip-group-field/chip-group-field.d.ts +8 -0
  85. package/dist/lib/components/ui/selection/chip-group-field/chip-group-field.types.d.ts +6 -0
  86. package/dist/lib/components/ui/selection/chip-group-field/partials/chip-group-reasons.d.ts +10 -0
  87. package/dist/lib/components/ui/selection/chip-group-field/partials/use-chip-rail.d.ts +12 -0
  88. package/dist/lib/components/ui/selection/choice-group/choice-group-root.d.ts +5 -2
  89. package/dist/lib/components/ui/selection/index.d.ts +1 -0
  90. package/dist/lib/components/ui/selection/status-menu/index.d.ts +3 -0
  91. package/dist/lib/components/ui/selection/status-menu/partials/status-confirm.d.ts +12 -0
  92. package/dist/lib/components/ui/selection/status-menu/partials/status-icon.d.ts +6 -0
  93. package/dist/lib/components/ui/selection/status-menu/partials/status-items.d.ts +18 -0
  94. package/dist/lib/components/ui/selection/status-menu/partials/status-menu-list.d.ts +14 -0
  95. package/dist/lib/components/ui/selection/status-menu/partials/status-sheet-row.d.ts +11 -0
  96. package/dist/lib/components/ui/selection/status-menu/partials/status-sheet.d.ts +21 -0
  97. package/dist/lib/components/ui/selection/status-menu/partials/status-trigger.d.ts +18 -0
  98. package/dist/lib/components/ui/selection/status-menu/status-menu-data.d.ts +12 -0
  99. package/dist/lib/components/ui/selection/status-menu/status-menu-form-field.d.ts +20 -0
  100. package/dist/lib/components/ui/selection/status-menu/status-menu.d.ts +13 -0
  101. package/dist/lib/components/ui/selection/status-menu/types.d.ts +76 -0
  102. package/dist/lib/components/ui/selection/status-menu/use-status-change.d.ts +18 -0
  103. package/dist/lib/components/ui/selection/status-menu/use-status-menu.d.ts +30 -0
  104. package/dist/lib/components/ui/widgets/calculator/partials/calculator-keypad.d.ts +1 -5
  105. package/dist/lib/components/ui/widgets/calculator/variants/key-layout.d.ts +3 -49
  106. package/dist/lib/components/ui/widgets/keypad/cash-model.d.ts +16 -0
  107. package/dist/lib/components/ui/widgets/keypad/index.d.ts +5 -0
  108. package/dist/lib/components/ui/widgets/keypad/key-layout.d.ts +56 -0
  109. package/dist/lib/components/ui/widgets/keypad/keypad-binding.d.ts +32 -0
  110. package/dist/lib/components/ui/widgets/keypad/keypad-cash.d.ts +28 -0
  111. package/dist/lib/components/ui/widgets/keypad/keypad-display.d.ts +11 -0
  112. package/dist/lib/components/ui/widgets/keypad/keypad-entry.d.ts +39 -0
  113. package/dist/lib/components/ui/widgets/keypad/keypad-grid.d.ts +27 -0
  114. package/dist/lib/components/ui/widgets/{calculator/partials/calculator-key.d.ts → keypad/keypad-key.d.ts} +6 -3
  115. package/dist/lib/components/ui/widgets/keypad/keypad-layout.d.ts +19 -0
  116. package/dist/lib/components/ui/widgets/{calculator/partials → keypad}/keypad-navigation.d.ts +1 -1
  117. package/dist/lib/components/ui/widgets/keypad/keypad.d.ts +9 -0
  118. package/dist/lib/components/ui/widgets/keypad/keypad.types.d.ts +91 -0
  119. package/dist/lib/components/ui/widgets/keypad/use-keypad-cash.d.ts +14 -0
  120. package/dist/lib/components/ui/widgets/keypad/use-keypad-keyboard.d.ts +7 -0
  121. package/dist/lib/components/ui/widgets/keypad/use-keypad-number.d.ts +10 -0
  122. package/dist/lib/components/ui/widgets/keypad/use-keypad-quantity.d.ts +16 -0
  123. package/dist/lib/components/ui/widgets/keypad/use-keypad-value.d.ts +17 -0
  124. package/dist/lib/components/ui/widgets/keypad/use-keypad.d.ts +23 -0
  125. package/dist/lib/components/ui/widgets/native-charts/core/label-placement.d.ts +25 -0
  126. package/dist/lib/components/ui/widgets/native-charts/core/reference-geometry.d.ts +3 -2
  127. package/dist/lib/hooks/index.d.ts +1 -0
  128. package/dist/lib/hooks/use-barcode-scanner.d.ts +39 -0
  129. package/dist/lib/i18n/resources/en/en.d.ts +28 -0
  130. package/dist/lib/i18n/resources/en/material.json.d.ts +30 -2
  131. package/dist/lib/i18n/resources/fr/fr.d.ts +28 -0
  132. package/dist/lib/i18n/resources/fr/material.json.d.ts +30 -2
  133. package/dist/lib/i18n/resources/resources.d.ts +56 -0
  134. package/dist/lib/i18n/translator.d.ts +1 -1
  135. package/dist/lib/keypad/index.d.ts +20 -0
  136. package/dist/lib/print/output/csv.d.ts +37 -0
  137. package/dist/lib/print/output/index.d.ts +1 -0
  138. package/dist/lib.js +1 -1
  139. package/dist/main.js +54 -53
  140. package/dist/map.js +9 -9
  141. package/dist/material.css +1 -1
  142. package/dist/pdf-reader.js +1 -1
  143. package/dist/{percent-field-DGlU_xii.js → percent-field-sAq7BaxN.js} +15 -14
  144. package/dist/{picker-surface-CGhqNR8a.js → picker-surface--6E-nvpU.js} +4 -4
  145. package/dist/{popover-ZxL7ktKk.js → popover-bEB8I_-w.js} +1 -1
  146. package/dist/{pos-types-BQ0QpYMp.js → pos-types-Cpdc4E33.js} +1 -1
  147. package/dist/{pos-variant-CKy8h_5i.js → pos-variant-ClMGu59I.js} +25 -25
  148. package/dist/print.js +146 -127
  149. package/dist/{programmer-variant-CbHstE28.js → programmer-variant-ChzgyA7x.js} +30 -30
  150. package/dist/{public-D-tdOz4b.js → public-BY8N5NDT.js} +9 -9
  151. package/dist/{rich-code-block-CTPj1LuD.js → rich-code-block-D2GiWuqD.js} +18 -18
  152. package/dist/{scientific-variant-B8U9h3II.js → scientific-variant-31_93gcZ.js} +8 -8
  153. package/dist/{sortable-provider-CSoWE7Ce.js → sortable-provider-DGiVn72t.js} +2 -2
  154. package/dist/{spinner-ffuwBXHl.js → spinner-CEVomQ5K.js} +1 -1
  155. package/dist/standard-layout-DTr9yy8Z.js +48 -0
  156. package/dist/{tabs-JgRq6ZJx.js → tabs-B8MXBbPW.js} +14 -14
  157. package/dist/{text-field-DURkIP-v.js → text-field-DN88qD0Q.js} +16 -15
  158. package/dist/theme-picker.js +13 -13
  159. package/dist/{theme-toggle-n_Zg7JAA.js → theme-toggle-C7pnu1Xh.js} +1 -1
  160. package/dist/{toolbar-items-IANZdcw4.js → toolbar-items-BDfoh8QB.js} +4 -4
  161. package/dist/{translate-material-CMHUH5Ha.js → translate-material-s_lxIV43.js} +1 -1
  162. package/dist/{translator-9zA-Um3W.js → translator-Cgql9srO.js} +56 -4
  163. package/dist/use-calculator-keyboard-DysnFK8m.js +222 -0
  164. package/dist/{use-chat-composer-BoIN7RCA.js → use-chat-composer-xCcp9fVc.js} +2250 -2195
  165. package/dist/{use-heading-spy-Ciw9GBHL.js → use-heading-spy-FIZlf9qj.js} +235 -154
  166. package/dist/{use-material-translation-CLNaMhUf.js → use-material-translation-xNzbSVgS.js} +1 -1
  167. package/dist/{use-return-focus-DWhbTGVe.js → use-return-focus-CW9KkDkp.js} +1 -1
  168. package/docs/README.md +2 -1
  169. package/docs/components/README.md +2 -0
  170. package/docs/components/buttons-and-actions.md +81 -2
  171. package/docs/components/data-display.md +6 -3
  172. package/docs/components/data-table-filter-and-lists.md +19 -6
  173. package/docs/components/feedback-and-status.md +20 -0
  174. package/docs/components/form-fields.md +60 -7
  175. package/docs/components/navigation-and-layout.md +48 -2
  176. package/docs/components/primitives.md +44 -2
  177. package/docs/components/rich-widgets.md +28 -0
  178. package/docs/components/selection-and-choice.md +176 -1
  179. package/docs/guides/calculator.md +1 -1
  180. package/docs/guides/cards-and-tiles.md +31 -24
  181. package/docs/guides/csp.md +4 -3
  182. package/docs/guides/data-grid.md +5 -4
  183. package/docs/guides/field-look-refined.md +2 -2
  184. package/docs/guides/forms.md +1 -0
  185. package/docs/guides/helpers.md +49 -0
  186. package/docs/guides/keypad.md +233 -0
  187. package/docs/guides/printing.md +51 -1
  188. package/docs/previews/buttons-actions-producttile.webp +0 -0
  189. package/docs/previews/data-display-codeblock.webp +0 -0
  190. package/docs/previews/data-display-codegroup.webp +0 -0
  191. package/docs/previews/data-display-jsonviewer.webp +0 -0
  192. package/docs/previews/form-fields-quantitystepper.webp +0 -0
  193. package/docs/previews/navigation-layout-panelcard.webp +0 -0
  194. package/docs/previews/rich-widgets-keypad.webp +0 -0
  195. package/docs/previews/selection-choice-statusmenu.webp +0 -0
  196. package/docs/roadmap.md +21 -0
  197. package/docs/troubleshooting.md +68 -4
  198. package/lib/components/ui/actions/action-card/action-card-tile.css +47 -0
  199. package/lib/components/ui/display/chip/chip-soft.css +27 -2
  200. package/lib/components/ui/display/code/code-moonlight.css +10 -10
  201. package/lib/components/ui/primitives/collapsible/collapsible.css +9 -0
  202. package/lib/components/ui/widgets/calculator/calculator-keys.css +3 -0
  203. package/lib/components/ui/widgets/calculator/calculator.css +118 -0
  204. package/package.json +7 -2
  205. package/dist/field-blur-context-BLGgf_3z.js +0 -73
  206. package/dist/key-layout-cR79xfGE.js +0 -38
  207. package/dist/standard-layout-BuSRr3TY.js +0 -40
@@ -29,6 +29,7 @@ value type each one writes. Everything is imported from
29
29
  | <img src="../previews/selection-choice-sortablelist.webp" width="96" alt=""> | [SortableList](#sortablelist) | Reorderable list: drag or keyboard, announced; two-line labels, row menu |
30
30
  | <img src="../previews/selection-choice-treeselectfield.webp" width="96" alt=""> | [TreeSelectField](#treeselectfield) | Pick from a tree, single or multiple |
31
31
  | <img src="../previews/selection-choice-switcher.webp" width="96" alt=""> | [Switcher](#switcher) | Workspace, establishment or class switcher: menu, search, groups, links; `size="compact"` an icon-only round trigger for the app bar's `titleAfter` slot |
32
+ | <img src="../previews/selection-choice-statusmenu.webp" width="96" alt=""> | [StatusMenu](#statusmenu) | A record's status as a soft chip that opens a menu (a bottom sheet on a phone) of statuses: transitions, locked, busy, read-only; `StatusMenuFormField` for forms |
32
33
 
33
34
  ## Select
34
35
 
@@ -839,6 +840,61 @@ export function Topics() {
839
840
  }
840
841
  ```
841
842
 
843
+ <a id="chipgroupfield-tones"></a>
844
+
845
+ An option's `tone` colours its chip with the tones `Badge` takes (`primary`,
846
+ `secondary`, `tertiary`, `success`, `info`, `warning`, `error`, `neutral`, and
847
+ an app's own from `AppTones`), so a category filter can match the toned tiles
848
+ below it. An unchosen chip shows a soft tint of the tone with the tone's text
849
+ colour; a chosen chip shows the filled tone under its on-colour, both at AA
850
+ contrast in light and dark. The leading check, `aria-checked`, the roving
851
+ focus and the boundary of an unchosen chip are unchanged. No `tone` keeps the
852
+ neutral chip.
853
+
854
+ ```tsx
855
+ <ChipGroupField
856
+ label="Categories"
857
+ options={[
858
+ { value: 'drinks', label: 'Drinks', icon: 'glass', tone: 'info' },
859
+ { value: 'bakery', label: 'Bakery', tone: 'warning' },
860
+ { value: 'other', label: 'Other' },
861
+ ]}
862
+ value={value}
863
+ onValueChange={setValue}
864
+ />
865
+ ```
866
+
867
+ `overflow="scroll"` keeps the chips on one line that scrolls sideways (touch,
868
+ trackpad, the wheel; a focused chip scrolls into view), with a fade at each
869
+ edge that hides more chips and no page scroll. The roving focus and the
870
+ selection are the same. The default `overflow="wrap"` lets the chips flow onto
871
+ several lines; `columns` and `groups` draw a grid and ignore `overflow`.
872
+
873
+ `overflow="scroll"` for one sideways-scrolling line:
874
+
875
+ ```tsx
876
+ import { ChipGroupField } from '@nxgt/material/components';
877
+ import { useState } from 'react';
878
+
879
+ const subjects = ['Algebra', 'Biology', 'Chemistry', 'Drama', 'Economics', 'French'].map(
880
+ (label) => ({ value: label.toLowerCase(), label }),
881
+ );
882
+
883
+ export function Subjects() {
884
+ const [value, setValue] = useState<string[]>([]);
885
+ return (
886
+ <ChipGroupField
887
+ label="Subjects"
888
+ type="multiple"
889
+ overflow="scroll"
890
+ options={subjects}
891
+ value={value}
892
+ onValueChange={setValue}
893
+ />
894
+ );
895
+ }
896
+ ```
897
+
842
898
  Balanced by default, and the unbalanced fill:
843
899
 
844
900
  ```tsx
@@ -857,7 +913,7 @@ const four = ['a', 'b', 'c', 'd'].map((value) => ({ value, title: value.toUpperC
857
913
  | `value` / `onValueChange` | `string \| null` or `string[]` | — | the chosen value, by `type` |
858
914
  | `deselectable` | `boolean` | `false` | `single` only: pressing the chosen chip clears it |
859
915
  | `max` / `min` | `number` | — | `multiple` only: `max` blocks the other chips once reached and speaks "{count} of {max} chosen"; `min` is a hint only |
860
- | `options` | `{ value; label; icon?; count?; description?; disabled?; disabledReason? }[]` | — | the chips, in one flow; `count`: see [option counts](#option-counts) |
916
+ | `options` | `{ value; label; icon?; tone?; count?; description?; disabled?; disabledReason? }[]` | — | the chips, in one flow; `count`: see [option counts](#option-counts); `tone`: see [tones](#chipgroupfield-tones) |
861
917
  | `groups` | `{ label: string; options }[]` | — | titled sections instead of `options` (it wins when both are given): see [slot picker](#slot-picker-columns-groups-and-reasons) |
862
918
  | `columns` | `number \| { base?; sm?; md?; lg? }` | — | equal-width columns (1 to 12; one count per breakpoint, each holding to the next); Up and Down then move by row. Unset, the chips flow and wrap |
863
919
  | `showDisabledReason` | `boolean` | `true` | prints a disabled option's `disabledReason` below the whole group ("Label: reason"), so the chip row keeps its wrapping; `false` leaves it to the tooltip and the accessible description |
@@ -1020,6 +1076,125 @@ on a touch screen.
1020
1076
 
1021
1077
  ---
1022
1078
 
1079
+ ## StatusMenu
1080
+
1081
+ ![StatusMenu](../previews/selection-choice-statusmenu.webp)
1082
+
1083
+ A record's status (a ticket, an order, a patient, a device) as a chip that
1084
+ changes it in one tap: a soft Badge pill (the tone's container, the tone's
1085
+ icon, the words, a chevron) that opens a menu of radio items. The held status
1086
+ is checked and inert. The button is named "Status of Ticket 42: Open", and
1087
+ `onSelect` never runs for the held status, for a blocked one, or when locked or
1088
+ busy.
1089
+
1090
+ - `transitions` greys the statuses it does not reach, each saying why. A
1091
+ terminal status needs `done: []`; unlike `useAllowedTransitions`, a missing
1092
+ key allows every move.
1093
+ - `locked` picks nothing and shows its `reason` as a tooltip and as the menu's
1094
+ first line.
1095
+ - `busy` keeps the old status and swaps the chevron for a spinner. A returned
1096
+ promise does the same until it settles; a rejection is announced and sent to
1097
+ `onError`.
1098
+ - `confirm` on a choice asks first, in a dialog.
1099
+ - `readOnly` draws a plain label, no button.
1100
+
1101
+ On a phone (under 768px) the statuses open in a bottom sheet of 44px rows
1102
+ instead of the menu; a status that cannot be picked says why in its own row
1103
+ (touch has no tooltip). `presentation` is `'auto'` (default), `'menu'` or
1104
+ `'sheet'`.
1105
+
1106
+ ```tsx
1107
+ import { StatusMenu, type StatusChoice } from '@nxgt/material/components';
1108
+
1109
+ const statuses: StatusChoice<'todo' | 'progress' | 'done'>[] = [
1110
+ { value: 'todo', label: 'To do', tone: 'neutral', icon: 'circle' },
1111
+ { value: 'progress', label: 'In progress', tone: 'info', icon: 'circle-half-stroke' },
1112
+ { value: 'done', label: 'Done', tone: 'success', icon: 'circle-check' },
1113
+ ];
1114
+
1115
+ <StatusMenu
1116
+ label="Ticket 42"
1117
+ value={status}
1118
+ choices={statuses}
1119
+ transitions={{ done: ['progress'] }}
1120
+ presentation="auto" // 'menu' | 'sheet' to force one
1121
+ onSelect={(next) => setStatus(next)}
1122
+ />;
1123
+ ```
1124
+
1125
+ Saving through a server, with a status that asks first:
1126
+
1127
+ ```tsx
1128
+ const statuses: StatusChoice<'open' | 'cancelled'>[] = [
1129
+ { value: 'open', label: 'Open', tone: 'info' },
1130
+ {
1131
+ value: 'cancelled',
1132
+ label: 'Cancelled',
1133
+ tone: 'error',
1134
+ confirm: { title: 'Cancel this ticket?', confirmLabel: 'Cancel ticket', destructive: true },
1135
+ },
1136
+ ];
1137
+
1138
+ <StatusMenu
1139
+ label="Ticket 42"
1140
+ value={status}
1141
+ choices={statuses}
1142
+ onSelect={async (next) => {
1143
+ await saveStatus(ticketId, next); // pending until it settles; a throw is announced
1144
+ setStatus(next);
1145
+ }}
1146
+ onError={(error) => toast.error(String(error))}
1147
+ />;
1148
+ ```
1149
+
1150
+ `confirm` defaults: `confirmLabel` "Confirm", `destructive` `false`, `description`
1151
+ "Change the status to {label}?" (`material.status-menu.confirm-default`). The menu
1152
+ marks such a choice "Asks first" (`material.status-menu.confirms`). A confirmed
1153
+ save shows its progress in the dialog (busy buttons, a failure kept in it), not on the chip.
1154
+
1155
+ | Prop | Type | Default | Effect |
1156
+ | --- | --- | --- | --- |
1157
+ | `label` | `string` | — | what the status belongs to; names the button "Status of {label}: {status}" |
1158
+ | `value` / `choices` | `V` / `readonly StatusChoice<V>[]` | — | the held status, and every status: `value`, `label`, `tone`, `icon`, `description`, `disabled`, `disabledReason`, `confirm` (`title`, `description`, `confirmLabel`, `destructive`: a `ConfirmDialog` runs `onSelect` on confirm, a failure stays in the dialog and reaches `onError`) |
1159
+ | `onSelect` | `(next, prev) => void \| Promise<void>` | — | a different status was picked; a returned promise keeps the chip pending (menu closed, focus on the chip, spinner, `aria-busy`, old status shown) until it settles |
1160
+ | `onError` | `(error) => void` | — | the promise rejected (or threw): the failure is announced and the old status stays |
1161
+ | `transitions` | `Partial<Record<V, readonly V[]>>` | — | the statuses reachable from each status (no key: every move; a terminal status is `[]`) |
1162
+ | `unavailable` | `'disabled' \| 'hidden'` | `'disabled'` | an unreachable status is greyed with "Not available from {status}", or dropped |
1163
+ | `locked` / `reason` | `boolean` / `string` | — | nothing can be picked; `reason` is the tooltip, the button's description and the menu's first line |
1164
+ | `busy` | `boolean` | `false` | `aria-busy` and `aria-disabled`, a spinner, the old status kept, the menu does not open |
1165
+ | `readOnly` | `boolean` | `false` | a plain label |
1166
+ | `size` | `'sm' \| 'md'` | `'md'` | chip height |
1167
+ | `presentation` | `'auto' \| 'menu' \| 'sheet'` | `'auto'` | a dropdown menu, a bottom sheet of 44px rows, or the sheet under 768px |
1168
+ | `open` / `onOpenChange` | `boolean` / `(open) => void` | — | controlled menu |
1169
+ | `triggerProps` / `className` | | | on the button / on the menu |
1170
+
1171
+ Texts come from `material.status-menu.*` (English and French). The rules
1172
+ (which statuses can be picked, and why not) are `@nxgt/material-core/status-menu`.
1173
+
1174
+ **Form wrapper:** `StatusMenuFormField` takes `control`, `name`, `rules` and the
1175
+ StatusMenu props except `value` (the form holds it). A pick runs
1176
+ `onSelect` first; the form takes the new status once `onSelect` resolves (a
1177
+ promise keeps the pending state and the old status, and a rejection keeps the old
1178
+ value). `field.onBlur` fires when focus leaves the chip and its menu or sheet. An
1179
+ empty value (`''`) matches no choice, so the chip shows an empty value: give an
1180
+ initial status. The error message (or `helperText`) is written under the chip and linked to
1181
+ it with `aria-describedby` and `aria-invalid`, a refused submit focuses the chip,
1182
+ and `disabled` (or the form's) locks it.
1183
+
1184
+ ```tsx
1185
+ <StatusMenuFormField
1186
+ control={form.control}
1187
+ name="status"
1188
+ label="Ticket 42"
1189
+ choices={statuses}
1190
+ rules={{ required: 'Pick a status.' }}
1191
+ />
1192
+ ```
1193
+
1194
+ **Storybook:** `Selection & choice/StatusMenu` (and `/In a form`) · **Vue:** React only for now (#2816)
1195
+
1196
+ ---
1197
+
1023
1198
  ## OptionTile
1024
1199
 
1025
1200
  ![OptionTile](../previews/selection-choice-optiontile.webp)
@@ -123,7 +123,7 @@ numbers: `sqrt(-1)` is "Undefined result".
123
123
  />
124
124
  ```
125
125
 
126
- The props below are read only by this variant (`currency` is shared). A point of sale till for a counter, on `@nxgt/material-core/calculator/pos`; the
126
+ The props below are read only by this variant (`currency` is shared). A point of sale till for a counter, on `@nxgt/material-core/calculator/pos` (the engine; apps import `@nxgt/material/calculator`). The
127
127
  tab is named "Point of sale". The keys type an amount the way a till does:
128
128
  `1`, `2`, `5` is `1.25`, `00` adds two zeros, backspace drops the last digit,
129
129
  `C` clears it. `+` adds it to the ticket. `×` types a quantity first (`2.50 ×
@@ -265,6 +265,16 @@ focus. Below 30rem of list width the "Sort:" prefix is visually hidden (the name
265
265
  stays, and the button's accessible name is still "Sort: Name, Ascending").
266
266
  With no sortable column there is no control, and with neither a sortable column nor a `select` column there is no list bar.
267
267
 
268
+ For a short list the band is noise: `showSort={false}` drops the button (on a
269
+ phone too) and leaves every column sortable, so a
270
+ `layout={{ phone: 'cards', wide: 'table' }}` keeps its header sorting on desktop.
271
+ A list with a `select` column keeps its bar for "Select all". `DataTable` only:
272
+ `ServerTable` does not pass it through.
273
+
274
+ ```tsx
275
+ <DataTable layout="cards" showSort={false} renderCard={…} … />
276
+ ```
277
+
268
278
  ## Columns
269
279
 
270
280
  Tiles are always one column. For cards, the list's own width, not the window's, picks the columns: one, two from 36rem,
@@ -326,49 +336,46 @@ grid of compact cells you read at a glance and change in place. Use
326
336
  [`ActionCard variant="tile"`](../components/buttons-and-actions.md#actioncard-tile)
327
337
  in a `ResponsiveGrid minItem="xs"`, one grid per section under its own `h2`:
328
338
 
329
- - **`status`** is a soft `Badge` with its word and an icon, so a status is
330
- never told by colour alone.
339
+ - **`action`** is a [`StatusMenu`](../components/selection-and-choice.md#statusmenu)
340
+ chip: it shows the status as a word, an icon and a tone (never colour alone),
341
+ so the tile needs no `status` badge, and it changes the status in place (a
342
+ bottom sheet on a phone). Its button is named "Status of {name}: {status}".
343
+ The title is the one button (it opens the detail), so the card and its chip
344
+ are two tab stops and nothing nests. When the tile is too narrow for the title
345
+ and the chip side by side (two per row at 360px), the chip wraps under the
346
+ title.
331
347
  - **`meta`** is one line, the occupant only while occupied; a long one is
332
348
  clipped and shown in full in a tooltip.
333
- - **`action`** is a `RowMenu` with a `DropdownMenuRadioGroup` of statuses,
334
- named "Change status of {name}". The title is the one button (it opens the
335
- detail), so the card and its menu are two tab stops and nothing nests.
336
349
 
337
350
  ```tsx
338
- import {
339
- ActionCard,
340
- Badge,
341
- DropdownMenuRadioGroup,
342
- DropdownMenuRadioItem,
343
- ResponsiveGrid,
344
- RowMenu,
345
- } from '@nxgt/material/components';
351
+ import { ActionCard, ResponsiveGrid, StatusMenu } from '@nxgt/material/components';
352
+
353
+ const choices = [
354
+ { value: 'free', label: 'Free', tone: 'success', icon: 'check' },
355
+ { value: 'occupied', label: 'Occupied', tone: 'warning', icon: 'user' },
356
+ { value: 'out', label: 'Out of service', tone: 'error', icon: 'ban' },
357
+ ];
346
358
 
347
359
  <ResponsiveGrid minItem="xs">
348
360
  <ActionCard
349
361
  variant="tile"
350
362
  title="Bed A1"
351
- status={<Badge variant="soft" tone="warning" icon="user">Occupied</Badge>}
352
363
  meta="Amara Diallo"
353
364
  onClick={() => openDetail('a1')}
354
365
  action={
355
- <RowMenu
366
+ <StatusMenu
356
367
  label="Bed A1"
357
- triggerProps={{ 'aria-label': 'Change status of Bed A1' }}
358
- >
359
- <DropdownMenuRadioGroup value={status} onValueChange={setStatus}>
360
- <DropdownMenuRadioItem value="free">Free</DropdownMenuRadioItem>
361
- <DropdownMenuRadioItem value="occupied">Occupied</DropdownMenuRadioItem>
362
- <DropdownMenuRadioItem value="cleaning">Cleaning</DropdownMenuRadioItem>
363
- </DropdownMenuRadioGroup>
364
- </RowMenu>
368
+ value={status}
369
+ choices={choices}
370
+ onSelect={setStatus}
371
+ />
365
372
  }
366
373
  />
367
374
  </ResponsiveGrid>;
368
375
  ```
369
376
 
370
377
  `title` is required with `action`: it is the card's button, named after the
371
- title, the status and the meta line.
378
+ title and the meta line.
372
379
 
373
380
  For the figures of the whole board, put a `StatCardGroup` above a `DataTable`
374
381
  with one row per section, one numeric column per status (they add up to the
@@ -84,9 +84,10 @@ A virtual `DataTable` (`virtual`) adds no `style` attribute either: the
84
84
  server renders its first rows as plain rows, and the spacer rows' heights
85
85
  are custom properties written through `style.setProperty` after mount,
86
86
  which `style-src-attr` does not govern (`StrictCspVirtual` renders one under
87
- `style-src-attr 'none'`). Its scroll region's `maxHeight` is still an inline
88
- `style` on the server, one of #596's (a `maxHeightClassName` for
89
- `DataTable`, as `DataGrid` has, is [#800](https://github.com/softistx/nxgt-material/issues/800)).
87
+ `style-src-attr 'none'`). Its scroll region's `maxHeight` is an inline
88
+ `style` on the server, one of #596's: give `maxHeightClassName`
89
+ (`maxHeightClassName="max-h-[28rem]"`, as `DataGrid` has) instead, and the
90
+ region has no `style` attribute.
90
91
 
91
92
  A keyline `Carousel` (`multi-browse`, `hero`) renders no `style` attribute
92
93
  either: its first frame comes from container-query CSS, and the masks that
@@ -1123,10 +1123,11 @@ A strict policy works with no `style-src-attr 'unsafe-inline'` for the grid:
1123
1123
  the CSSOM, which `style-src-attr` does not govern. A frozen column's drag
1124
1124
  rewrites `--dg-start-*` the same way.
1125
1125
  - A custom editor must follow the same rule: classes only.
1126
- - `DataTable`'s own `maxHeight` (which a virtual `DataTable` needs) is an
1127
- inline `style={{ maxHeight }}` on its scroll region, so it is not covered
1128
- by this list; the grid takes a class instead. A `maxHeightClassName` on
1129
- `DataTable` is proposed in [#800](https://github.com/softistx/nxgt-material/issues/800).
1126
+ - `DataTable`'s own `maxHeight` is an inline `style={{ maxHeight }}` on its
1127
+ scroll region, so it is not covered by this list; the grid takes a class
1128
+ instead. A virtual `DataTable` under a strict CSP takes
1129
+ `maxHeightClassName` (a class such as `max-h-[28rem]`, no inline style;
1130
+ [#800](https://github.com/softistx/nxgt-material/issues/800)).
1130
1131
 
1131
1132
  The grid's popups (the select's list, the date's calendar, the menus) are
1132
1133
  library components and add nothing to the policy: see the
@@ -41,7 +41,7 @@ import { TextField } from '@nxgt/material/components';
41
41
  | --- | --- |
42
42
  | `TextField` and the `*Field` text wrappers | `leading` / `trailing` icons and actions are muted with no well fill; a text unit (`"/ 20"`, a currency code) sits inline at the end in muted text. `CurrencyField` follows |
43
43
  | `NumberField`, `PercentField`, `CalculatorField` | one `role="group"` box; the value is end-aligned in tabular figures, the suffix hugs it, the Decrease and Increase buttons are a tinted round pair at the end (the `DurationField` look), no dividers |
44
- | `QuantityField`, `AgeField` | the value, the unit and the pair share one box; the unit is a chip that opens a menu |
44
+ | `QuantityField`, `AgeField` | the value, the unit and the pair share one box; the unit is a chip that opens a menu (no chip without `units` or `allowCustomUnit`) |
45
45
  | `NumberField variant="score"` | a big value with its maximum, no box at rest; see [the form fields page](../components/form-fields.md#numberfield) |
46
46
  | Date, time and duration fields | the shared focus ring, a dashed hairline when read-only, the recessed surface when disabled |
47
47
 
@@ -49,7 +49,7 @@ import { TextField } from '@nxgt/material/components';
49
49
 
50
50
  - The `NumberField` order is the input, Decrease, Increase (it was Decrease,
51
51
  input, Increase). With a `CalculatorField`: input, Open calculator,
52
- Decrease, Increase. A `QuantityField` is value, unit, Decrease, Increase.
52
+ Decrease, Increase. A `QuantityField` is value, unit (when it has one), Decrease, Increase.
53
53
  - The number box is a `group`. A test that queries an unnamed
54
54
  `getByRole('group')` now also matches it: name the one you mean.
55
55
  - A `QuantityField` unit is a button named "Unit: mg", not a `combobox`:
@@ -537,6 +537,7 @@ or fails validation.
537
537
  | `Checkbox` · `CheckboxFormField` | `boolean` | `false` (see [Traps](#traps)) |
538
538
  | `CheckboxGroup` · `CheckboxGroupFormField` | `string[]` | `[]` |
539
539
  | `RadioGroup` · `RadioGroupFormField` | `string` | `''` |
540
+ | `StatusMenu` · `StatusMenuFormField` | `string`: the picked `choices` value | `''` matches no choice, so the chip shows an empty value: give an initial status |
540
541
  | `CheckCardField` · `CheckCardFormField` | `boolean`: `true` for the first card, `false` for the second | `false` |
541
542
  | `ChipGroupField` · `ChipGroupFormField` | `type="single"` (default): `string \| null`. `type="multiple"`: `string[]` | `null` / `[]` |
542
543
  | `ChoiceCardGroup` · `ChoiceCardGroupFormField` | same as `ChipGroupField` | `null` / `[]` |
@@ -139,6 +139,7 @@ Vue: the Redux helpers are Pinia store factories: `defineLocalStore`
139
139
  | `useFocusOnMount<T>(focus = true)` | A ref whose element takes focus once mounted, and again whenever `focus` turns true: the heading of what replaced the content the user acted on. See [below](#usefocusonmount). |
140
140
  | `useScrollAway(ref?, { threshold? })` | True while the reader scrolls down (by `threshold`, 24 px, since the scroll last turned); false scrolling up, at the top or at the end. Folds an extended `Fab`. Without `ref`, the window; false on the server. See [Fab](../components/buttons-and-actions.md#fab). |
141
141
  | `useLatched(value)`, `useLatchedWhileClosed(open, value)` | The last value that was not `null` / `undefined`, and the value from the last open render while `open` is false: keep a dialog's text while it fades out after the app cleared its subject. `ConfirmDialog` does it itself. See [below](#uselatched). |
142
+ | `useBarcodeScanner({ onScan, enabled?, acceptIn?, maxGap?, minLength?, onKey? })`, `createScanner()`, `attachBarcodeScanner(target, options)` | Hears a keyboard-wedge barcode reader anywhere on the page: a run of keys each within `maxGap` ms (50), at least `minLength` (4) long, closed by Enter, reaches `onScan(code)`; slower or shorter runs are typing. `createScanner` is the framework-free detector (`@nxgt/material-core/barcode-scanner`, also re-exported by `@nxgt/material/hooks`). See [below](#usebarcodescanner). |
142
143
  | `useCountdown(duration, { paused?, onExpire?, key? })` | Seconds left of `duration` (the clock `CountdownTimer` draws), for a button or a message that waits: ticks on the client only, `duration` on the server and first render. A new `duration` **or a new `key`** starts over, so the same `retryAfter` twice restarts it. See [below](#usecountdown). |
143
144
  | `useSubmitOnce(busy?)` | One submit in flight: wraps a submit handler so a double click or a second Enter is dropped until the request ends (the handler's promise, or `busy` back to false). See [below](#usesubmitonce). |
144
145
  | `useDrafts<T>(storageKey, { serialize?, parse?, storage? })`, `clearDrafts(storageKey, storage?)` | Unsent values kept by key in `sessionStorage`: `{ get, set, drop, clear, keys, loaded }`. Read after the first render (no hydration mismatch), storage errors swallowed, `drop(key)` once saved; `clearDrafts` empties a store key without a hook (sign-out). See [below](#usedrafts). |
@@ -260,6 +261,54 @@ export function ItemDialog({ item, onClose }: { item: Item | null; onClose: () =
260
261
  things: it returns `value` while `open`, and the value of the last open render
261
262
  once `open` is false.
262
263
 
264
+ ### `useBarcodeScanner`
265
+
266
+ A USB or Bluetooth barcode reader is a keyboard: it types the whole code in a
267
+ few milliseconds a key and ends with Enter. The hook tells that run from a
268
+ person typing by timing alone, with one capture-phase `keydown` listener on
269
+ the document, for a till, a stock count or a check-in screen. It does not draw
270
+ barcodes: that is [`Barecode`](../components/data-display.md#barecode).
271
+
272
+ ```tsx
273
+ import { useState } from 'react';
274
+ import { useBarcodeScanner } from '@nxgt/material/hooks';
275
+
276
+ export function Till({ add }: { add: (code: string) => void }) {
277
+ const [query, setQuery] = useState('');
278
+ useBarcodeScanner({ onScan: add, acceptIn: '[data-till-search]' });
279
+ return (
280
+ <input
281
+ data-till-search=""
282
+ aria-label="Search an item"
283
+ value={query}
284
+ onChange={(event) => setQuery(event.target.value)}
285
+ />
286
+ );
287
+ }
288
+ ```
289
+
290
+ | Option | Default | Effect |
291
+ | --- | --- | --- |
292
+ | `onScan` | required | called with the code when a scan ends; the Enter that closed it is swallowed (`preventDefault`, `stopPropagation`) |
293
+ | `enabled` | `true` | while `false` nothing is listened to and nothing is swallowed |
294
+ | `acceptIn` | none | a CSS selector of the form fields a scan is also accepted in (the search field); every other `input`, `textarea`, `select` and editable element is typed in. Outside form fields a scan is always accepted. In an accepted field the scan's characters are already typed in when Enter arrives: `onScan` clears the field itself |
295
+ | `maxGap` | `50` | the longest pause between two keys of one scan, in ms |
296
+ | `minLength` | `4` | the shortest code taken for a scan |
297
+ | `onKey` | none | `(event, { result, pending })` for each key the scanner looked at: a screen with its own shortcuts skips them while `pending.length > 1`, since those keys are a scan's |
298
+
299
+ Ctrl, Meta and Alt chords and key repeat are ignored. `onScan` and `onKey` are
300
+ read from a ref, so a new function each render never re-binds the listener.
301
+ The hook does nothing on the server. A reader that types slower than 50 ms a
302
+ key (some Bluetooth modes) needs a larger `maxGap`.
303
+
304
+ Outside React, `createScanner({ maxGap?, minLength? })` from
305
+ `@nxgt/material-core/barcode-scanner` (or `@nxgt/material/hooks`, which re-exports it) is the pure detector:
306
+ `feed(key, time)` answers `{ kind: 'pass' }`, `{ kind: 'buffering' }` or
307
+ `{ kind: 'scan', code }`; `reset()` and `pending` complete it. The timing is
308
+ the caller's (`event.timeStamp`), so it is tested without a clock.
309
+ `attachBarcodeScanner(target, options)` is the DOM binding without React and
310
+ returns what removes the listener.
311
+
263
312
  ### `useCountdown`
264
313
 
265
314
  The clock behind [`CountdownTimer`](../components/feedback-and-status.md#countdowntimer),