@nxgt/material 2.7.1 → 2.9.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 (223) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +9 -6
  3. package/dist/{_Uint8Array-JRUmWcRr.js → _Uint8Array-azThpjEP.js} +1 -1
  4. package/dist/{_overRest-DvC5rm5s.js → _overRest-DMn4Dt71.js} +1 -1
  5. package/dist/{abnfDiagram-O67JEVCF-Cl4ukf6a.js → abnfDiagram-O67JEVCF-D-rmT4BY.js} +4 -4
  6. package/dist/{architectureDiagram-NJMV4G6O-WmFlBQF5.js → architectureDiagram-NJMV4G6O-DOc_c_T9.js} +7 -7
  7. package/dist/{async-slice-OftEOtMA.js → async-slice-DhZDjEtw.js} +2 -2
  8. package/dist/{blockDiagram-BEXU5L5S-BM-gkWAO.js → blockDiagram-BEXU5L5S-BOsyK4E_.js} +11 -11
  9. package/dist/{c4Diagram-YGBWAQC7-D9xSt2QO.js → c4Diagram-YGBWAQC7-El4SkIzX.js} +8 -8
  10. package/dist/{channel-Dg92Hoym.js → channel-CnLB6790.js} +1 -1
  11. package/dist/{chunk-3FUC2YCW-Cf18L00V.js → chunk-3FUC2YCW-BCsKm3ZT.js} +8 -8
  12. package/dist/{chunk-5DYCD2WN-Cja1INgd.js → chunk-5DYCD2WN-Cd1zazNp.js} +3 -3
  13. package/dist/{chunk-6AEJRKK7-3YWRP4aV.js → chunk-6AEJRKK7-BLBlw8ui.js} +2 -2
  14. package/dist/{chunk-742MDFTN-Cs1qGiTK.js → chunk-742MDFTN-CsUjFyzK.js} +2 -2
  15. package/dist/{chunk-7INBJB4K-BY5n77ka.js → chunk-7INBJB4K-k-I67NSP.js} +6 -6
  16. package/dist/{chunk-7M6MHVWA-C3x6E3yT.js → chunk-7M6MHVWA-C3qpW8zM.js} +9 -9
  17. package/dist/{chunk-7PRAP22T-_Av9vcXz.js → chunk-7PRAP22T-0C6_H8Li.js} +1 -1
  18. package/dist/{chunk-GNY47TPC-D1E6DHDV.js → chunk-GNY47TPC-CzPNLW5k.js} +11 -11
  19. package/dist/{chunk-GTNCS2PH-BZ9WMD1v.js → chunk-GTNCS2PH-AJi9PVT7.js} +3 -3
  20. package/dist/{chunk-GWA4HPMP-Co-A6dtV.js → chunk-GWA4HPMP-B8jXI0Tl.js} +3 -3
  21. package/dist/{chunk-MBY4JIJT-C3RQP_Nk.js → chunk-MBY4JIJT-BFIqOe-8.js} +4 -4
  22. package/dist/{chunk-NETBCI7D-DJUuN4EY.js → chunk-NETBCI7D-tNinefdn.js} +1 -1
  23. package/dist/{chunk-O7XYJQB3-A66QSOPA.js → chunk-O7XYJQB3-BChweQ_V.js} +1 -1
  24. package/dist/{chunk-UA2S7LBM-BNeHeMx9.js → chunk-UA2S7LBM-ChuksCDg.js} +6 -6
  25. package/dist/{chunk-WEXAMYUT-CucJfehv.js → chunk-WEXAMYUT-CbFOhEve.js} +2 -2
  26. package/dist/{chunk-XXDRQBXY-FgXU14Bh.js → chunk-XXDRQBXY-dKXxu_aj.js} +1 -1
  27. package/dist/{chunk-Z7XXMR3K-DMTBxZxM.js → chunk-Z7XXMR3K-C9P-XjM2.js} +8 -8
  28. package/dist/{chunk-ZIGJFQKS-C3xUAXJq.js → chunk-ZIGJFQKS-9VXArDKF.js} +3 -3
  29. package/dist/{classDiagram-v2-NBCMYWYE-BDaxYvDy.js → classDiagram-v2-NBCMYWYE-D00ZobEC.js} +14 -14
  30. package/dist/clsx-CbxlCXZ3.js +16 -0
  31. package/dist/components/base-content.css +139 -0
  32. package/dist/components/base.css +150 -0
  33. package/dist/components/components.css +2 -0
  34. package/dist/components/reduced-motion.css +56 -0
  35. package/dist/components/theme-tokens.css +116 -0
  36. package/dist/components/ui/actions/button-group/button-group.css +1 -1
  37. package/dist/components/ui/feedback/toast/toast.css +21 -0
  38. package/dist/components/ui/primitives/label/label.css +33 -0
  39. package/dist/{components-DrStgM4J.js → components-km60bAdr.js} +34658 -34218
  40. package/dist/components.js +6 -6
  41. package/dist/{cose-bilkent-JH36ORCC-DRUT3qBb.js → cose-bilkent-JH36ORCC-P94vkHv_.js} +2 -2
  42. package/dist/{cynefinDiagram-VND7K2PF-B9HrdQW8.js → cynefinDiagram-VND7K2PF-EIpXbLXm.js} +4 -4
  43. package/dist/{dagre-6A5THRUB-DZF0nICj.js → dagre-6A5THRUB-D_F3_LlW.js} +20 -20
  44. package/dist/{diagram-22UHCM2B-CVZYpeev.js → diagram-22UHCM2B-CsxOExpW.js} +15 -15
  45. package/dist/{diagram-3UASUU5V-D7MmBJbM.js → diagram-3UASUU5V-Dl7AETV1.js} +7 -7
  46. package/dist/{diagram-ATOU4E4O-CNY-8kl0.js → diagram-ATOU4E4O-isL7nwg2.js} +3 -3
  47. package/dist/{diagram-CDSNMT55-C8Mk1smi.js → diagram-CDSNMT55-Bnhlo5at.js} +5 -5
  48. package/dist/{diagram-MLGK6HIB-CpfeILwW.js → diagram-MLGK6HIB-DvBcyz8v.js} +4 -4
  49. package/dist/{diagram-MPIPVDR6-Do7bUnTd.js → diagram-MPIPVDR6-iTevwmNE.js} +4 -4
  50. package/dist/{dist-C3YK_ean.js → dist-B6_-RPZ3.js} +1 -1
  51. package/dist/{dist-CRbPtqwD.js → dist-BvUQHUs_.js} +4 -4
  52. package/dist/dnd.js +1 -1
  53. package/dist/{ebnfDiagram-ZINNZB2B-1GewX9Zq.js → ebnfDiagram-ZINNZB2B-RVuQIpm0.js} +4 -4
  54. package/dist/{elk-276RUBZZ-DI4eicFj.js → elk-276RUBZZ-BK-NMLiI.js} +26 -26
  55. package/dist/{erDiagram-OPXOYQCR-CUbpVns3.js → erDiagram-OPXOYQCR-DC1GgS4l.js} +14 -14
  56. package/dist/field-blur--x_n3oMy.js +3637 -0
  57. package/dist/flowDiagram-KWPJA3E3-BUSlDNQP.js +17 -0
  58. package/dist/{ganttDiagram-FUAMR5RP-D3vmspam.js → ganttDiagram-FUAMR5RP-DOp1p_x0.js} +6 -6
  59. package/dist/{gitGraphDiagram-X574FWY7-80yOq8dE.js → gitGraphDiagram-X574FWY7-DerJL1Q-.js} +3 -3
  60. package/dist/{graphlib-DLJn7qS3.js → graphlib-DLjE3QPd.js} +6 -6
  61. package/dist/hooks.js +3 -3
  62. package/dist/{i18n-provider-DBFRz5uH.js → i18n-provider-CQL7fqLy.js} +1 -1
  63. package/dist/i18n.js +6 -6
  64. package/dist/icon-button-G9zdQ3Ix.js +2753 -0
  65. package/dist/{infoDiagram-VRGFBTTK-Cboh-B9n.js → infoDiagram-VRGFBTTK-CtVsLyCQ.js} +3 -3
  66. package/dist/{isArrayLikeObject-CO5xeuaJ.js → isArrayLikeObject-CO-5KAzG.js} +3 -3
  67. package/dist/{isEmpty-DOC38Ces.js → isEmpty-Bl1drJE7.js} +3 -3
  68. package/dist/{ishikawaDiagram-OU5B5YK6-DwTd1YxL.js → ishikawaDiagram-OU5B5YK6-D1n8vp8C.js} +4 -4
  69. package/dist/{journeyDiagram-ZHPQQLJL-Dxvl3oeN.js → journeyDiagram-ZHPQQLJL-DuQ9Th0y.js} +4 -4
  70. package/dist/{kanban-definition-PNTS6WVX-S8plI4wn.js → kanban-definition-PNTS6WVX-D1-Owjil.js} +10 -10
  71. package/dist/lib/components/ui/actions/button/busy-click-capture.d.ts +11 -0
  72. package/dist/lib/components/ui/actions/button/button-tones.d.ts +36 -0
  73. package/dist/lib/components/ui/actions/button/button.d.ts +19 -0
  74. package/dist/lib/components/ui/actions/button/button.wrap.d.ts +8 -0
  75. package/dist/lib/components/ui/actions/button/label-text.d.ts +10 -0
  76. package/dist/lib/components/ui/actions/button/use-click-lock.d.ts +18 -0
  77. package/dist/lib/components/ui/actions/copy-button/copy-button.d.ts +5 -1
  78. package/dist/lib/components/ui/actions/icon-button/icon-button.compound-variants.d.ts +114 -0
  79. package/dist/lib/components/ui/collections/data-table/hooks.d.ts +13 -13
  80. package/dist/lib/components/ui/collections/data-table/partials/data-table-empty-row.d.ts +5 -1
  81. package/dist/lib/components/ui/collections/data-table/partials/data-table-scroll-region.d.ts +16 -0
  82. package/dist/lib/components/ui/collections/data-table/partials/data-table-toolbar.d.ts +24 -0
  83. package/dist/lib/components/ui/collections/data-table/select-checkbox.d.ts +10 -0
  84. package/dist/lib/components/ui/collections/data-table/types.d.ts +9 -0
  85. package/dist/lib/components/ui/collections/data-table/use-data-table.d.ts +12 -1
  86. package/dist/lib/components/ui/collections/data-table/use-scroll-edges.d.ts +22 -0
  87. package/dist/lib/components/ui/dates/calendar/calendar-day.d.ts +8 -0
  88. package/dist/lib/components/ui/dates/calendar/calendar.d.ts +3 -3
  89. package/dist/lib/components/ui/dates/calendar/calendar.utils.d.ts +8 -0
  90. package/dist/lib/components/ui/dates/calendar/use-calendar-labels.d.ts +22 -0
  91. package/dist/lib/components/ui/display/description/description.d.ts +2 -1
  92. package/dist/lib/components/ui/display/list-tile/list-tile.d.ts +9 -1
  93. package/dist/lib/components/ui/display/list-tile/partials/list-tile-body.d.ts +11 -0
  94. package/dist/lib/components/ui/display/list-tile/partials/list-tile-classes.d.ts +10 -0
  95. package/dist/lib/components/ui/feedback/alert-dialog/alert-dialog.d.ts +1 -1
  96. package/dist/lib/components/ui/feedback/toast/partials/toast-actions.d.ts +3 -0
  97. package/dist/lib/components/ui/feedback/toast/toast.d.ts +27 -3
  98. package/dist/lib/components/ui/feedback/toast/toaster.d.ts +19 -0
  99. package/dist/lib/components/ui/feedback/toast/toasts.d.ts +3 -0
  100. package/dist/lib/components/ui/forms/field-blur.d.ts +7 -2
  101. package/dist/lib/components/ui/forms/use-field-blur.d.ts +8 -1
  102. package/dist/lib/components/ui/layout/app-shell/activity-content.d.ts +10 -1
  103. package/dist/lib/components/ui/layout/app-shell/activity-layout.d.ts +26 -2
  104. package/dist/lib/components/ui/layout/app-shell/app-header.d.ts +8 -2
  105. package/dist/lib/components/ui/layout/app-shell/app-sidebar.d.ts +8 -2
  106. package/dist/lib/components/ui/layout/app-shell/types.d.ts +3 -0
  107. package/dist/lib/components/ui/layout/app-shell/use-content-scroll.d.ts +10 -0
  108. package/dist/lib/components/ui/layout/app-shell/use-navigation-sheet.d.ts +19 -0
  109. package/dist/lib/components/ui/layout/sidebar/index.d.ts +5 -0
  110. package/dist/lib/components/ui/layout/sidebar/sidebar-content.d.ts +2 -0
  111. package/dist/lib/components/ui/layout/sidebar/sidebar-context.d.ts +7 -0
  112. package/dist/lib/components/ui/layout/sidebar/sidebar-footer.d.ts +2 -0
  113. package/dist/lib/components/ui/layout/sidebar/sidebar-header.d.ts +2 -0
  114. package/dist/lib/components/ui/layout/sidebar/sidebar-menu-item-tooltip.d.ts +7 -0
  115. package/dist/lib/components/ui/layout/sidebar/sidebar-menu-item.d.ts +31 -0
  116. package/dist/lib/components/ui/layout/sidebar/sidebar-menu-label.d.ts +4 -0
  117. package/dist/lib/components/ui/layout/sidebar/sidebar.d.ts +1 -12
  118. package/dist/lib/components/ui/overlays/context-menu/context-menu-items.d.ts +10 -0
  119. package/dist/lib/components/ui/overlays/context-menu/context-menu-sub.d.ts +8 -0
  120. package/dist/lib/components/ui/overlays/context-menu/context-menu.d.ts +4 -14
  121. package/dist/lib/components/ui/overlays/dialog/dialog-outside.d.ts +6 -0
  122. package/dist/lib/components/ui/overlays/dialog/dialog-scroll-content.d.ts +1 -1
  123. package/dist/lib/components/ui/overlays/dialog/dialog.d.ts +1 -1
  124. package/dist/lib/components/ui/overlays/dialog/use-return-focus.d.ts +3 -2
  125. package/dist/lib/components/ui/overlays/drawer/drawer.d.ts +1 -1
  126. package/dist/lib/components/ui/overlays/dropdown-menu/dropdown-menu-items.d.ts +11 -0
  127. package/dist/lib/components/ui/overlays/dropdown-menu/dropdown-menu-sub.d.ts +8 -0
  128. package/dist/lib/components/ui/overlays/dropdown-menu/dropdown-menu.d.ts +6 -21
  129. package/dist/lib/components/ui/overlays/popover/popover.d.ts +1 -1
  130. package/dist/lib/components/ui/overlays/sheet/sheet.d.ts +1 -1
  131. package/dist/lib/components/ui/primitives/card/card.d.ts +10 -1
  132. package/dist/lib/components/ui/primitives/link/link-component.d.ts +14 -0
  133. package/dist/lib/components/ui/primitives/tabs/partials/tabs-content.d.ts +3 -0
  134. package/dist/lib/components/ui/primitives/tabs/partials/tabs-indicator.d.ts +13 -0
  135. package/dist/lib/components/ui/primitives/tabs/partials/tabs-list.d.ts +3 -0
  136. package/dist/lib/components/ui/primitives/tabs/partials/tabs-trigger.d.ts +6 -0
  137. package/dist/lib/components/ui/primitives/tabs/tabs-classes.d.ts +27 -0
  138. package/dist/lib/components/ui/primitives/tabs/tabs-variant.d.ts +5 -0
  139. package/dist/lib/components/ui/primitives/tabs/tabs.d.ts +11 -7
  140. package/dist/lib/components/ui/selection/select/select.d.ts +1 -1
  141. package/dist/lib/components/ui/selection/select-chip-field/select-chip-field.d.ts +1 -1
  142. package/dist/lib/components/ui/widgets/emoji/types.d.ts +1 -6
  143. package/dist/lib/i18n/resources/en/en.d.ts +23 -0
  144. package/dist/lib/i18n/resources/en/material.json.d.ts +28 -5
  145. package/dist/lib/i18n/resources/fr/fr.d.ts +23 -0
  146. package/dist/lib/i18n/resources/fr/material.json.d.ts +28 -5
  147. package/dist/lib/i18n/resources/resources.d.ts +46 -0
  148. package/dist/lib/i18n/translator.d.ts +1 -1
  149. package/dist/lib/lib/redux/status-slice.d.ts +1 -1
  150. package/dist/lib.js +39 -55
  151. package/dist/main.js +13 -13
  152. package/dist/map.js +35 -35
  153. package/dist/material.css +1 -1
  154. package/dist/{merge-CuDTNgpm.js → merge-CwwtONfp.js} +4 -4
  155. package/dist/{mermaid.core-O7mtb1-p.js → mermaid.core-Dfr4zfuP.js} +49 -49
  156. package/dist/{mindmap-definition-NLK3R4M7-CO0E97EZ.js → mindmap-definition-NLK3R4M7-_qKWGSrk.js} +13 -13
  157. package/dist/{pegDiagram-GJSIUBJH-CAO6aYvj.js → pegDiagram-GJSIUBJH-CWKobqLl.js} +4 -4
  158. package/dist/{pieDiagram-5QR66LMP-BXxVfv5K.js → pieDiagram-5QR66LMP-B6at48Bd.js} +5 -5
  159. package/dist/print.js +2 -2
  160. package/dist/{quadrantDiagram-O4NWA36T-BbidZDM8.js → quadrantDiagram-O4NWA36T-Cr9bA0xJ.js} +3 -3
  161. package/dist/{railroadDiagram-XR7U4H2S-DmKDXg8V.js → railroadDiagram-XR7U4H2S-fsZ7oqPG.js} +4 -4
  162. package/dist/{requirementDiagram-PLB6GJNP-wFvgSWJP.js → requirementDiagram-PLB6GJNP-BBs-lByV.js} +13 -13
  163. package/dist/rolldown-runtime-BFFgcFxR.js +30 -0
  164. package/dist/{sankeyDiagram-IPEJSGJF-Cka5t09q.js → sankeyDiagram-IPEJSGJF-BzX37jGY.js} +3 -3
  165. package/dist/{sequenceDiagram-PO4LG4MO-B2qb55A5.js → sequenceDiagram-PO4LG4MO-Bgx1kuGA.js} +5 -5
  166. package/dist/{sortable-provider-C5VYhY8u.js → sortable-provider-WZT5I34N.js} +24 -24
  167. package/dist/{src-C9kQSo6K.js → src-CsLQInsN.js} +1 -1
  168. package/dist/{stateDiagram-v2-GCMORJYK-EMa7q3ju.js → stateDiagram-v2-GCMORJYK-Chc9Z6Mr.js} +14 -14
  169. package/dist/{stored-language-B0o4cBh8.js → stored-language-BW32owje.js} +56 -10
  170. package/dist/styles.css +10 -368
  171. package/dist/{swimlanes-2SLR337P-BtZ7Di5u.js → swimlanes-2SLR337P-D0yLvfot.js} +12 -12
  172. package/dist/swimlanesDiagram-TC7HE7FX-BOd0Czfs.js +28 -0
  173. package/dist/{timeline-definition-EJHVYXUP-2Z6UABw8.js → timeline-definition-EJHVYXUP-nDNR-Dt-.js} +5 -5
  174. package/dist/{translate-material-BCwQrt2G.js → translate-material-ZEzF-LKH.js} +2 -2
  175. package/dist/{translator-CwpmpI24.js → translator-BfqVlNGW.js} +1 -1
  176. package/dist/{use-heading-spy-BtBJiU63.js → use-heading-spy-DvZVu15k.js} +37 -37
  177. package/dist/{use-material-translation-DG_7z3Q7.js → use-material-translation-B2GUtxUY.js} +2 -2
  178. package/dist/{usecaseDiagram-POWQR4AR-DT56cp_B.js → usecaseDiagram-POWQR4AR-Bwen99ay.js} +13 -13
  179. package/dist/{utils-UBn-46YY.js → utils-DC8bw9A_.js} +346 -360
  180. package/dist/{vennDiagram-UO4OBE2U-CO0yFCI8.js → vennDiagram-UO4OBE2U-BpgP__1T.js} +4 -4
  181. package/dist/{wardleyDiagram-VNRHLVJA-QkwghAYo.js → wardleyDiagram-VNRHLVJA-CYqKMb2f.js} +4 -4
  182. package/dist/{xychartDiagram-PMCCYNJV-B2aZlrno.js → xychartDiagram-PMCCYNJV-b-Quuxoi.js} +7 -7
  183. package/docs/components/buttons-and-actions.md +60 -10
  184. package/docs/components/data-display.md +24 -2
  185. package/docs/components/data-table-filter-and-lists.md +88 -10
  186. package/docs/components/date-and-time.md +24 -0
  187. package/docs/components/feedback-and-status.md +85 -8
  188. package/docs/components/form-fields.md +14 -5
  189. package/docs/components/media-and-files.md +7 -0
  190. package/docs/components/navigation-and-layout.md +21 -3
  191. package/docs/components/overlays-and-menus.md +21 -6
  192. package/docs/components/primitives.md +96 -7
  193. package/docs/components/rich-widgets.md +10 -0
  194. package/docs/components/selection-and-choice.md +13 -2
  195. package/docs/getting-started.md +4 -2
  196. package/docs/guides/app-shell.md +97 -4
  197. package/docs/guides/forms.md +54 -5
  198. package/docs/guides/helpers.md +2 -2
  199. package/docs/guides/i18n.md +24 -4
  200. package/docs/guides/ssr.md +1 -1
  201. package/docs/guides/theming.md +147 -5
  202. package/docs/previews/data-display-codegroup.webp +0 -0
  203. package/docs/previews/data-display-description.webp +0 -0
  204. package/docs/previews/feedback-status-toast.webp +0 -0
  205. package/docs/previews/primitives-tabs.webp +0 -0
  206. package/docs/previews/rich-widgets-emojipicker.webp +0 -0
  207. package/docs/roadmap.md +29 -1
  208. package/docs/troubleshooting.md +937 -11
  209. package/lib/components/base-content.css +139 -0
  210. package/lib/components/base.css +150 -0
  211. package/lib/components/components.css +2 -0
  212. package/lib/components/reduced-motion.css +56 -0
  213. package/lib/components/theme-tokens.css +116 -0
  214. package/lib/components/ui/actions/button-group/button-group.css +1 -1
  215. package/lib/components/ui/feedback/toast/toast.css +21 -0
  216. package/lib/components/ui/primitives/label/label.css +33 -0
  217. package/lib/styles.css +10 -368
  218. package/package.json +2 -2
  219. package/dist/field-blur-DWQtxiAe.js +0 -3483
  220. package/dist/flowDiagram-KWPJA3E3-DZxumFfu.js +0 -17
  221. package/dist/icon-button-BxySFfrr.js +0 -2718
  222. package/dist/lib/components/ui/widgets/emoji/emoji.groups.d.ts +0 -6
  223. package/dist/swimlanesDiagram-TC7HE7FX-CROh8GTF.js +0 -28
@@ -59,11 +59,14 @@ which work alone.
59
59
  | Prop | Type | Default | Effect |
60
60
  | --- | --- | --- | --- |
61
61
  | `items` | `MenuItem[]` | — | `label` (already translated), `url`, `icon`, `footer` |
62
+ | `linkComponent` | `AppShellLinkComponent` | React Router `Link` | The link each menu entry renders, given `href`; see the [app shell guide](../guides/app-shell.md) |
62
63
  | `brand` | `ReactNode` | — | sidebar header |
63
64
  | `avatar` | `ReactNode` | — | an image URL or a node, in the header |
64
65
  | `headerActions` | `ReactNode` | — | header, before the avatar |
65
66
  | `title`, `subtitle`, `actions` | `ReactNode` | — | `ActivityContent`'s header card |
66
67
  | `header` / `hideHeader` | `ReactNode` / `boolean` | — | `ActivityContent`: replace or drop the header card |
68
+ | `contentAs` / `scroll` | `'main' \| 'div'` / `'document' \| 'container'` | `'main'` / `'document'` | the content region is the page's `<main>`; the document scrolls (see the [app shell guide](../guides/app-shell.md#landmarks-headings-and-scrolling)) |
69
+ | `titleAs` | `'h1'` … `'h6'` \| `'div'` | `'h1'` | `ActivityContent`: the title's heading level |
67
70
 
68
71
  The active entry is the longest match across the whole menu, segment-aware.
69
72
  `AppBreadcrumb` reads `handle.breadcrumb` from `useMatches()`, so the shell
@@ -110,12 +113,27 @@ import {
110
113
  ```
111
114
 
112
115
  Parts: `Sidebar`, `SidebarHeader`, `SidebarContent`, `SidebarFooter`,
113
- `SidebarMenuLabel` (`actions` slot), `SidebarMenuItem` (a button: `icon`,
116
+ `SidebarMenuLabel` (`actions` slot), `SidebarMenuItem` (a button, or a link with `href` and `linkComponent`: `icon`,
114
117
  `active`, `tooltip`).
115
118
 
119
+ A `SidebarMenuItem` stays a `<button>` without `href`. With `href` it renders a
120
+ real link, a plain `<a>` unless you pass `linkComponent` (given the `href`):
121
+
122
+ ```tsx
123
+ <SidebarMenuItem href="/inbox" linkComponent={MyLink} icon={<Inbox />}>Inbox</SidebarMenuItem>
124
+ ```
125
+
126
+ An entry's tooltip is its label, shown by default **only while the
127
+ `Sidebar` is collapsed**: expanded, the label is on screen and no tooltip
128
+ opens. A `tooltip` you pass is always shown, collapsed or expanded:
129
+
130
+ ```tsx
131
+ <SidebarMenuItem icon={<Inbox />} tooltip="3 unread">Inbox</SidebarMenuItem>
132
+ ```
133
+
116
134
  | Prop | Type | Default | Effect |
117
135
  | --- | --- | --- | --- |
118
- | `expanded` | `boolean` | `false` | labels beside the icons; collapsed shows icons with tooltips |
136
+ | `expanded` | `boolean` | `false` | labels beside the icons; collapsed shows icons with their labels as tooltips |
119
137
  | `onExpandedChange` | `(expanded: boolean) => void` | — | called by the toggle |
120
138
  | `withToggleIcon` | `boolean` | `false` | built-in expand / collapse button, shown on hover or while focus is inside the sidebar, named "Toggle sidebar" (`material.sidebar.toggle`) with `aria-expanded` |
121
139
 
@@ -573,7 +591,7 @@ import { Background, Card } from '@nxgt/material/components';
573
591
  </Background>
574
592
  ```
575
593
 
576
- No props of its own. It fills the viewport (`size-lv`); pass a `className`
594
+ No props of its own. It fills the viewport (`w-lvw h-lvh`); pass a `className`
577
595
  such as `h-96 w-full` to use it as a band. `ActivityLayout` and
578
596
  `ThreadLayout` sit on it.
579
597
 
@@ -8,6 +8,19 @@ command palette) with this library's styling, and render into a portal on
8
8
  [Feedback & status](feedback-and-status.md). Everything is imported from
9
9
  `@nxgt/material/components`.
10
10
 
11
+ **The page behind a modal layer is inert.** While a `Dialog`, `Sheet`,
12
+ `Drawer`, `AlertDialog` or `CommandDialog` is open — and a `Select` list, a
13
+ `ContextMenu`, or a `DropdownMenu` or `Popover` given `modal` — everything else
14
+ on the page is `inert` as well as `aria-hidden`: its controls leave the tab
15
+ order and the accessibility tree, so a keyboard or screen-reader user stays in
16
+ the layer and axe's `aria-hidden-focus` passes. Live regions stay reachable:
17
+ a toast shown over a dialog is announced, and its action can be used. Layers
18
+ stack: a `Select` open in a `Dialog` makes the dialog inert too, until it
19
+ closes. Focus left on the page when the layer opens (an `onOpenAutoFocus`
20
+ that prevents the default and focuses nothing) goes to the layer's content.
21
+ Non-modal layers (`modal={false}`, and `DropdownMenu` and `Popover` by
22
+ default) leave the page as it is.
23
+
11
24
  | | Component | What it does |
12
25
  | --- | --- | --- |
13
26
  | <img src="../previews/overlays-menus-dialog.webp" width="96" alt=""> | [Dialog](#dialog) | Modal dialog with an icon close button |
@@ -92,7 +105,7 @@ export function Terms() {
92
105
  | --- | --- | --- | --- |
93
106
  | `defaultOpen` / `open` (`Dialog`) | `boolean` | `false` | open state, uncontrolled / controlled |
94
107
  | `onOpenChange` (`Dialog`) | `(open: boolean) => void` | — | called on trigger, Escape, outside click and close |
95
- | `modal` (`Dialog`) | `boolean` | `true` | `false` drops the backdrop and lets the page stay interactive |
108
+ | `modal` (`Dialog`) | `boolean` | `true` | `false` drops the backdrop and lets the page stay interactive; `true` makes the page behind `inert` while it is open |
96
109
  | `asChild` (`DialogTrigger`) | `boolean` | **`true`** | the trigger is its child — pass one element, usually a `Button` |
97
110
  | `variant`, `color` (`DialogClose`) | as `IconButton` | `'ghost'` | a top-right X button, named "Close" (`material.buttons.close`, translated) while it shows only the icon; text children or an `aria-label` name it instead, and icon-only children leave it unnamed; children replace the X |
98
111
 
@@ -100,8 +113,9 @@ export function Terms() {
100
113
  It ignores outside clicks that land on an open `Combobox` list, so a combobox
101
114
  inside a dialog works. On open it moves focus into the dialog: to its first
102
115
  control, or to the content itself when it has none. An `onOpenAutoFocus` of
103
- yours that calls `event.preventDefault()` keeps focus where it was (then focus
104
- what you want yourself).
116
+ yours that calls `event.preventDefault()` keeps focus off that control (then
117
+ focus what you want yourself): a modal dialog then focuses its content, as the
118
+ page behind it is inert, and a non-modal one leaves focus where it was.
105
119
  On close it hands focus back to the element that had it when the dialog
106
120
  opened — its `DialogTrigger`, or the button of yours that set `open` on a
107
121
  controlled `Dialog` — if that element is still on the page and focus has not
@@ -368,7 +382,7 @@ the fullscreen element shows.
368
382
 
369
383
  ![DropdownMenu](../previews/overlays-menus-dropdownmenu.webp)
370
384
 
371
- Menu with checkbox and radio items, submenus, shortcuts and an error item.
385
+ Menu with checkbox and radio items, submenus, shortcuts and an error item. It is non-modal by default: Radix's modal menu marks the rest of the page `aria-hidden` while its buttons and links stay focusable, which accessibility audits (axe `aria-hidden-focus`) report; a non-modal menu hides nothing and still closes on Escape or an outside click (Tab stays in the menu). Pass `modal` for a modal one: the page behind it is then `inert` too, so its controls leave the tab order and axe passes.
372
386
 
373
387
  ```tsx
374
388
  import {
@@ -408,11 +422,12 @@ Parts: `DropdownMenu`, `DropdownMenuTrigger`, `DropdownMenuContent`,
408
422
 
409
423
  | Prop | Type | Default | Effect |
410
424
  | --- | --- | --- | --- |
425
+ | `modal` (`DropdownMenu`) | `boolean` | `false` | `true` brings back Radix's modal menu: scroll lock, outside clicks swallowed, the rest of the page `aria-hidden` and `inert` |
411
426
  | `variant` (`DropdownMenuItem`) | `'default' \| 'error'` | `'default'` | error colour for a destructive item |
412
- | `active` (`DropdownMenuItem`) | `boolean` | — | highlights the current item (filled primary) |
427
+ | `active` (`DropdownMenuItem`) | `boolean` | — | highlights the current item (filled primary) and sets `data-active` on it; style or select it with `data-[active]:` or `[data-active]` |
413
428
  | `inset` (item, label, sub-trigger) | `boolean` | — | indents to line up with checkbox and radio items |
414
429
  | `onSelect` (`DropdownMenuItem`) | `(event: Event) => void` | — | the item's action; `event.preventDefault()` keeps the menu open |
415
- | `defaultOpen` / `open`, `modal` (`DropdownMenu`) | `boolean` | —, `true` | open state; a modal menu blocks the page behind it |
430
+ | `defaultOpen` / `open` (`DropdownMenu`) | `boolean` | — | open state; see `modal` above for the page behind the menu |
416
431
  | `container` (`DropdownMenuContent`) | `Element \| DocumentFragment \| null` | `document.body` | where the menu is portalled; for a menu opened in fullscreen, pass `document.fullscreenElement`, read when the menu opens (see [troubleshooting](../troubleshooting.md#a-dropdownmenu-opened-in-fullscreen-does-not-show)) |
417
432
 
418
433
  **Storybook:** `Overlays & menus/DropdownMenu` · **Vue:** `DropdownMenu` (same parts)
@@ -147,7 +147,9 @@ import {
147
147
  ```
148
148
 
149
149
  Parts: `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`,
150
- `CardContent`, `CardFooter` — each a styled `<div>` taking its element's props.
150
+ `CardContent`, `CardFooter` — each a styled `<div>` taking its element's props,
151
+ except `CardTitle`, a heading: an `h3` by default, another level or a `div`
152
+ with `as` (`<CardTitle as="h2">`), so `getByRole('heading', { name })` finds it.
151
153
  `CardAction` sits top-right of the header.
152
154
 
153
155
  **Storybook:** `Primitives/Card` · **Vue:** `Card` (same parts)
@@ -224,12 +226,24 @@ import { Input, Label } from '@nxgt/material/components';
224
226
 
225
227
  | Prop | Type | Default | Effect |
226
228
  | --- | --- | --- | --- |
227
- | `withAsterisk` | `boolean` | — | appends a red `*`, hidden from screen readers: mark the field itself `required` (or `aria-required`), which every field of this library does for you |
229
+ | `withAsterisk` | `boolean` | — | draws a red `*` with CSS after the label, so it is in neither the label's text nor its accessible name (`getByLabel('Email', { exact: true })` and `getByLabelText` match "Email"): mark the field itself `required` (or `aria-required`), which every field of this library does for you |
228
230
  | `htmlFor` | `string` | — | the control it labels; clicking the label focuses it |
229
231
 
230
232
  Fields on the [Form fields](form-fields.md) page render it for you through
231
233
  their `label` prop.
232
234
 
235
+ To restyle the mark, target `[data-slot="label-text"][data-required]::after`:
236
+ `background-color` sets its colour (default `var(--color-error)`), `width` and
237
+ `height` its size. It stays visible in forced-colors mode and in print.
238
+
239
+ ```css
240
+ [data-slot="label-text"][data-required]::after {
241
+ background-color: #b45309;
242
+ width: 0.75em;
243
+ height: 0.75em;
244
+ }
245
+ ```
246
+
233
247
  **Storybook:** `Primitives/Label` · **Vue:** `Label`
234
248
 
235
249
  ---
@@ -559,7 +573,7 @@ For sorting, paging and selection use `DataTable` on the
559
573
 
560
574
  ![Tabs](../previews/primitives-tabs.webp)
561
575
 
562
- Tabs whose triggers can take an icon. `variant="line"` is the quieter pill style.
576
+ Tabs whose triggers can take an icon, in two looks: `underline` (the default) and the `segmented` pill.
563
577
 
564
578
  ```tsx
565
579
  import {
@@ -584,16 +598,91 @@ import {
584
598
  </Tabs>;
585
599
  ```
586
600
 
587
- Parts: `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`.
601
+ Parts: `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`. `TabsList` and
602
+ `TabsTrigger` read the look from `Tabs`.
603
+
604
+ **`underline`** (the default) lays the tabs over a border; the active one
605
+ takes the primary colour, and a bar under it slides to the tab clicked or
606
+ reached with the arrow keys. The list scrolls sideways when the tabs do not
607
+ fit (its scrollbar hidden), which suits a phone or a narrow popover.
608
+
609
+ **`segmented`** is a neutral pill track: the active tab sits on a raised
610
+ surface that slides between tabs, and its label turns to the foreground
611
+ colour and semibold — the state never rests on the surface alone. It suits a
612
+ few short choices, such as a view switch.
613
+
614
+ ```tsx
615
+ <Tabs defaultValue="week" variant="segmented">
616
+ <TabsList>
617
+ <TabsTrigger value="day">Day</TabsTrigger>
618
+ <TabsTrigger value="week">Week</TabsTrigger>
619
+ <TabsTrigger value="month">Month</TabsTrigger>
620
+ </TabsList>
621
+ <TabsContent value="day">Today.</TabsContent>
622
+ <TabsContent value="week">This week.</TabsContent>
623
+ <TabsContent value="month">This month.</TabsContent>
624
+ </Tabs>
625
+ ```
626
+
627
+ `orientation="vertical"` lays the tabs in a column, the indicator on their
628
+ left side; the arrow keys become Up and Down.
629
+
630
+ ```tsx
631
+ <Tabs defaultValue="profile" orientation="vertical">
632
+ <TabsList>
633
+ <TabsTrigger value="profile">Profile</TabsTrigger>
634
+ <TabsTrigger value="security">Security</TabsTrigger>
635
+ </TabsList>
636
+ <TabsContent value="profile">Your profile.</TabsContent>
637
+ <TabsContent value="security">Your sessions.</TabsContent>
638
+ </Tabs>
639
+ ```
640
+
641
+ The sliding indicator is measured once the page runs: server HTML, and a
642
+ page without JavaScript, show a static indicator under (or behind) the
643
+ active tab instead, and hydrating swaps one for the other without changing
644
+ the markup. The list then carries `data-indicator="ready"`. The focus ring is
645
+ an outline in the primary colour inside the tab, so a scrolling list never
646
+ clips it.
647
+
648
+ With `TabsList asChild` the list is your child element and the sliding
649
+ indicator is placed inside it, measured against it, so the child must render
650
+ its children directly: a component child that wraps them in an inner element
651
+ hides the indicator.
652
+
653
+ `dir="rtl"` on `Tabs` (or Radix's `DirectionProvider`) runs the tabs from the
654
+ right: the indicator follows them, and the Left arrow key moves to the next
655
+ tab. Under `prefers-reduced-motion: reduce` the indicator jumps to the active
656
+ tab instead of sliding.
657
+
658
+ **A tab with only an icon needs a name.** A screen reader announces a
659
+ `TabsTrigger` by its text; with an icon and no label it is only "tab". Give it
660
+ `aria-label`, and `title` for sighted users who hover it:
661
+
662
+ ```tsx
663
+ <TabsTrigger value="recent" icon={<Clock />} aria-label="Recent" title="Recent" />
664
+ ```
665
+
666
+ The emoji picker's group tabs are named this way, from
667
+ `material.emoji.groups.*`.
668
+
669
+ **The default look changed.** A `Tabs` with no `variant` used to render the
670
+ boxed `bar` look (a primary ring around the active tab, a smaller label under
671
+ its icon on the phone); it now renders `underline`. `variant="bar"` and
672
+ `variant="line"` are deprecated aliases: `bar` renders as `underline`, `line`
673
+ as `segmented`. Both still type-check and render; write `underline` or
674
+ `segmented` instead.
588
675
 
589
676
  | Prop | Type | Default | Effect |
590
677
  | --- | --- | --- | --- |
591
- | `variant` (`Tabs`) | `'bar' \| 'line'` | `'bar'` | full-width bar of equal triggers, or the quieter line style |
678
+ | `variant` (`Tabs`) | `'underline' \| 'segmented' \| 'bar' \| 'line'` | `'underline'` | the look; `bar` and `line` are deprecated aliases of `underline` and `segmented` |
592
679
  | `defaultValue` / `value` (`Tabs`) | `string` | — | the selected tab, uncontrolled / controlled |
593
680
  | `onValueChange` (`Tabs`) | `(value: string) => void` | — | called when a trigger is selected |
594
- | `icon` (`TabsTrigger`) | `ReactNode` | — | shown before the label (above it on the phone in `bar`) |
681
+ | `orientation` (`Tabs`) | `'horizontal' \| 'vertical'` | `'horizontal'` | a row or a column, and the arrow keys' direction |
682
+ | `dir` (`Tabs`) | `'ltr' \| 'rtl'` | `'ltr'` | the reading direction: which way the tabs run and the arrow keys move |
683
+ | `icon` (`TabsTrigger`) | `ReactNode` | — | shown before the label |
595
684
 
596
- **Storybook:** `Primitives/Tabs` · **Vue:** `Tabs` (same parts, `variant="line"`)
685
+ **Storybook:** `Primitives/Tabs` (stories `Underline`, `Segmented`, `WithIcons`, `Scrolling`, `RightToLeft`, `Vertical`, `DeprecatedAliases`, `Hydrated`) · **Vue:** `Tabs` (same parts, the same variants, `icon` prop or `#icon` slot)
597
686
 
598
687
  ---
599
688
 
@@ -244,6 +244,16 @@ const EMOJIS: EmojiItem[] = [
244
244
  Parts: `EmojiPicker` (a button opening a popover), `EmojiList` (the searchable
245
245
  grid on its own, same `emojis` / `onSelect`), `expandEmojiShortcodes`.
246
246
 
247
+ The category tabs are icon-only. Each is named from
248
+ `material.emoji.groups.<group>` (for example `people_body`, `flags`): that name
249
+ is its `aria-label` and `title`, in the provider's language (see
250
+ [i18n](../guides/i18n.md)). An `EmojiItem` with no `group` falls under
251
+ `general`; a `group` is lowercased, with ` & ` and spaces turned into `_`
252
+ ("Food & Drink" becomes `food_drink`). A group the bundles do not name gets
253
+ its words, capitalised, as an English name (`hand_signs` becomes "Hand
254
+ signs"), and an icon of its own is not provided: it shows the generic grid
255
+ icon.
256
+
247
257
  | Prop | Type | Default | Effect |
248
258
  | --- | --- | --- | --- |
249
259
  | `emojis` | `EmojiItem[]` | — | the set offered; `group` drives the category tabs, `shortcodes` the search |
@@ -30,7 +30,9 @@ value type each one writes. Everything is imported from
30
30
 
31
31
  ![Select](../previews/selection-choice-select.webp)
32
32
 
33
- Select primitives with groups, labels and scroll buttons.
33
+ Select primitives with groups, labels and scroll buttons. Its list is modal:
34
+ while it is open the rest of the page is `inert` (a `Select` in a `Dialog`
35
+ makes the dialog inert too, until the list closes).
34
36
 
35
37
  ```tsx
36
38
  import {
@@ -132,9 +134,13 @@ export function Framework() {
132
134
  | `onValueChange` | `(value: string) => void` | — | Radix Select's |
133
135
  | `renderOption` | `(option: SelectOption) => ReactNode` | — | custom item content |
134
136
  | `placeholder` | `ReactNode` | `label` | the trigger's text while nothing is selected |
135
- | `required` | `boolean` | — | an asterisk after the label, and Radix Select's `required` |
137
+ | `required` | `boolean` | — | an asterisk after the label, and Radix Select's `required`; the trigger is named by the label |
136
138
  | `label`, `helperText`, `error` | | — | the field's text and state |
137
139
 
140
+ **Accessibility:** the trigger is named by `label`, described by the help or
141
+ error text (`aria-describedby`) and marked `aria-invalid` on error, so
142
+ `getByRole('combobox', { name: 'Framework' })` finds it and
143
+ `toHaveAccessibleDescription(…)` / `toBeInvalid()` match.
138
144
  **Emits:** `string`.
139
145
  **Form wrapper:** `SelectFormField` — `control` and `name`, see
140
146
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
@@ -530,6 +536,11 @@ export function Topics() {
530
536
  | `onValueChange` | `(value: string[]) => void` | — | |
531
537
  | `options` | `{ value, label, icon? }[]` | — | the chips |
532
538
 
539
+ **Accessibility:** the chips are toggle buttons in a `role="group"` named by
540
+ `label` and described by the help text: a chosen chip is
541
+ `aria-pressed="true"` (never `aria-selected`), in `single` mode too.
542
+ Query `getByRole('group', { name: 'Topics' })`, then
543
+ `getByRole('button', { name: 'Design' })`; there is no `option` role.
533
544
  **Emits:** `string[]`. `SelectChipFormField` stores `string | null` in
534
545
  `single` mode (`null` once the choice is cleared; `''` and `null` both show
535
546
  nothing selected) and `string[]` in `multiple`. Options are matched by
@@ -20,7 +20,7 @@ hooks throw ("Invalid hook call") and design tokens come from two builds.
20
20
  | `react-hook-form` | `^7` | every `*FormField` wrapper ([forms](guides/forms.md)) |
21
21
  | `react-redux`, `@reduxjs/toolkit` | `^9`, `^2` | `DataTable`, filter, gallery, kanban, player, the slice helpers in `/lib` |
22
22
  | `@tanstack/react-table` | `^9` | `DataTable` |
23
- | `react-use` | `^17` | `useTheme`, `useIsMobile`, `ScrollProgress` (`useScroll` / `useWindowScroll`), `CopyButton` (`useCopyToClipboard`), other hooks |
23
+ | `react-use` | `^17` | `useTheme`, `useIsMobile`, `ScrollProgress` (`useScroll` / `useWindowScroll`), other hooks |
24
24
 
25
25
  **Optional peers** are only needed by the entry that uses them. An app that
26
26
  does not import that entry never has them in its tree.
@@ -255,7 +255,9 @@ A `ConfirmationDialog` given `open` follows it, and closes only through
255
255
  open (an alert dialog to its Cancel), and hand it back on close. A test that
256
256
  types or presses a key right after opening one now acts inside it
257
257
  ([details](troubleshooting.md#keys-typed-right-after-opening-a-dialog-sheet-or-drawer-land-inside-it)).
258
- To keep focus where it was, prevent the default:
258
+ To keep focus off the first control, prevent the default (on a modal it
259
+ lands on the content, as the page behind is inert;
260
+ [details](troubleshooting.md#focus-moves-into-a-dialog-although-onopenautofocus-prevented-it)):
259
261
 
260
262
  ```tsx
261
263
  <DialogContent onOpenAutoFocus={(event) => event.preventDefault()}>{/* … */}</DialogContent>
@@ -51,8 +51,8 @@ wraps. Each leaf route renders `ActivityContent`.
51
51
  `useMatches()`. That hook only works under a **data router**:
52
52
  `createBrowserRouter` / `createMemoryRouter` + `RouterProvider`, or a
53
53
  framework-mode app. Under `<BrowserRouter>` or `<MemoryRouter>` it throws
54
- `useMatches must be used within a data router.`. The sidebar and header also
55
- navigate with `react-router`, so the shell cannot render outside a router.
54
+ `useMatches must be used within a data router.`. The sidebar entries are React Router `Link`s too, so the shell cannot render
55
+ outside a router.
56
56
 
57
57
  In a test or a story:
58
58
 
@@ -142,10 +142,36 @@ partitionMenu(items); // { content, footer }
142
142
  | `items` | `MenuItem[]` | — | Sidebar entries, and the mobile menu sheet |
143
143
  | `brand` | `ReactNode` | — | Replaces the sidebar header: a logo, a product name |
144
144
  | `avatar` | `ReactNode` | — | End of the header. A `string` is used as the image URL of an `Avatar`; anything else is rendered as-is |
145
+ | `linkComponent` | `AppShellLinkComponent` | React Router `Link` | The link each menu entry renders, given `href` (see below) |
145
146
  | `headerActions` | `ReactNode` | — | In the header, before the avatar: notifications, help, `ThemeToggle` |
146
147
  | `children` | `ReactNode` | — | The routed content, usually `<Outlet />` |
148
+ | `contentAs` | `'main' \| 'div'` | `'main'` | The content region's element: the page's `<main>` landmark, or a `div` when your app renders its own `<main>` |
149
+ | `scroll` | `'document' \| 'container'` | `'document'` | Who scrolls: the page (header and sidebar sticky), or the content region inside a shell one screen high |
150
+
151
+ Each menu entry is a real link, `<a href="/invoices">`, so it works before
152
+ hydration on a server-rendered page, opens in a new tab on middle-click, and
153
+ is announced as a link, named by its label alone ("Identities": the icon beside
154
+ it is decorative); the current page's entry carries `aria-current="page"`.
155
+ By default it is a React Router `Link` (client-side navigation, the router's
156
+ `basename`), so leaving `linkComponent` out never reloads the page. Pass it only
157
+ for another router or a custom link: a component taking
158
+ `href`. It must spread **every other prop it receives** onto the element it
159
+ renders (`className`, `aria-current`, the tooltip's handlers, `data-state`) and
160
+ forward `ref`: the styling, the tooltip and the current-page mark arrive that
161
+ way. A link that forwards only a `DataTable`'s `DataTableLinkProps` is not
162
+ enough here.
147
163
 
148
- The rest of `div` props (`className`, `id`…) go to the outer container.
164
+ ```tsx
165
+ import { Link } from 'react-router';
166
+ import type { AppShellLinkComponent } from '@nxgt/material/components';
167
+
168
+ // Spread everything but `href`; `ref` is a prop in React 19.
169
+ const MyLink: AppShellLinkComponent = ({ href, ...props }) => <Link to={href} {...props} />;
170
+ // <ActivityLayout items={items} linkComponent={MyLink}>…</ActivityLayout>
171
+ ```
172
+
173
+ `className` and the rest of the `div` props (`id`, `data-*`…) go to the outer
174
+ element, `[data-slot=activity-layout]`.
149
175
  `avatar` and `headerActions` are props because the shell holds no session:
150
176
  read the user wherever your app keeps it and pass the node.
151
177
 
@@ -180,7 +206,54 @@ export function Shell({ items, user }: { items: MenuItem[]; user: { name: string
180
206
  ```
181
207
 
182
208
  On screens below 768 px the sidebar is hidden and the header shows a menu
183
- button that opens the same entries in a sheet. Breadcrumbs are hidden there.
209
+ button ("Open menu") that opens the same entries in a sheet. Breadcrumbs are
210
+ hidden there. The sheet is a dialog named "Navigation"
211
+ (`material.app-shell.navigation`) with a visible "Close" button. Following an
212
+ entry closes it — so does any change of location, Back included — and focus
213
+ goes to the page's `<main>`; closed with Escape, the close button or the
214
+ overlay, focus returns to the menu button. Its entries show their labels, so
215
+ they have no tooltip: none opens over the first entry as focus moves in.
216
+
217
+ ## Landmarks, headings and scrolling
218
+
219
+ - **One `<main>`.** The content region is the page's `<main>` landmark
220
+ (`getByRole('main')`). If your app already renders a `<main>` around the
221
+ shell, pass `contentAs="div"`: a page has only one.
222
+ - **The screen's title is the `h1`.** `ActivityContent`'s `title` is a heading,
223
+ level 1 by default (`titleAs`); the shell itself renders no heading, so a
224
+ screen's title is the page's top one. A `CardTitle` is an `h3` (its `as`
225
+ changes the level).
226
+ - **The document scrolls.** The shell is `100%` wide and at least `100dvh`
227
+ high, so it never adds a scrollbar's width of horizontal scroll and its
228
+ bottom is never under a phone's toolbar. The page grows with the screen and
229
+ the browser scrolls it; the header and the desktop sidebar are sticky.
230
+ `scroll="container"` is the opt-in for a screen that manages its own height
231
+ (a full-height map, a board): the shell is then exactly `100dvh` high and
232
+ the content region scrolls inside it.
233
+ - **A new screen starts at the top.** With the document scrolling, React
234
+ Router's `<ScrollRestoration />` handles positions as for any page. With
235
+ `scroll="container"` the content region outlives navigations, and
236
+ `ScrollRestoration` only knows the window, so the shell does it for that
237
+ box: a new pathname opens at the top, and Back or Forward (a `POP`) returns
238
+ to where that entry was left (kept per `location.key`, for the session). A
239
+ change of query or hash only (a filter, a tab, an `#anchor`) keeps the
240
+ position.
241
+
242
+ ```tsx
243
+ import { Outlet } from 'react-router';
244
+ import { ActivityLayout, type MenuItem } from '@nxgt/material/components';
245
+
246
+ // The app renders its own <main>, and the board below needs a fixed height.
247
+ export function BoardShell({ items }: { items: MenuItem[] }) {
248
+ return (
249
+ <main>
250
+ <ActivityLayout items={items} contentAs="div" scroll="container">
251
+ <Outlet />
252
+ </ActivityLayout>
253
+ </main>
254
+ );
255
+ }
256
+ ```
184
257
 
185
258
  ## `ActivityContent`
186
259
 
@@ -191,6 +264,7 @@ button that opens the same entries in a sheet. Breadcrumbs are hidden there.
191
264
  | `actions` | `ReactNode` | — | Right side of the header card: buttons |
192
265
  | `header` | `ReactNode` | — | Replaces the header card entirely |
193
266
  | `hideHeader` | `boolean` | `false` | No header card |
267
+ | `titleAs` | `'h1'` … `'h6'` \| `'div'` | `'h1'` | The title's element: the screen's heading, `'h2'` under a heading of yours, `'div'` for none |
194
268
 
195
269
  ```tsx
196
270
  import { ActivityContent, Button } from '@nxgt/material/components';
@@ -208,6 +282,9 @@ export function InvoicesPage() {
208
282
  }
209
283
  ```
210
284
 
285
+ The header card wraps: on a narrow screen the actions go under the title
286
+ instead of squeezing it, and a long title wraps rather than scrolling.
287
+
211
288
  ## Breadcrumbs
212
289
 
213
290
  `AppBreadcrumb` (rendered by `ActivityLayout`) starts with a home icon linking
@@ -272,3 +349,19 @@ own. They take the same `items`; `AppHeader` takes `actions`, `avatar` and
272
349
  `showMenuIcon` (default `true`). The lower-level `Sidebar`, `TopAppBar` and
273
350
  `Breadcrumb` primitives are on the
274
351
  [navigation & layout](../components/navigation-and-layout.md) page.
352
+
353
+ `AppHeader` needs a React Router context even on its own: its menu sheet reads
354
+ the location (`useLocation`) to close when it changes, and outside a router
355
+ that hook throws. Render it under your `RouterProvider` — or, in a test, inside
356
+ a `MemoryRouter`:
357
+
358
+ ```tsx
359
+ import { MemoryRouter } from 'react-router';
360
+ import { AppHeader } from '@nxgt/material/components';
361
+
362
+ render(
363
+ <MemoryRouter>
364
+ <AppHeader items={items} />
365
+ </MemoryRouter>,
366
+ );
367
+ ```
@@ -57,7 +57,7 @@ What a wrapper does for you, from the field state of `name`:
57
57
  | `helperText` | `fieldState.error?.message`, or your own `helperText` when there is no error |
58
58
  | `disabled` | your own `disabled`, or `field.disabled` from React Hook Form |
59
59
  | `name` | the field `name` |
60
- | `id` | yours when you pass one. Otherwise the text-like wrappers (text, number, date…) use the field `name`, so two forms on one page sharing a field name get the same `id`: pass your own. `SwitchFormField`, `CheckboxFormField`, `SliderFormField`, `RadioGroupFormField` and `CheckboxGroupFormField` generate one instead; a group's option *i* is `` `${id}-${i}` `` |
60
+ | `id` | yours when you pass one. Otherwise every wrapper generates one (`useId`), never the field `name`, so two forms on one page sharing a field name (a sign-in and a sign-up form) keep their labels apart; a group's option *i* is `` `${id}-${i}` `` |
61
61
  | `aria-labelledby` / `aria-describedby` | on `RadioGroupFormField` and `CheckboxGroupFormField`, the `radiogroup` / `group` is named by its `label` (`` `${id}-label` ``) and described by the helper or error text (`` `${id}-helper` ``), so a screen reader announces the question and the error with the group. The `radiogroup` also gets `aria-invalid`; the checkbox `group` does not (ARIA forbids it there), its error is its description — for an error with no message, a hidden "This value is not valid." inside your `helperText`. Your own `aria-label` / `aria-labelledby` wins; your `aria-describedby` ids are kept, first |
62
62
 
63
63
  Every wrapper has the same shape: the React Hook Form half and the field's own
@@ -223,8 +223,18 @@ export function SubscribeForm({
223
223
  schema's input type match what `NumberFormField` emits, and a cleared field
224
224
  fails with "This field is required.".
225
225
  - `Button` defaults to `type="button"`: the submit button needs
226
- `type="submit"`. `loading` shows a spinner and ignores clicks while the
227
- promise returned by `onSave` is pending.
226
+ `type="submit"`. With `loading={form.formState.isSubmitting}` it is busy
227
+ (`aria-busy`, `aria-disabled`, a spinner) from the first click until the
228
+ promise returned by `onSave` settles, and cancels every click meanwhile — a
229
+ double click, or Enter pressed twice in a field, submits once. It stays
230
+ focusable. For a `loading` that turns true only after an `await` (a
231
+ mutation's `isPending`), add `lockOnClick`
232
+ ([Button](../components/buttons-and-actions.md#button)). The guard sees
233
+ clicks, Space and Enter on the button, and Enter in a field when it is the
234
+ form's first submit button; `form.requestSubmit()` and `form.submit()` fire
235
+ no click, so a submit your code starts (an OTP step that submits on its
236
+ last digit, a shortcut) checks `formState.isSubmitting` itself. A
237
+ `lockOnClick` whose `loading` never turns true releases after one second.
228
238
  - Default values must have the type the field emits. See
229
239
  [What each field emits](#what-each-field-emits).
230
240
 
@@ -247,8 +257,9 @@ Then, for the schema of [A complete form](#a-complete-form):
247
257
 
248
258
  | Input | English | French |
249
259
  | --- | --- | --- |
250
- | `email: 'nope'` | `Invalid email address.` | `adresse e-mail invalide.` |
251
- | `name: 'ab'` for `z.string().min(3)` | `This value is too small: it must be at least 3 characters.` | `Cette valeur est trop petite : elle doit être au minimum 3 caractères.` |
260
+ | `email: 'nope'` | `Invalid email address.` | `Adresse e-mail invalide.` |
261
+ | `name: 'ab'` for `z.string().min(3)` | `Must contain at least 3 characters.` | `Doit contenir au moins 3 caractères.` |
262
+ | `bio` too long for `z.string().max(80)` | `Must contain at most 80 characters.` | `Doit contenir au plus 80 caractères.` |
252
263
  | a missing key (`undefined`) | `This field is required.` | `Ce champ est requis.` |
253
264
 
254
265
  A message set on the schema wins over the error map, so override one field
@@ -280,6 +291,13 @@ How it picks the language, and what that implies:
280
291
  (`form.trigger()` re-runs it).
281
292
  - It is safe on a server: with no `localStorage` (or when reading it throws)
282
293
  it writes English, unless you pass `language`.
294
+ - A string length reads as a length (`Must contain at most 80 characters.`);
295
+ a number or a list keeps the size wording (`This value is too large: it
296
+ must be at most 2 items.`). The strings are `material.zod.too_big_string_*`
297
+ and `material.zod.too_small_string_*` (`inclusive` / `exclusive`): an app
298
+ that overrode `too_big_inclusive` for strings overrides these now.
299
+ - A format message that opens with its noun (`Invalid email address.`,
300
+ `Adresse e-mail invalide.`) starts with a capital letter in every language.
283
301
  - `invalid_type` says what was expected (`z.number()` given a string says
284
302
  `This value must be a number.`); a type with no string of its own says
285
303
  `This value has the wrong type.`
@@ -520,6 +538,37 @@ own entry, so the main barrel cannot render it. Use `LocationFormField` from
520
538
  `Field` is the same renderer without React Hook Form: `<Field config={…} />`,
521
539
  where the config carries `value` and `onChange` / `onValueChange` itself.
522
540
 
541
+ ## Focusing the first invalid field
542
+
543
+ React Hook Form moves focus to the first invalid field on a failed
544
+ `handleSubmit` (`shouldFocusError`, on by default), and to the field you name
545
+ in `setError(name, error, { shouldFocus: true })` or `setFocus(name)`. It does
546
+ that by calling `focus()` on the `ref` the field registered, which every
547
+ `*FormField` (and the config-driven `FormField`) hands over: an input takes
548
+ it directly; a field made of several controls (a select, a group of chips, a
549
+ date range, an upload, a dialog editor) hands over a handle that focuses the
550
+ first control a user can reach in it.
551
+
552
+ ```tsx
553
+ const form = useForm<Values>({ resolver: zodResolver(schema) });
554
+
555
+ // A failed submit focuses the first invalid field, nothing to wire:
556
+ <form onSubmit={form.handleSubmit(save)}>…</form>
557
+
558
+ // A server error on one field:
559
+ form.setError('email', { type: 'server', message: 'Already used' }, { shouldFocus: true });
560
+ form.setFocus('email'); // or without an error
561
+ ```
562
+
563
+ - Only a **mounted** field takes focus: React Hook Form skips one in a closed
564
+ tab or a collapsed section, so open it before calling `setError`.
565
+ - `FilterFormField` is the exception: it holds criteria, not a value that can
566
+ be invalid.
567
+ - A field of your own passes `field.ref` to its control:
568
+ `<input {...field} />` (`{...field}` carries `ref`).
569
+ - If focus does not move, see
570
+ [`setError(…, { shouldFocus: true })` does nothing](../troubleshooting.md#seterror--shouldfocus-true--or-a-failed-submit-does-not-focus-the-field).
571
+
523
572
  ## Server errors
524
573
 
525
574
  Map an API's field errors onto the form with `setError`, and put anything that
@@ -45,7 +45,7 @@ import { cn, DATE_UTILS, formatNumber, searchParam, splitMatch } from '@nxgt/mat
45
45
 
46
46
  export function OrderRow({ order, query }: { order: { ref: string; total: number; createdAt: string; late: boolean }; query: string }) {
47
47
  return (
48
- <tr className={cn('border-b', order.late && 'text-error')}>
48
+ <tr className={cn('border-b', order.late && 'text-error-text')}>
49
49
  <td>
50
50
  {splitMatch(order.ref, query).map((part, i) =>
51
51
  part.match ? <mark key={i}>{part.text}</mark> : <span key={i}>{part.text}</span>,
@@ -119,7 +119,7 @@ Vue: the Redux helpers are Pinia store factories: `defineLocalStore`
119
119
  | `useTheme()` | `{ theme, preference, updateTheme }`: light / dark / system, stored in `localStorage`, applied as the `dark` class. See [theming](theming.md#usetheme). |
120
120
  | `resolveTheme(stored, prefersDark)`, `applyResolvedTheme(theme)` | The pure theme logic behind `useTheme`, for a boot script. |
121
121
  | `useIsClient()` | `false` on the server and first render, `true` after mount: a hydration guard. |
122
- | `useIsMobile()` | `true` below 768 px. `false` on the server and while hydrating, so SSR HTML is the desktop layout; a phone switches right after. |
122
+ | `useIsMobile()` | `true` below 768 px (`ResponsiveButton` no longer uses it: it collapses with CSS). `false` on the server and while hydrating, so SSR HTML is the desktop layout; a phone switches right after. |
123
123
  | `useHeadingSpy(ids, onActive, options?)` | IntersectionObserver over heading ids; reports the topmost visible one. |
124
124
  | `useLocalizedDate()` | `{ language, locale, format }`: `DATE_UTILS.format` bound to the `I18nProvider` language (`en` without a provider). |
125
125
  | `useNumberFormatter()` | `{ formatNumber, formatCurrency }`: `Intl.NumberFormat` with the options you pass, bound to the `I18nProvider` language (`en-US` without a provider). For what `formatAmount` does not cover: percentages, units, compact notation. |
@@ -206,10 +206,13 @@ presets and `Min`/`Max` fields, the `variant="4"` spinner's label,
206
206
  `withCloseButton`. A range `Slider`'s readout inputs read
207
207
  `material.slider.minimum` / `maximum` ("Minimum", "Maximum"), and a
208
208
  single value's input, when nothing names the slider, `material.slider.value`
209
- ("Value", "Valeur"). The other icon-only buttons are named from it too: the
209
+ ("Value", "Valeur"). `CopyButton` reads `material.copy.copy`, `copied` and `failed` ("Copy", "Copied",
210
+ "Copy failed"). The other icon-only buttons are named from it too: the
210
211
  `Sidebar` toggle (`material.sidebar.toggle`), the data table's row expander
211
212
  (`material.table.labels.toggle-row`), its loading status, "Loading data…" /
212
- "Chargement des données…" (`material.table.loading`), a chip's remove button — `Chip`,
213
+ "Chargement des données…" (`material.table.loading`), its scroll region,
214
+ "Scrollable table" / "Tableau défilant"
215
+ (`material.table.labels.scroll-region`, or the `scrollLabel` prop), a chip's remove button — `Chip`,
213
216
  `ComboboxChip` and the `Autocomplete` chips, "Remove {label}"
214
217
  (`material.chip.dismiss-label`) — the `LocationPreview` trigger
215
218
  (`material.map.preview` / `preview-named`), and the delete button of the
@@ -324,10 +327,27 @@ and description and against the English ones, ignoring accents: in French,
324
327
 
325
328
  Some strings are still English in every language, among them the filter's
326
329
  validation messages, the camera's error messages, the editor's callout type
327
- names and default block content, and the names of the emoji picker's category
328
- tabs. File sizes (`2.0 KB`) and the filter chips' dates are not formatted for
330
+ names and default block content. File sizes (`2.0 KB`) and the filter chips' dates are not formatted for
329
331
  the provider's language.
330
332
 
333
+ The emoji picker's category tabs are icon-only; their name, read by screen
334
+ readers and shown as a tooltip, comes from `material.emoji.groups.<group>`.
335
+ Both languages ship `all`, `recent`, `general`, `people_body`, `components`,
336
+ `animals_nature`, `food_drink`, `travel_places`, `activities`, `objects`,
337
+ `symbols` and `flags`:
338
+
339
+ ```ts
340
+ // English / French values of the shipped keys
341
+ material.emoji.groups.people_body; // "People & body" / "Personnes et corps"
342
+ material.emoji.groups.food_drink; // "Food & drink" / "Nourriture et boissons"
343
+ material.emoji.groups.flags; // "Flags" / "Drapeaux"
344
+ ```
345
+
346
+ A group your `EmojiItem`s name that is not in this list (`hand_signs`) reads
347
+ as its words in English (`Hand signs`). As for every `material.*` key above,
348
+ the components read their own bundle: your own translator sees a key you add,
349
+ the tab does not.
350
+
331
351
  ```tsx
332
352
  // Under <I18nProvider defaultLanguage="fr">
333
353
  <CarouselNext /> // screen reader: "Diapositive suivante"