@nxgt/material 2.4.2 → 2.6.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 (210) hide show
  1. package/README.md +28 -21
  2. package/dist/{_Uint8Array-DEXn42kj.js → _Uint8Array-CDlDeQmw.js} +1 -1
  3. package/dist/_overRest-CWIWQwDJ.js +81 -0
  4. package/dist/{abnfDiagram-O67JEVCF-C7NxOWqd.js → abnfDiagram-O67JEVCF-Cl4ukf6a.js} +4 -4
  5. package/dist/{architectureDiagram-NJMV4G6O-BIvwyzEh.js → architectureDiagram-NJMV4G6O-WmFlBQF5.js} +7 -7
  6. package/dist/{client-only.utils-DhcBUmau.js → async-slice-Qoen0Mqm.js} +164 -93
  7. package/dist/{blockDiagram-BEXU5L5S-CzgMFdtn.js → blockDiagram-BEXU5L5S-_6EVgQ2-.js} +11 -11
  8. package/dist/{c4Diagram-YGBWAQC7-WgqlRIL5.js → c4Diagram-YGBWAQC7-D9xSt2QO.js} +8 -8
  9. package/dist/{channel-Ba8pEAWx.js → channel-Dg92Hoym.js} +1 -1
  10. package/dist/{chunk-3FUC2YCW-DnIzetwB.js → chunk-3FUC2YCW-CRQHl-Jx.js} +8 -8
  11. package/dist/{chunk-5DYCD2WN-Bqi3iL5i.js → chunk-5DYCD2WN-Cja1INgd.js} +3 -3
  12. package/dist/{chunk-6AEJRKK7-DDXoGWDs.js → chunk-6AEJRKK7-3YWRP4aV.js} +2 -2
  13. package/dist/{chunk-742MDFTN-nG_ROTnT.js → chunk-742MDFTN-Cs1qGiTK.js} +2 -2
  14. package/dist/{chunk-7INBJB4K-Dg64CbhT.js → chunk-7INBJB4K-BY5n77ka.js} +6 -6
  15. package/dist/{chunk-7M6MHVWA-CPdb6LVf.js → chunk-7M6MHVWA-DqXpf6CM.js} +9 -9
  16. package/dist/{chunk-7PRAP22T-DIiYRAce.js → chunk-7PRAP22T-_Av9vcXz.js} +1 -1
  17. package/dist/{chunk-GNY47TPC-CxWZwDbp.js → chunk-GNY47TPC-BgMsLbCS.js} +11 -11
  18. package/dist/{chunk-GTNCS2PH-CzDYTYNy.js → chunk-GTNCS2PH-BZ9WMD1v.js} +3 -3
  19. package/dist/{chunk-GWA4HPMP-CmsxMX3A.js → chunk-GWA4HPMP-Co-A6dtV.js} +3 -3
  20. package/dist/{chunk-MBY4JIJT-IA2XBafa.js → chunk-MBY4JIJT-C3RQP_Nk.js} +4 -4
  21. package/dist/{chunk-NETBCI7D-Cq92-0cA.js → chunk-NETBCI7D-CsyH43bj.js} +1 -1
  22. package/dist/{chunk-O7XYJQB3-C87zK8E5.js → chunk-O7XYJQB3-A66QSOPA.js} +1 -1
  23. package/dist/{chunk-UA2S7LBM-B0LGFGu3.js → chunk-UA2S7LBM-BNeHeMx9.js} +6 -6
  24. package/dist/{chunk-WEXAMYUT-C-QP0DFq.js → chunk-WEXAMYUT-CucJfehv.js} +2 -2
  25. package/dist/{chunk-XXDRQBXY-Bq4sFnO2.js → chunk-XXDRQBXY-FgXU14Bh.js} +1 -1
  26. package/dist/{chunk-Z7XXMR3K-vh8Pvgr_.js → chunk-Z7XXMR3K-CoCjTVVx.js} +8 -8
  27. package/dist/{chunk-ZIGJFQKS-HDJ6e6OJ.js → chunk-ZIGJFQKS-C3xUAXJq.js} +3 -3
  28. package/dist/{classDiagram-v2-NBCMYWYE-BSyQx7Df.js → classDiagram-v2-NBCMYWYE-j4vk04t-.js} +14 -14
  29. package/dist/{components-C3jpOwPw.js → components-C3wIUrWw.js} +26931 -25668
  30. package/dist/components.js +6 -6
  31. package/dist/{cose-bilkent-JH36ORCC-11-T1rfk.js → cose-bilkent-JH36ORCC-DRUT3qBb.js} +2 -2
  32. package/dist/{cynefinDiagram-VND7K2PF-1kHi2rGa.js → cynefinDiagram-VND7K2PF-B9HrdQW8.js} +4 -4
  33. package/dist/{dagre-6A5THRUB-BDk37t1_.js → dagre-6A5THRUB-DXq_YBNL.js} +572 -613
  34. package/dist/{diagram-22UHCM2B-BvH24G-C.js → diagram-22UHCM2B-He8NDldw.js} +15 -15
  35. package/dist/{diagram-3UASUU5V-DAKVPT0Q.js → diagram-3UASUU5V-CeILva4U.js} +7 -7
  36. package/dist/{diagram-ATOU4E4O-Clju_0RY.js → diagram-ATOU4E4O-CNY-8kl0.js} +3 -3
  37. package/dist/{diagram-CDSNMT55-w7kkJ6MU.js → diagram-CDSNMT55-C8Mk1smi.js} +5 -5
  38. package/dist/{diagram-MLGK6HIB-mUOtRVmm.js → diagram-MLGK6HIB-CpfeILwW.js} +4 -4
  39. package/dist/{diagram-MPIPVDR6-DEUw58dq.js → diagram-MPIPVDR6-Do7bUnTd.js} +4 -4
  40. package/dist/{dist-gYpAi-su.js → dist-C3YK_ean.js} +1 -1
  41. package/dist/{dist-CeHNxLJG.js → dist-CRbPtqwD.js} +1 -1
  42. package/dist/dnd.js +1 -1
  43. package/dist/{ebnfDiagram-ZINNZB2B-BYfatie5.js → ebnfDiagram-ZINNZB2B-1GewX9Zq.js} +4 -4
  44. package/dist/{elk-276RUBZZ-CtsHzWCy.js → elk-276RUBZZ-DFn4kTey.js} +13 -13
  45. package/dist/{erDiagram-OPXOYQCR-DTrqayuU.js → erDiagram-OPXOYQCR-BzVUcUwh.js} +14 -14
  46. package/dist/{field-blur-EUOqpHbF.js → field-blur-BlrE9fOD.js} +1109 -1083
  47. package/dist/flowDiagram-KWPJA3E3-B4pJCj2Q.js +17 -0
  48. package/dist/{ganttDiagram-FUAMR5RP-DxWSt8R4.js → ganttDiagram-FUAMR5RP-B4A4UFFi.js} +6 -6
  49. package/dist/{gitGraphDiagram-X574FWY7-DcGaPSwy.js → gitGraphDiagram-X574FWY7-80yOq8dE.js} +3 -3
  50. package/dist/{graphlib-BOTXrGOf.js → graphlib-CQ3hLS1N.js} +103 -139
  51. package/dist/hooks.js +2 -2
  52. package/dist/{i18n-provider-XTTU9ArK.js → i18n-provider-CbYD2KxP.js} +1 -1
  53. package/dist/i18n.js +6 -6
  54. package/dist/{icon-button-Dl9PDYCE.js → icon-button-BxySFfrr.js} +581 -581
  55. package/dist/{infoDiagram-VRGFBTTK-Cvj167dl.js → infoDiagram-VRGFBTTK-Cboh-B9n.js} +3 -3
  56. package/dist/isArrayLikeObject-5rnYwEK7.js +28 -0
  57. package/dist/isEmpty-DChvniYe.js +53 -0
  58. package/dist/{ishikawaDiagram-OU5B5YK6-ChBwjCfr.js → ishikawaDiagram-OU5B5YK6-DwTd1YxL.js} +4 -4
  59. package/dist/{journeyDiagram-ZHPQQLJL-BycdRLE6.js → journeyDiagram-ZHPQQLJL-C91SPPkY.js} +4 -4
  60. package/dist/{kanban-definition-PNTS6WVX-BRoa-Yp5.js → kanban-definition-PNTS6WVX-S8plI4wn.js} +10 -10
  61. package/dist/lib/components/ui/actions/button/button.compound-variants.d.ts +114 -0
  62. package/dist/lib/components/ui/actions/button/button.d.ts +4 -7
  63. package/dist/lib/components/ui/actions/button/button.variants.d.ts +7 -0
  64. package/dist/lib/components/ui/actions/button/has-text-content.d.ts +10 -0
  65. package/dist/lib/components/ui/actions/icon-button/icon-button.variants.d.ts +1 -1
  66. package/dist/lib/components/ui/collections/data-table/data-table-context.d.ts +4 -0
  67. package/dist/lib/components/ui/collections/data-table/data-table-pagination.d.ts +3 -1
  68. package/dist/lib/components/ui/collections/data-table/data-table.d.ts +12 -2
  69. package/dist/lib/components/ui/collections/data-table/hooks.d.ts +13 -13
  70. package/dist/lib/components/ui/collections/data-table/partials/data-table-body.d.ts +9 -5
  71. package/dist/lib/components/ui/collections/data-table/partials/data-table-empty-row.d.ts +14 -0
  72. package/dist/lib/components/ui/collections/data-table/partials/data-table-skeleton-rows.d.ts +7 -0
  73. package/dist/lib/components/ui/collections/filter/filter-field-number-range.d.ts +15 -0
  74. package/dist/lib/components/ui/collections/pagination/index.d.ts +1 -1
  75. package/dist/lib/components/ui/collections/pagination/pagination.d.ts +9 -1
  76. package/dist/lib/components/ui/collections/tree/use-tree.d.ts +2 -0
  77. package/dist/lib/components/ui/dates/date-range-field/date-range-field-trigger.d.ts +1 -1
  78. package/dist/lib/components/ui/dates/date-range-field/date-range-field.d.ts +10 -1
  79. package/dist/lib/components/ui/dates/date-range-field/date-range-field.utils.d.ts +13 -0
  80. package/dist/lib/components/ui/dates/date-range-field/index.d.ts +1 -0
  81. package/dist/lib/components/ui/dates/date-range-field/partials/date-range-field-content.d.ts +25 -0
  82. package/dist/lib/components/ui/dates/date-range-field/partials/date-range-presets.d.ts +11 -0
  83. package/dist/lib/components/ui/dates/date-range-field/partials/date-range-times.d.ts +11 -0
  84. package/dist/lib/components/ui/dates/date-range-field/use-date-range-presets.d.ts +22 -0
  85. package/dist/lib/components/ui/dates/time-picker/time-picker-utils.d.ts +7 -0
  86. package/dist/lib/components/ui/display/chip/chip.d.ts +1 -1
  87. package/dist/lib/components/ui/feedback/toast/toast.d.ts +9 -4
  88. package/dist/lib/components/ui/fields/number-field/number-field.d.ts +3 -1
  89. package/dist/lib/components/ui/fields/otp-field/index.d.ts +1 -0
  90. package/dist/lib/components/ui/fields/otp-field/input-otp.d.ts +1 -1
  91. package/dist/lib/components/ui/fields/slider/slider-header.d.ts +19 -0
  92. package/dist/lib/components/ui/fields/slider/slider-readout.d.ts +5 -1
  93. package/dist/lib/components/ui/fields/slider/slider.d.ts +7 -0
  94. package/dist/lib/components/ui/fields/switch/switch.d.ts +7 -0
  95. package/dist/lib/components/ui/layout/stepper/stepper-step.d.ts +17 -0
  96. package/dist/lib/components/ui/media/camera/camera-content.d.ts +1 -2
  97. package/dist/lib/components/ui/media/camera/camera-context.d.ts +4 -0
  98. package/dist/lib/components/ui/media/camera/camera-provider.d.ts +2 -0
  99. package/dist/lib/components/ui/media/camera/camera.d.ts +6 -3
  100. package/dist/lib/components/ui/media/camera/capture-photo.d.ts +13 -0
  101. package/dist/lib/components/ui/media/camera/index.d.ts +9 -1
  102. package/dist/lib/components/ui/media/camera/state/camera.reducers.d.ts +1 -1
  103. package/dist/lib/components/ui/media/camera/state/camera.slice.d.ts +4 -9
  104. package/dist/lib/components/ui/media/camera/state/camera.store.d.ts +11 -5
  105. package/dist/lib/components/ui/media/camera/state/camera.thunks.d.ts +4 -4
  106. package/dist/lib/components/ui/media/camera/types.d.ts +19 -9
  107. package/dist/lib/components/ui/media/camera/use-camera.d.ts +5 -11
  108. package/dist/lib/components/ui/media/camera/utils/camera.utils.d.ts +0 -1
  109. package/dist/lib/components/ui/media/file-preview/file-preview-pdf.lazy.d.ts +9 -0
  110. package/dist/lib/components/ui/media/file-preview/file-preview.d.ts +12 -0
  111. package/dist/lib/components/ui/media/file-preview/index.d.ts +3 -0
  112. package/dist/lib/components/ui/media/file-preview/partials/file-preview-body.d.ts +9 -0
  113. package/dist/lib/components/ui/media/file-preview/partials/file-preview-card.d.ts +20 -0
  114. package/dist/lib/components/ui/media/file-preview/partials/file-preview-media.d.ts +11 -0
  115. package/dist/lib/components/ui/media/file-preview/partials/file-preview-pdf.d.ts +10 -0
  116. package/dist/lib/components/ui/media/file-preview/partials/file-preview-status.d.ts +17 -0
  117. package/dist/lib/components/ui/media/file-preview/partials/file-preview-text.d.ts +13 -0
  118. package/dist/lib/components/ui/media/file-preview/types.d.ts +23 -0
  119. package/dist/lib/components/ui/media/file-preview/use-file-preview.d.ts +22 -0
  120. package/dist/lib/components/ui/media/file-preview/use-text-preview.d.ts +15 -0
  121. package/dist/lib/components/ui/media/index.d.ts +2 -0
  122. package/dist/lib/components/ui/media/upload/network-upload-field/use-image-preview.d.ts +6 -0
  123. package/dist/lib/components/ui/overlays/dialog/dialog-outside.d.ts +5 -0
  124. package/dist/lib/components/ui/overlays/dialog/dialog-scroll-content.d.ts +9 -0
  125. package/dist/lib/components/ui/overlays/dialog/index.d.ts +1 -0
  126. package/dist/lib/components/ui/primitives/icon/icon-context.d.ts +1 -1
  127. package/dist/lib/components/ui/primitives/table/table.d.ts +6 -1
  128. package/dist/lib/components/ui/selection/index.d.ts +2 -0
  129. package/dist/lib/components/ui/selection/native-select/index.d.ts +1 -0
  130. package/dist/lib/components/ui/selection/native-select/native-select.d.ts +11 -0
  131. package/dist/lib/components/ui/selection/radio-group/radio-group.d.ts +8 -1
  132. package/dist/lib/components/ui/selection/tree-select-field/index.d.ts +2 -0
  133. package/dist/lib/components/ui/selection/tree-select-field/partials/tree-select-content.d.ts +13 -0
  134. package/dist/lib/components/ui/selection/tree-select-field/partials/tree-select-panel.d.ts +19 -0
  135. package/dist/lib/components/ui/selection/tree-select-field/partials/tree-select-trigger.d.ts +24 -0
  136. package/dist/lib/components/ui/selection/tree-select-field/tree-select-field.d.ts +9 -0
  137. package/dist/lib/components/ui/selection/tree-select-field/tree-select-form-field.d.ts +8 -0
  138. package/dist/lib/components/ui/selection/tree-select-field/tree-select.utils.d.ts +35 -0
  139. package/dist/lib/components/ui/selection/tree-select-field/types.d.ts +37 -0
  140. package/dist/lib/components/ui/selection/tree-select-field/use-tree-select-field.d.ts +20 -0
  141. package/dist/lib/i18n/resources/en/en.d.ts +40 -0
  142. package/dist/lib/i18n/resources/en/material.json.d.ts +43 -3
  143. package/dist/lib/i18n/resources/fr/fr.d.ts +40 -0
  144. package/dist/lib/i18n/resources/fr/material.json.d.ts +43 -3
  145. package/dist/lib/i18n/resources/resources.d.ts +80 -0
  146. package/dist/lib/i18n/translator.d.ts +1 -1
  147. package/dist/lib/lib/redux/status-slice.d.ts +1 -1
  148. package/dist/lib.js +295 -4
  149. package/dist/main.js +16 -16
  150. package/dist/map.js +5 -5
  151. package/dist/material.css +1 -1
  152. package/dist/merge-_Awk4ywA.js +156 -0
  153. package/dist/{mermaid.core-BGAQ3ZAZ.js → mermaid.core-DciYB1MV.js} +49 -49
  154. package/dist/{mindmap-definition-NLK3R4M7-HIqs4Eil.js → mindmap-definition-NLK3R4M7-Brf_2jIE.js} +13 -13
  155. package/dist/{pegDiagram-GJSIUBJH-Dcm3LCQX.js → pegDiagram-GJSIUBJH-CAO6aYvj.js} +4 -4
  156. package/dist/{pieDiagram-5QR66LMP-DQ6djEkW.js → pieDiagram-5QR66LMP-MPh-EWf-.js} +5 -5
  157. package/dist/print.js +2 -2
  158. package/dist/{quadrantDiagram-O4NWA36T-yUWorB72.js → quadrantDiagram-O4NWA36T-CwDeE9KB.js} +3 -3
  159. package/dist/{railroadDiagram-XR7U4H2S-CzOJlTUA.js → railroadDiagram-XR7U4H2S-DmKDXg8V.js} +4 -4
  160. package/dist/{requirementDiagram-PLB6GJNP-Cs5-8Nro.js → requirementDiagram-PLB6GJNP-CCP3YtvB.js} +13 -13
  161. package/dist/{sankeyDiagram-IPEJSGJF-CCqxyVoj.js → sankeyDiagram-IPEJSGJF-vgVchd2a.js} +3 -3
  162. package/dist/{sequenceDiagram-PO4LG4MO-DMLeIQ03.js → sequenceDiagram-PO4LG4MO-B2qb55A5.js} +5 -5
  163. package/dist/{sortable-provider-DuRzFvwe.js → sortable-provider-CzSnl6It.js} +17 -17
  164. package/dist/{src-CJ8DNrI1.js → src-C9kQSo6K.js} +1 -1
  165. package/dist/{stateDiagram-v2-GCMORJYK-D-8sLHg8.js → stateDiagram-v2-GCMORJYK-T-oIXh-8.js} +14 -14
  166. package/dist/{stored-language-BP90Ydtg.js → stored-language-DmUUhRb5.js} +90 -6
  167. package/dist/{swimlanes-2SLR337P-DM_lDm6v.js → swimlanes-2SLR337P-tW3_wBWO.js} +12 -12
  168. package/dist/swimlanesDiagram-TC7HE7FX-B1lWzHp1.js +28 -0
  169. package/dist/{timeline-definition-EJHVYXUP-D3Tz-JnK.js → timeline-definition-EJHVYXUP-SdA6wt2-.js} +5 -5
  170. package/dist/{translate-material-Ctzt0Y2N.js → translate-material-DuY12xca.js} +2 -2
  171. package/dist/{translator-Cfug7hC5.js → translator-CaIYjyFP.js} +1 -1
  172. package/dist/{use-heading-spy-DtVo2HBk.js → use-heading-spy-Crdz4OH5.js} +2 -2
  173. package/dist/{use-material-translation-CJswHOmK.js → use-material-translation-Od0QsFPA.js} +2 -2
  174. package/dist/{usecaseDiagram-POWQR4AR-Cxx8PDrr.js → usecaseDiagram-POWQR4AR-B3rBcR4E.js} +13 -13
  175. package/dist/{vennDiagram-UO4OBE2U-Aql8mhcy.js → vennDiagram-UO4OBE2U-CO0yFCI8.js} +4 -4
  176. package/dist/{wardleyDiagram-VNRHLVJA-tknlhG_t.js → wardleyDiagram-VNRHLVJA-QkwghAYo.js} +4 -4
  177. package/dist/{xychartDiagram-PMCCYNJV-CM0QlWD5.js → xychartDiagram-PMCCYNJV-stQWLgCx.js} +7 -7
  178. package/docs/components/README.md +2 -1
  179. package/docs/components/buttons-and-actions.md +8 -3
  180. package/docs/components/data-display.md +4 -4
  181. package/docs/components/data-table-filter-and-lists.md +100 -6
  182. package/docs/components/date-and-time.md +106 -6
  183. package/docs/components/feedback-and-status.md +4 -3
  184. package/docs/components/form-fields.md +55 -5
  185. package/docs/components/media-and-files.md +117 -8
  186. package/docs/components/navigation-and-layout.md +6 -1
  187. package/docs/components/overlays-and-menus.md +27 -2
  188. package/docs/components/primitives.md +34 -3
  189. package/docs/components/rich-widgets.md +1 -1
  190. package/docs/components/selection-and-choice.md +111 -8
  191. package/docs/guides/forms.md +2 -1
  192. package/docs/guides/i18n.md +58 -9
  193. package/docs/guides/ssr.md +3 -1
  194. package/docs/previews/date-time-daterangefield.webp +0 -0
  195. package/docs/previews/media-files-camera.webp +0 -0
  196. package/docs/previews/media-files-filepreview.webp +0 -0
  197. package/docs/previews/navigation-layout-stepper.webp +0 -0
  198. package/docs/previews/selection-choice-treeselectfield.webp +0 -0
  199. package/docs/roadmap.md +42 -3
  200. package/docs/troubleshooting.md +167 -0
  201. package/package.json +2 -2
  202. package/dist/flowDiagram-KWPJA3E3-B8Isvcc5.js +0 -17
  203. package/dist/isArrayLikeObject-CCJDO2RL.js +0 -86
  204. package/dist/lib/components/ui/fields/slider/slider-ticks.d.ts +0 -1
  205. package/dist/lib/components/ui/media/camera/camera.provider.d.ts +0 -6
  206. package/dist/lib/components/ui/media/camera/state/index.d.ts +0 -2
  207. package/dist/lib/components/ui/media/camera/utils/index.d.ts +0 -1
  208. package/dist/lib-DIj5dvbb.js +0 -367
  209. package/dist/merge-Dm-kHFxo.js +0 -172
  210. package/dist/swimlanesDiagram-TC7HE7FX-CbV1TsML.js +0 -28
@@ -23,7 +23,7 @@ each other, so either can be used alone.
23
23
 
24
24
  ![DataTable](../previews/data-table-filter-lists-datatable.webp)
25
25
 
26
- TanStack Table v9 with sorting, filtering, selection, expandable rows, pagination or load-more, and column visibility.
26
+ TanStack Table v9 with sorting, filtering, selection, expandable rows, pagination or load-more, column visibility, skeleton rows while loading and your own empty state.
27
27
 
28
28
  ```tsx
29
29
  import {
@@ -52,7 +52,8 @@ function PeopleTable({ people }: { people: Person[] }) {
52
52
 
53
53
  Parts: `DataTable`; `DataTableColumnHeader` (a sortable header with a hide
54
54
  action); `DataTablePagination` and `DataTableViewOptions` (used by the table,
55
- exported for a table of your own); `useColumnHelpers<T>()` returns a
55
+ exported for a table of your own; `DataTablePagination` takes `disabled`);
56
+ `useColumnHelpers<T>()` returns a
56
57
  `columnHelper` bound to the table's features and `predefinedColumns()` —
57
58
  `select` (checkboxes) and `expand` (a toggle for `renderExpandedRow`, named
58
59
  "Toggle row details" (`material.table.labels.toggle-row`) with `aria-expanded`);
@@ -71,6 +72,61 @@ table enables, for typing your own column defs.
71
72
  | `onLoadMore` / `isLoadingMore` | `() => void` / `boolean` | — | adds a "load more" button under the rows |
72
73
  | `options` | `{ showPage?: boolean; showSelected?: boolean }` | both `true` | parts of the footer |
73
74
  | `showHeader` | `boolean` | hidden on mobile | header row |
75
+ | `loading` | `boolean` | `false` | skeleton rows, or the current rows dimmed; `aria-busy`; controls disabled |
76
+ | `loadingRows` | `number` | the page size | how many skeleton rows |
77
+ | `empty` | `ReactNode \| ({ filtered }) => ReactNode` | icon and "No data available" | content of the empty row, spanning every visible column |
78
+
79
+ ### Loading and empty states
80
+
81
+ ```tsx
82
+ import { DataTable, useColumnHelpers } from '@nxgt/material/components';
83
+
84
+ type Person = { id: string; name: string; email: string };
85
+
86
+ function PeopleTable({
87
+ people,
88
+ isFetching,
89
+ }: {
90
+ people: Person[];
91
+ isFetching: boolean;
92
+ }) {
93
+ const { columnHelper } = useColumnHelpers<Person>();
94
+ const columns = [
95
+ columnHelper.accessor('name', { header: 'Name' }),
96
+ columnHelper.accessor('email', { header: 'Email' }),
97
+ ];
98
+
99
+ return (
100
+ <DataTable
101
+ data={people}
102
+ columns={columns}
103
+ loading={isFetching}
104
+ loadingRows={5}
105
+ empty={({ filtered }) =>
106
+ filtered ? 'No person matches these filters.' : 'No people yet.'
107
+ }
108
+ />
109
+ );
110
+ }
111
+ ```
112
+
113
+ While `loading` is `true`, the table is `aria-busy` and a visually
114
+ hidden status says "Loading data…" (`material.table.loading`). With no row to
115
+ show, the body is `loadingRows` skeleton rows (default: the page size), one
116
+ cell per visible column. With rows already shown — a refresh — the rows stay
117
+ where they are, dimmed and `inert`, rather than being swapped for
118
+ skeletons: the table keeps its height, so nothing below it jumps when the data
119
+ arrives. Sorting (`DataTableColumnHeader`), the `select` column's "Select
120
+ all", the toolbar's native controls (it
121
+ is a disabled `<fieldset>`, so every input or button you render there, a
122
+ filter or an export, is disabled
123
+ too) and pagination are disabled meanwhile.
124
+
125
+ When no row is left after filtering, the empty state is one `TableEmpty`
126
+ cell spanning every visible column: today's icon and "No data available" by
127
+ default, or your own content, told whether rows exist but none matches the
128
+ filters (`filtered`); content that renders nothing gets the default.
129
+ `DataTableEmpty` and `DataTableEmptyState` type the prop and its argument.
74
130
 
75
131
  For the TanStack API itself (column helpers, features, row models), see
76
132
  [TanStack Table v9](https://tanstack.com/table/latest).
@@ -116,7 +172,10 @@ const schema: FilterSchema = {
116
172
  A field's `type` is one of `text`, `number`, `select`, `multi-select`, `date`,
117
173
  `date-range`, `number-range`, `checkbox-group`, `radio-group`,
118
174
  `chips-multiple`, `chips-single`, `slider`, `slider-range`, `switch` or
119
- `custom`; `schema.groups` puts fields in collapsible sections.
175
+ `custom`; `schema.groups` puts fields in collapsible sections. A
176
+ `number-range` or `slider-range` is a `role="group"` named by its `label`,
177
+ with its inputs named by their part ("Min" / "Max", or the slider's
178
+ "<label> Minimum" / "<label> Maximum"): `getByRole('group', { name: 'Price' })`.
120
179
 
121
180
  Parts: `Filter` (one component, `variant` picks the layout);
122
181
  `FilterProvider` + `FilterContent` + `FilterActions` to lay the panel out
@@ -174,9 +233,39 @@ import { PaginationBar } from '@nxgt/material/components';
174
233
  <PaginationBar page={page} pageCount={12} onPageChange={setPage} />
175
234
  ```
176
235
 
236
+ ```tsx
237
+ import {
238
+ Pagination,
239
+ PaginationContent,
240
+ PaginationFirst,
241
+ PaginationItem,
242
+ PaginationLast,
243
+ PaginationLink,
244
+ } from '@nxgt/material/components';
245
+
246
+ <Pagination>
247
+ <PaginationContent>
248
+ <PaginationItem>
249
+ <PaginationFirst href="?page=1" />
250
+ </PaginationItem>
251
+ <PaginationItem>
252
+ <PaginationLink href="?page=2" isActive>
253
+ 2
254
+ </PaginationLink>
255
+ </PaginationItem>
256
+ <PaginationItem>
257
+ <PaginationLast href="?page=12" />
258
+ </PaginationItem>
259
+ </PaginationContent>
260
+ </Pagination>;
261
+ ```
262
+
177
263
  Parts: `Pagination`, `PaginationContent`, `PaginationItem`,
178
- `PaginationLink` (`isActive`), `PaginationPrevious`, `PaginationNext`,
179
- `PaginationEllipsis` for link-based pagination; `PaginationBar` for the
264
+ `PaginationLink` (`isActive`), `PaginationFirst`, `PaginationPrevious`,
265
+ `PaginationNext`, `PaginationLast`, `PaginationEllipsis` for link-based
266
+ pagination (first / previous / next / last take an optional `label` and
267
+ `icon`; their names are translated, `material.pagination.*`, and the text
268
+ hides below `sm`); `PaginationBar` for the
180
269
  stateful one; `paginationItems(page, pageCount, siblingCount)` for the
181
270
  window alone.
182
271
 
@@ -188,7 +277,7 @@ window alone.
188
277
  | `siblingCount` | `number` | `1` | pages shown on each side of the current one |
189
278
  | `disabled` | `boolean` | — | disables every button |
190
279
 
191
- **Storybook:** `Data table, filter & lists/Pagination` · **Vue:** `Pagination` (parts), `PaginationFirst`, `PaginationLast`
280
+ **Storybook:** `Data table, filter & lists/Pagination` · **Vue:** `Pagination` (parts), `PaginationFirst`, `PaginationLast`, `PaginationLink`, `PaginationBar`, `paginationItems`
192
281
 
193
282
  ---
194
283
 
@@ -230,6 +319,11 @@ own) and `TreeItem` (one row); `findTreeNode` and `flattenVisibleTree`.
230
319
  Arrow keys move and expand / collapse, Home / End jump, Enter and Space
231
320
  select.
232
321
 
322
+ Tab lands on the focused row, or on the first visible row when that one is
323
+ filtered or folded away; mounting a `Tree` does not move focus. `useTree`
324
+ returns that row as `tabbableId`: give it `tabindex="0"` in a tree you build
325
+ yourself.
326
+
233
327
  **Storybook:** `Data table, filter & lists/Tree` · **Vue:** `Tree`, `TreeItem`, `useTree`
234
328
 
235
329
  ---
@@ -105,7 +105,7 @@ calendar is labelled by the trigger, so named by the label.
105
105
  **Emits:** `Date | undefined`.
106
106
  **Form wrapper:** `DateFormField` — `control` and `name`, see
107
107
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
108
- **Storybook:** `Date & time/DateField` · **Vue:** `DateField`, `DateFormField`
108
+ **Storybook:** `Date & time/DateField` · **Vue:** `DateField`, `DateFormField` (`disabled-days`, same rules)
109
109
 
110
110
  ---
111
111
 
@@ -113,7 +113,7 @@ calendar is labelled by the trigger, so named by the label.
113
113
 
114
114
  ![DateRangeField](../previews/date-time-daterangefield.webp)
115
115
 
116
- From / to range picked in a popover calendar.
116
+ From / to range picked in a popover calendar, with presets and times.
117
117
 
118
118
  ```tsx
119
119
  import { type DateRange, DateRangeField } from '@nxgt/material/components';
@@ -128,10 +128,13 @@ export function Stay() {
128
128
  | Prop | Type | Default | Effect |
129
129
  | --- | --- | --- | --- |
130
130
  | `value` | `DateRange` | — | `{ from: Date \| undefined; to?: Date }` |
131
- | `onValueChange` | `(value?: DateRange) => void` | — | called on each click: `to` is unset after the first |
131
+ | `onValueChange` | `(value?: DateRange) => void` | — | called on each click: after the first, `to` is the same day as `from` |
132
132
  | `placeholder`, `label`, `helperText`, `error`, `required` | | — | `placeholder` is the trigger's text; `label` and `helperText` sit beside it; `error` and `required` its state |
133
133
  | `disabled` | `boolean` | — | disables the field: the trigger cannot be focused or opened |
134
134
  | `disabledDays` | react-day-picker's `Matcher \| Matcher[]` | — | days that cannot be picked (the calendar's `disabled`) |
135
+ | `presets` | `readonly DateRangePresetOption[]` | — (no list) | one-click ranges beside the calendar: built-in ids (`DATE_RANGE_PRESET_IDS`) and `{ label, range }` of your own |
136
+ | `withTime` | `boolean` | `false` | a time of day on each end, under the calendar, and on the trigger; the popover stays open until OK |
137
+ | `minDate`, `maxDate` | `Date` | — | the first and last day that can be picked (the calendar's, as are `disablePast` / `disableFuture`) |
135
138
 
136
139
  Every other `Calendar` prop is passed to the calendar; it opens on the
137
140
  month of `value.from`. `id`, `aria-describedby` and `data-*` attributes go on
@@ -148,11 +151,103 @@ trigger in the field's root `div` (`data-slot="date-range-field"`, which takes
148
151
  rendered alone, shows only the value: use the field for the label and the
149
152
  helper text. The open calendar is labelled by the trigger.
150
153
 
151
- **Emits:** `DateRange | undefined` — `{ from, to }`, where `to` stays
152
- `undefined` until the second day is picked.
154
+ ### Presets
155
+
156
+ `presets` lists one-click ranges beside the calendar (above it on a narrow
157
+ screen), in the order you give them. A built-in id is translated
158
+ (`material.date-range-field.preset.<id>`); a preset of your own is
159
+ `{ label, range }`, where `range` is `{ from, to }` or `(now) => ({ from, to })`.
160
+
161
+ ```tsx
162
+ import {
163
+ type DateRange,
164
+ DateRangeField,
165
+ type DateRangePreset,
166
+ } from '@nxgt/material/components';
167
+ import { useState } from 'react';
168
+
169
+ const lastQuarter: DateRangePreset = {
170
+ label: 'Last quarter',
171
+ range: (now) => {
172
+ const start = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3 - 3, 1);
173
+ return { from: start, to: new Date(start.getFullYear(), start.getMonth() + 3, 0) };
174
+ },
175
+ };
176
+
177
+ export function Report() {
178
+ const [period, setPeriod] = useState<DateRange | undefined>();
179
+ return (
180
+ <DateRangeField
181
+ label="Period"
182
+ value={period}
183
+ onValueChange={setPeriod}
184
+ maxDate={new Date()}
185
+ presets={['today', 'last-7-days', 'this-month', 'last-month', lastQuarter]}
186
+ />
187
+ );
188
+ }
189
+ ```
190
+
191
+ | Built-in id | Range |
192
+ | --- | --- |
193
+ | `today`, `yesterday` | that day |
194
+ | `last-7-days`, `last-30-days` | ends today and counts it |
195
+ | `this-month`, `this-year` | from the first day to today |
196
+ | `last-month` | the whole previous month |
197
+
198
+ A built-in range starts at 00:00 (and with `withTime` ends at 23:59); your
199
+ own ranges keep the times you gave them. Picking a preset sets the value and
200
+ closes the popover (with `withTime` it stays open); open again, the calendar
201
+ is on that range's month. The list is a group named "Presets"
202
+ (`material.date-range-field.presets`) of buttons in the tab order, and the
203
+ one whose days are the value's has `aria-pressed="true"`. A preset is
204
+ disabled when its range starts before `minDate`, ends after `maxDate`, or
205
+ either end is one of `disabledDays`; the days between are not checked, as the
206
+ calendar lets a range span a disabled day.
207
+ The built-in presets use the current date when the popover opens; there is
208
+ no `now` prop, and none is planned. A preset of your own whose end comes
209
+ before its start is disabled.
210
+
211
+ ### Times
212
+
213
+ `withTime` adds two `TimePicker`s under the calendar, in groups named
214
+ "Start time" and "End time" (`material.date-range-field.start-time`,
215
+ `.end-time`), each disabled until its end has a day, and an OK button
216
+ (`material.buttons.ok`) that closes the popover. The trigger shows each end
217
+ with its time (date-fns `Pp`).
218
+
219
+ ```tsx
220
+ import { type DateRange, DateRangeField } from '@nxgt/material/components';
221
+ import { useState } from 'react';
222
+
223
+ export function Booking() {
224
+ const [slot, setSlot] = useState<DateRange | undefined>();
225
+ return (
226
+ <DateRangeField
227
+ label="Booking"
228
+ value={slot}
229
+ onValueChange={setSlot}
230
+ withTime
231
+ presets={['today']}
232
+ />
233
+ );
234
+ }
235
+ ```
236
+
237
+ A day picked on the calendar keeps the time that end had; a new start is
238
+ 00:00 and a new end 23:59, as a built-in preset ends, so a filter on
239
+ `<= to` keeps the last day. On the same day, an end time set before the
240
+ start time becomes the start time.
241
+
242
+ Without `withTime`, a picked day is at 00:00. (`@nxgt/material-vue` gives
243
+ it the current time of day.)
244
+
245
+ **Emits:** `DateRange | undefined` — `{ from, to }`, where the first day
246
+ picked is both `from` and `to` (react-day-picker's `addToRange`) until the
247
+ second day is picked.
153
248
  **Form wrapper:** `DateRangeFormField` — `control` and `name`, see
154
249
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
155
- **Storybook:** `Date & time/DateRangeField` · **Vue:** `DateRangeField`, `DateRangeFormField`
250
+ **Storybook:** `Date & time/DateRangeField` (stories `DisabledDays`, `MinMaxDates`, `Presets`, `DisabledPresets`, `WithTime`) · **Vue:** `DateRangeField`, `DateRangeFormField` (`disabled-days`, `presets`, `with-time`, `min-date` / `max-date`; without time, a picked day carries the current time of day there, 00:00 here)
156
251
 
157
252
  ---
158
253
 
@@ -235,6 +330,11 @@ export function Clock() {
235
330
  | `label` | `ReactNode` | — | header text |
236
331
  | `footer` | `ReactNode` | — | under the inputs (the OK button in `TimeField`) |
237
332
 
333
+ The hours and minutes inputs are named "Hours" and "Minutes"
334
+ (`material.time-picker.hours`, `.minutes`). AM / PM shows the half of the day
335
+ `date` is in, so a date changed from outside shows its own; switching it moves
336
+ the time 12 hours, and a click on the pressed half changes nothing.
337
+
238
338
  **Emits:** `Date | undefined`. No form wrapper — use `TimeFormField`.
239
339
  **Storybook:** `Date & time/TimePicker` · **Vue:** `TimePicker`
240
340
 
@@ -227,7 +227,7 @@ built on `AlertDialog`, so it does not close on an outside click. Put
227
227
  `AlertDialogCancel` / `AlertDialogAction` in its `footer`.
228
228
 
229
229
  **Storybook:** `Feedback & status/ConfirmDialog`, `Feedback & status/ConfirmationDialog`,
230
- `Feedback & status/CustomAlertDialog` · **Vue:** `ConfirmDialog`, `ConfirmationDialog`, `CustomAlertDialog`
230
+ `Feedback & status/CustomAlertDialog` · **Vue:** `ConfirmDialog`, `ConfirmationDialog` (`form-options` + `#form` slot for `renderForm`), `CustomAlertDialog`
231
231
 
232
232
  ---
233
233
 
@@ -415,7 +415,7 @@ toasts.dismiss(id);
415
415
  | --- | --- | --- | --- |
416
416
  | `title` / `description` | `ReactNode` | preset title for `success`, `error`, `loading` | toast text |
417
417
  | `icon` | `ReactNode` (`toast`) · icon name (`toasts.custom`) | preset icon | leading icon |
418
- | `action` | `{ label, onClick? }` or `(dismiss) => ReactNode` | a translated “Close” for `success`, `info`, `error` | trailing button |
418
+ | `action` | `ToastAction`: `{ label, onClick? }` or `(dismiss) => ReactNode` | a translated “Close” for `success`, `info`, `error` | trailing button: `onClick`, then the toast closes; or what the function renders, given `dismiss` |
419
419
  | `options` | Sonner `ExternalToast` | `duration: 5000` for presets, `Infinity` for `loading` | passed to Sonner (`id`, `duration`, `position`…) |
420
420
 
421
421
  The toast id defaults to the title, so two toasts with the same title
@@ -423,7 +423,8 @@ replace each other; pass `options: { id }` to show both. `Toaster` is Sonner's
423
423
  and takes its props (`position`, `expand`, `richColors`…). `Toast` is the
424
424
  component each toast renders, for use with `sonnerToast.custom`.
425
425
 
426
- **Storybook:** `Feedback & status/Toast` · **Vue:** `Toaster`, `toast`, `toasts`, `Toast`
426
+ **Types:** `ToastAction`, `ToastProps`, `ToastMethodProps`.
427
+ **Storybook:** `Feedback & status/Toast` · **Vue:** `Toaster`, `toast`, `toasts`, `Toast` (`ToastAction` type)
427
428
 
428
429
  ---
429
430
 
@@ -218,6 +218,7 @@ export function Quantity() {
218
218
  | `hideSteppers` | `boolean` | `false` | hides the +/− buttons |
219
219
  | `suffix` | `ReactNode` | — | unit after the input |
220
220
  | `aria-label` | `string` | — | names the input when it has no `label` (a part of a larger field) |
221
+ | `aria-labelledby` | `string` | — | names the input by another element's id (a part of a larger field) |
221
222
 
222
223
  **Emits:** `number | null`.
223
224
  **Form wrapper:** `NumberFormField` — `control` and `name`, see
@@ -412,10 +413,41 @@ export function Code() {
412
413
  | `error` | `boolean` | — | error colour on the slots |
413
414
  | `id` | `string` | from `useId()` | the input's id, which the label points to; the generated one is the same on the server and the client |
414
415
 
416
+ `InputOTP`, `InputOTPGroup`, `InputOTPSlot` and `InputOTPSeparator` are the
417
+ parts `OtpField` is built from (`input-otp`), for a layout of your own — no
418
+ label, no helper text:
419
+
420
+ ```tsx
421
+ import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from '@nxgt/material/components';
422
+ import { useState } from 'react';
423
+
424
+ export function Pin() {
425
+ const [pin, setPin] = useState('');
426
+ return (
427
+ <InputOTP aria-label="PIN" maxLength={4} value={pin} onChange={setPin}>
428
+ <InputOTPGroup>
429
+ <InputOTPSlot index={0} />
430
+ <InputOTPSlot index={1} />
431
+ </InputOTPGroup>
432
+ <InputOTPSeparator />
433
+ <InputOTPGroup>
434
+ <InputOTPSlot index={2} />
435
+ <InputOTPSlot index={3} />
436
+ </InputOTPGroup>
437
+ </InputOTP>
438
+ );
439
+ }
440
+ ```
441
+
442
+ `InputOTP` takes `input-otp`'s `OTPInput` props (`maxLength`, `value`,
443
+ `onChange`, `pattern`, …) plus `containerClassName`; `InputOTPSlot` takes the
444
+ `index` it shows; `InputOTPSeparator` is a `separator` with a dash, or your
445
+ children.
446
+
415
447
  **Emits:** `string` (the digits typed so far).
416
448
  **Form wrapper:** `OtpFormField` — `control` and `name`, see
417
449
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
418
- **Storybook:** `Form fields/OtpField` · **Vue:** `OtpField`, `OtpFormField`, `InputOTP`
450
+ **Storybook:** `Form fields/OtpField` · **Vue:** `OtpField`, `OtpFormField`, `InputOTP` (same parts)
419
451
 
420
452
  ---
421
453
 
@@ -475,10 +507,13 @@ import { InlineEdit } from '@nxgt/material/components';
475
507
  | `placeholder` | `string` | "Click to edit" | shown while empty |
476
508
  | `disabled` | `boolean` | `false` | read-only text |
477
509
 
510
+ Enter, Escape, Save and Cancel return focus to the field, while a click
511
+ elsewhere saves and leaves focus where it went.
512
+
478
513
  **Emits:** `string`, trimmed, on save only.
479
514
  **Form wrapper:** `InlineEditFormField` — `control` and `name`, see
480
515
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
481
- **Storybook:** `Form fields/InlineEdit` · **Vue:** `InlineEdit`
516
+ **Storybook:** `Form fields/InlineEdit` · **Vue:** `InlineEdit`, `InlineEditFormField`
482
517
 
483
518
  ---
484
519
 
@@ -605,15 +640,25 @@ import { Slider } from '@nxgt/material/components';
605
640
  Each thumb (`role="slider"`) is named by an `aria-labelledby` or `aria-label`
606
641
  you pass, else by the label (a vertical slider does not show it, and it still
607
642
  names the thumbs); a range's thumbs add "Minimum" and "Maximum":
608
- `getByRole('slider', { name: 'Price Minimum' })`. `aria-invalid` (or `error`)
609
- goes on every thumb too, not on the root `span`.
643
+ `getByRole('slider', { name: 'Price Minimum' })`. A labelled range is also a
644
+ `role="group"` named by the label, and its readout inputs are named
645
+ "Minimum" and "Maximum" (`material.slider.minimum` / `maximum`), so two range
646
+ sliders are told apart. A single value's readout input is named as its thumb
647
+ is, and "Value" (`material.slider.value`) when nothing names the slider.
648
+ `aria-invalid` (or `error`) goes on every thumb too,
649
+ not on the root `span`.
650
+
651
+ `SliderField` is the same component under `@nxgt/material-vue`'s name (an
652
+ alias, `SliderFieldProps` for its props), so code reads the same in both
653
+ libraries; neither name is deprecated, and the full naming alignment is
654
+ planned for 3.0.
610
655
 
611
656
  **Emits:** `number[]`. `SliderFormField` stores a plain `number` when the
612
657
  form's value is a number, and `number[]` when it is an array — the default
613
658
  value decides.
614
659
  **Form wrapper:** `SliderFormField` — `control` and `name`, see
615
660
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
616
- **Storybook:** `Form fields/Slider` · **Vue:** `Slider`, `SliderField`, `SliderFormField`
661
+ **Storybook:** `Form fields/Slider` · **Vue:** `Slider`, `SliderField` (`ticks`, `leading` / `trailing` or their slots), `SliderFormField`
617
662
 
618
663
  ---
619
664
 
@@ -656,6 +701,11 @@ while the switch is on, from `checked` or, uncontrolled, from `defaultChecked` a
656
701
  `[data-checked="true"]` (a bare `[data-checked]` also matches rows that are
657
702
  off). `data-filed` says the same and is deprecated: it goes in 3.0.
658
703
 
704
+ `SwitchField` is the same component under `@nxgt/material-vue`'s name (an
705
+ alias, `SwitchFieldProps` for its props), so code reads the same in both
706
+ libraries; neither name is deprecated, and the full naming alignment is
707
+ planned for 3.0.
708
+
659
709
  **Emits:** `boolean`.
660
710
  **Form wrapper:** `SwitchFormField` — `control` and `name`, see
661
711
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
@@ -11,6 +11,7 @@ them into a form.
11
11
  | <img src="../previews/media-files-s3uploadfield.webp" width="96" alt=""> | [S3UploadField, NetworkUploadField](#s3uploadfield-networkuploadfield) | Drag-and-drop uploader with progress, retry, previews and size / count limits |
12
12
  | <img src="../previews/media-files-uploadfield.webp" width="96" alt=""> | [UploadField](#uploadfield) | Labelled file input showing the chosen file's name |
13
13
  | <img src="../previews/media-files-filelist.webp" width="96" alt=""> | [FileList](#filelist) | File list with type icons, size, download and remove |
14
+ | <img src="../previews/media-files-filepreview.webp" width="96" alt=""> | [FilePreview](#filepreview) | One file previewed by its type: image, PDF, video, audio, text, or a download card |
14
15
  | | [FilesFormField](#filesformfield) | Free-solo autocomplete of file URLs, with image thumbnails |
15
16
  | <img src="../previews/media-files-imagefield.webp" width="96" alt=""> | [ImageField](#imagefield) | Single image upload with preview and progress |
16
17
  | <img src="../previews/media-files-imagecropper.webp" width="96" alt=""> | [ImageCropper](#imagecropper) | Crop with zoom, rotation, aspect presets and a round shape |
@@ -19,6 +20,7 @@ them into a form.
19
20
  | <img src="../previews/media-files-pdfreader.webp" width="96" alt=""> | [PdfReader](#pdfreader) | Browser-only PDF reader with zoom, search, thumbnails and print |
20
21
  | <img src="../previews/media-files-stxplayer.webp" width="96" alt=""> | [StxPlayer](#stxplayer) | Video / audio player with a queue, captions and mini layout |
21
22
  | | [VideoPlayer](#videoplayer) | Deprecated alias of `StxPlayer` |
23
+ | <img src="../previews/media-files-camera.webp" width="96" alt=""> | [Camera](#camera) | Webcam preview with a start / stop toggle and a still capture |
22
24
  | | [Print components](#print-components) | `PrintDocument` page shell and `@react-pdf/renderer` building blocks for PDF documents, and `downloadPdf`, `openPdf`, `usePdfFile` and `pdfResponse` to get the file |
23
25
  | <img src="../previews/media-files-map.webp" width="96" alt=""> | [Map](#map) | Leaflet map, markers and a location field (`@nxgt/material/map`) |
24
26
 
@@ -220,6 +222,75 @@ dropzone as well read the error twice, and the group still says it once
220
222
 
221
223
  ---
222
224
 
225
+ ## FilePreview
226
+
227
+ ![FilePreview](../previews/media-files-filepreview.webp)
228
+
229
+ Previews one file, picked by its MIME type (else its extension): an image, a PDF, a video or audio file, the text of a text, JSON, CSV or Markdown file, and a download card for anything else.
230
+
231
+ ```tsx
232
+ import { useState } from 'react';
233
+ import { FilePreview } from '@nxgt/material/components';
234
+
235
+ export function Attachment() {
236
+ const [file, setFile] = useState<File | null>(null);
237
+ return (
238
+ <>
239
+ <input type="file" onChange={(event) => setFile(event.target.files?.[0] ?? null)} />
240
+ {/* A File or Blob: shown through an object URL, revoked on change and unmount. */}
241
+ <FilePreview file={file} onError={(error) => console.warn(error)} />
242
+ {/* A URL: the kind comes from `type`, else the extension; `name` is the alt text. */}
243
+ <FilePreview file="/api/files/42" type="application/pdf" name="contract.pdf" />
244
+ </>
245
+ );
246
+ }
247
+ ```
248
+
249
+ | Prop | Type | Default | Effect |
250
+ | --- | --- | --- | --- |
251
+ | `file` | `File \| Blob \| string \| null` | — | the file, or its URL; nothing renders without one. A URL must be `http(s):`, relative, `blob:`, or a `data:` URL of an image, video, audio, plain text, CSV, Markdown, JSON or PDF; any other (`javascript:`, `data:text/html`, and also a `data:` URL of XML, TSV or YAML) shows the error state, with no link and nothing loaded |
252
+ | `type` | `string` | the `Blob`'s, or a `data:` URL's | MIME type; picks the kind before the extension does |
253
+ | `name` | `string` | the `File`'s, else the URL's last segment | shown on the card, the image's alt text and the download's file name; "Untitled file" when there is none |
254
+ | `maxTextBytes` | `number` | `262144` (`TEXT_PREVIEW_MAX_BYTES`, 256 KB) | how much of a text kind is read; past it, the preview shows the beginning and says so |
255
+ | `onError` | `(error: unknown) => void` | — | the file could not be read (an HTTP error: `FilePreviewHttpError`, with its `status`; a refused URL: `FilePreviewUnsafeUrlError`) or shown (a broken image, video or audio file) |
256
+ | `className`, other `<figure>` props | | | on the root `<figure>` |
257
+
258
+ | Kind | Picked by | Renders |
259
+ | --- | --- | --- |
260
+ | `image` | `image/*`; `.png` `.jpg` `.jpeg` `.gif` `.webp` `.avif` `.svg` `.bmp` `.ico` | `<img>`, its alt text the file name |
261
+ | `pdf` | `application/pdf`; `.pdf` | [`PdfReader`](#pdfreader), imported the first time a PDF is previewed; the card when the viewer cannot be loaded. A PDF the reader fails to open leaves the preview `ready`: `PdfReader` shows its own error, and `onError` is not called |
262
+ | `video` | `video/*`; `.mp4` `.m4v` `.webm` `.ogv` `.mov` | `<video controls>` |
263
+ | `audio` | `audio/*`; `.mp3` `.wav` `.ogg` `.oga` `.m4a` `.aac` `.flac` `.opus` | `<audio controls>` |
264
+ | `json` | `application/json`, `text/json`, `*+json`; `.json` | [`JsonViewer`](data-display.md#codeblock) |
265
+ | `markdown` | `text/markdown`, `text/x-markdown`; `.md` `.markdown` | [`Markdown`](data-display.md#markdown), rendered; its links are sanitized like any `Markdown`, but a remote image in it still loads, so an untrusted `.md` can make the browser request a URL of its choosing |
266
+ | `csv` | `text/csv`, `application/csv`, `text/tab-separated-values`; `.csv` `.tsv` | the text, in a `<pre>` |
267
+ | `text` | any other `text/*`, `application/xml`, `application/yaml`, `application/javascript`…; `.txt` `.log` `.xml` `.yaml` `.yml` `.ini` `.toml` | the text, in a `<pre>` |
268
+ | `other` | anything else, `application/octet-stream` without a known extension | a [`FileCard`](data-display.md#card-presets): icon, name, size and type, and a Download link |
269
+
270
+ A text kind is fetched (a URL) or read (a `Blob`) up to `maxTextBytes` and
271
+ decoded with the `charset` of the response's `Content-Type` (or of the
272
+ `Blob`'s `type`) when the browser's `TextDecoder` supports it, else as UTF-8;
273
+ a JSON or Markdown file cut by the cap is shown as plain text, with "Showing
274
+ the first 256.0 KB of this file." below it. The `<pre>` and the Markdown box
275
+ are regions named "Contents of {name}"; they scroll and take focus, so a
276
+ keyboard can scroll them.
277
+
278
+ The root is a `<figure>` named "Preview of {name}", with `data-kind` and
279
+ `data-state`: `pending` on the server and in the first client render — an
280
+ empty placeholder, with no object URL made and nothing fetched, so it hydrates
281
+ without a mismatch — then `loading` ("Loading preview…", put into a `status` live region that
282
+ stays mounted, so it is announced), `ready`
283
+ or `error` ("This file could not be previewed.", an `alert`, with a Download
284
+ link when there is something to download). The kind comes from
285
+ `fileKind({ type, name, url })`, exported too, from
286
+ [`@nxgt/material-core/file-preview`](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/README.md#a-files-preview-file-preview).
287
+ Every string is translated (`material.file-preview.*`, see the
288
+ [i18n guide](../guides/i18n.md)).
289
+
290
+ **Storybook:** `Media & files/FilePreview` · **Vue:** `FilePreview`
291
+
292
+ ---
293
+
223
294
  ## FilesFormField
224
295
 
225
296
  No preview: it has no story of its own. It is an [`Autocomplete`](selection-and-choice.md)
@@ -256,7 +327,7 @@ function Gallery() {
256
327
  | `freeSolo` | `boolean` | `true` | accept values that are not in `options` |
257
328
 
258
329
  **Form wrapper:** it is one — `control` and `name`, see [forms](../guides/forms.md).
259
- **Storybook:** — · **Vue:** —
330
+ **Storybook:** — · **Vue:** `FilesFormField`
260
331
 
261
332
  ---
262
333
 
@@ -633,6 +704,51 @@ No preview: it is [`StxPlayer`](#stxplayer) under another name
633
704
 
634
705
  ---
635
706
 
707
+ ## Camera
708
+
709
+ ![Camera](../previews/media-files-camera.webp)
710
+
711
+ Webcam preview with a start / stop toggle and a still capture.
712
+
713
+ ```tsx
714
+ import { Camera } from '@nxgt/material/components';
715
+
716
+ export function Webcam() {
717
+ return (
718
+ <div className="w-[640px]">
719
+ <Camera />
720
+ </div>
721
+ );
722
+ }
723
+ ```
724
+
725
+ It asks for camera permission when it mounts, lists the devices, streams on
726
+ the toggle and shows the captured photo beside the preview; the stream is
727
+ stopped on unmount. **It renders in the browser only, after mount**
728
+ (`clientOnly`): on the server, and on the first client render, it renders
729
+ nothing, so it is safe in an SSR page. `Camera` takes the props of a `div`
730
+ (`className`, …) and has no callback: the capture stays inside the
731
+ component. Each `Camera` keeps its own state.
732
+
733
+ `Camera` renders its own parts and takes no children. The helpers it runs
734
+ are exported, and the first three are what a webcam UI of your own needs
735
+ (`<video>` + `getUserMedia`):
736
+
737
+ | Export | What it is |
738
+ | --- | --- |
739
+ | `capturePhotoFromVideo(video, state)` | draws a `<video>` frame on a canvas (`captureSettings`: resolution, quality, format, mirror) and returns `{ capturedImage }` as a data URL, `{ alert }` on failure, `null` when not streaming or on the server |
740
+ | `isCameraSupported()`, `requestCameraPermissions()`, `getCameraDevices()` | the browser checks the component runs |
741
+ | `useCamera()`, `useCameraContext()` | the context `Camera`'s own parts read (`{ videoRef, canvasRef, streamRef, state, actions }`); they throw outside `Camera`, and `Camera` exposes no slot, so they serve its internals and tests, as in `@nxgt/material-vue` |
742
+ | `createCameraStore(preloaded?)` | the Redux store each `Camera` creates for itself, for tests |
743
+
744
+ Types: `CameraProps`, `CameraState`, `CameraActions`, `CameraContextValue`,
745
+ `CameraAlert`, `CameraDeviceInfo`, `CaptureSettings`, `ImageFormat`,
746
+ `CapturePhotoResult`, `CameraStore`.
747
+
748
+ **Storybook:** `Media & files/Camera` · **Vue:** `Camera`, `useCamera`, `capturePhotoFromVideo` (`useCameraStore` for the store)
749
+
750
+ ---
751
+
636
752
  ## Print components
637
753
 
638
754
  No preview: these render PDF with `@react-pdf/renderer`, not DOM, so there is
@@ -892,10 +1008,3 @@ Parts: `Map` (also exported as `MapComponent`), `Marker`, `LocationMarker`,
892
1008
 
893
1009
  **Form wrapper:** `LocationFormField` — see [forms](../guides/forms.md).
894
1010
  **Storybook:** `Media & files/Map` · **Vue:** —
895
-
896
- ---
897
-
898
- ## Not documented here
899
-
900
- `Media & files/Camera` exists in Storybook only: the `Camera` component is not
901
- exported from the package, so it has no entry on this page.
@@ -437,6 +437,11 @@ Parts: `Stepper`, `StepperIndicator` (the progress bar alone: `current`,
437
437
  | `onChange` | `(index: number) => void` | — | makes steps clickable |
438
438
  | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | vertical from `md` only; phones get the compact bar |
439
439
  | `renderStepLabel` | `(step, index) => ReactNode` | — | replaces the "Step n" caption |
440
+ | `name` | `string` | — | the stepper's `data-pw` (`stepper` without it), and the prefix of each step's: `<name>-step-N` instead of `step-N` |
441
+
442
+ Each step is an `<li>` with `data-pw="step-N"` (`<name>-step-N` with `name`),
443
+ N counted from 0, for tests to select; the current step's button carries
444
+ `aria-current="step"`, and a completed or failed step is announced as such.
440
445
 
441
446
  A server renders the desktop layout (every step), as does a hydrating client;
442
447
  a phone switches to the compact bar right after hydrating. A page rendered
@@ -550,7 +555,7 @@ On `DocPage`: `title`, `description`, `badge`, `crumbs`, `editHref`,
550
555
  `updatedAt`, and `copyPage` / `copyMarkdown` with `markdown` for the copy
551
556
  buttons.
552
557
 
553
- **Storybook:** `Navigation & layout/Doc`, `Navigation & layout/Doc/Api`, `Navigation & layout/Doc/Blocks` · **Vue:** `Doc`, `DocPage`, `DocTopbar`, `DocSidebar`, `DocToc`, `DocPager`, `DocSearch`, `DocChildCards`, `DocSeeAlso`, `DocEmpty`, `DocProgress`
558
+ **Storybook:** `Navigation & layout/Doc`, `Navigation & layout/Doc/Api`, `Navigation & layout/Doc/Blocks` · **Vue:** `Doc`, `DocPage`, `DocTopbar`, `DocSidebar`, `DocNav`, `DocArticle`, `DocToc`, `DocPager`, `DocSearch`, `DocChildCards`, `DocSeeAlso`, `DocEmpty`, `DocProgress`, `DocLink`, `DefaultDocLink`, `DocCopyPage` (no `linkComponent`: `@navigate`)
554
559
 
555
560
  ---
556
561