@nxgt/material-vue 0.26.1 → 0.28.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 (168) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README.md +10 -7
  3. package/dist/Button-D1uzdbOJ.js +728 -0
  4. package/dist/{DropdownMenuTrigger-BHAhXGUO.js → DropdownMenuTrigger-BsuyNVzV.js} +48 -41
  5. package/dist/HoverCardTrigger-DaM5d4hE.js +510 -0
  6. package/dist/{camera-DvKuqT4L.js → camera-CxXijySa.js} +4 -4
  7. package/dist/components/base-content.css +139 -0
  8. package/dist/components/base.css +150 -0
  9. package/dist/components/components.css +2 -0
  10. package/dist/components/reduced-motion.css +56 -0
  11. package/dist/components/theme-tokens.css +116 -0
  12. package/dist/components/ui/actions/button-group/button-group.css +1 -1
  13. package/dist/components/ui/feedback/toast/toast.css +21 -0
  14. package/dist/components/ui/primitives/label/label.css +33 -0
  15. package/dist/{components-CCWod7cJ.js → components-Db9OdMfQ.js} +19053 -18364
  16. package/dist/components.js +7 -7
  17. package/dist/composables.js +3 -3
  18. package/dist/dnd.js +1 -1
  19. package/dist/field-name-attribute-Bl7LSVK0.js +4 -0
  20. package/dist/{form-scope-x-7TYn2s.js → form-scope-BILi3ke-.js} +1 -1
  21. package/dist/{i18n-provider-CaaZCmaR.js → i18n-provider-BPK68eqM.js} +1 -1
  22. package/dist/i18n.js +4 -4
  23. package/dist/icon-button-D60G5B_Z.js +262 -0
  24. package/dist/lib/components/ui/actions/button/Button.vue.d.ts +22 -4
  25. package/dist/lib/components/ui/actions/button/button-tones.d.ts +36 -0
  26. package/dist/lib/components/ui/actions/button/button.variants.d.ts +1 -1
  27. package/dist/lib/components/ui/actions/button/button.wrap.d.ts +8 -0
  28. package/dist/lib/components/ui/actions/button/label-text.d.ts +9 -0
  29. package/dist/lib/components/ui/actions/button/prepend-to-child.d.ts +18 -0
  30. package/dist/lib/components/ui/actions/button/use-click-lock.d.ts +14 -0
  31. package/dist/lib/components/ui/actions/copy-button/copy-button.vue.d.ts +6 -2
  32. package/dist/lib/components/ui/actions/icon-button/icon-button.compound-variants.d.ts +28 -0
  33. package/dist/lib/components/ui/actions/icon-button/icon-button.vue.d.ts +1 -1
  34. package/dist/lib/components/ui/actions/responsive-button/responsive-button.vue.d.ts +1 -3
  35. package/dist/lib/components/ui/collections/data-table/partials/data-table-empty.vue.d.ts +5 -1
  36. package/dist/lib/components/ui/collections/data-table/partials/data-table-scroll-region.vue.d.ts +28 -0
  37. package/dist/lib/components/ui/collections/data-table/partials/data-table-toolbar.d.ts +60 -0
  38. package/dist/lib/components/ui/collections/data-table/types.d.ts +9 -0
  39. package/dist/lib/components/ui/collections/data-table/use-scroll-edges.d.ts +22 -0
  40. package/dist/lib/components/ui/collections/pagination/PaginationFirst.vue.d.ts +1 -1
  41. package/dist/lib/components/ui/collections/pagination/PaginationItem.vue.d.ts +1 -1
  42. package/dist/lib/components/ui/collections/pagination/PaginationLast.vue.d.ts +1 -1
  43. package/dist/lib/components/ui/collections/pagination/PaginationNext.vue.d.ts +1 -1
  44. package/dist/lib/components/ui/collections/pagination/PaginationPrevious.vue.d.ts +1 -1
  45. package/dist/lib/components/ui/dates/calendar/Calendar.vue.d.ts +4 -4
  46. package/dist/lib/components/ui/dates/calendar/calendar-day-label.d.ts +17 -0
  47. package/dist/lib/components/ui/dates/calendar/calendar-years.d.ts +11 -0
  48. package/dist/lib/components/ui/dates/calendar/partials/calendar-month-select.vue.d.ts +18 -0
  49. package/dist/lib/components/ui/dates/calendar/partials/calendar-year-select.vue.d.ts +19 -0
  50. package/dist/lib/components/ui/dates/calendar/use-calendar-locale.d.ts +7 -0
  51. package/dist/lib/components/ui/dates/calendar/use-calendar-selected-hint.d.ts +17 -0
  52. package/dist/lib/components/ui/dates/date-range-field/partials/date-range-day.vue.d.ts +8 -0
  53. package/dist/lib/components/ui/display/cards/cards.gallery.d.ts +3 -3
  54. package/dist/lib/components/ui/display/list-tile/list-tile.vue.d.ts +10 -6
  55. package/dist/lib/components/ui/feedback/alert-dialog/AlertDialogContent.vue.d.ts +2 -2
  56. package/dist/lib/components/ui/feedback/sonner/index.d.ts +1 -0
  57. package/dist/lib/components/ui/feedback/toast/toast.d.ts +7 -0
  58. package/dist/lib/components/ui/feedback/toast/toast.vue.d.ts +18 -2
  59. package/dist/lib/components/ui/fields/contacts/consts.d.ts +1 -1
  60. package/dist/lib/components/ui/forms/use-field-blur.d.ts +3 -0
  61. package/dist/lib/components/ui/layout/app-shell/activity-content.vue.d.ts +9 -3
  62. package/dist/lib/components/ui/layout/app-shell/activity-layout.vue.d.ts +21 -4
  63. package/dist/lib/components/ui/layout/app-shell/app-header.vue.d.ts +7 -5
  64. package/dist/lib/components/ui/layout/app-shell/app-sidebar.vue.d.ts +4 -1
  65. package/dist/lib/components/ui/layout/app-shell/index.d.ts +2 -2
  66. package/dist/lib/components/ui/layout/app-shell/router-menu-link.d.ts +7 -0
  67. package/dist/lib/components/ui/layout/app-shell/types.d.ts +8 -0
  68. package/dist/lib/components/ui/layout/app-shell/use-content-scroll.d.ts +13 -0
  69. package/dist/lib/components/ui/layout/app-shell/use-navigation-sheet.d.ts +17 -0
  70. package/dist/lib/components/ui/layout/sidebar/sidebar-context.d.ts +15 -0
  71. package/dist/lib/components/ui/layout/sidebar/sidebar-menu-item.vue.d.ts +13 -4
  72. package/dist/lib/components/ui/media/carousel/carousel-next.vue.d.ts +1 -1
  73. package/dist/lib/components/ui/media/carousel/carousel-previous.vue.d.ts +1 -1
  74. package/dist/lib/components/ui/menu-item-variant.d.ts +4 -4
  75. package/dist/lib/components/ui/overlays/context-menu/ContextMenuContent.vue.d.ts +2 -2
  76. package/dist/lib/components/ui/overlays/dialog/DialogContent.vue.d.ts +2 -2
  77. package/dist/lib/components/ui/overlays/dialog/DialogScrollContent.vue.d.ts +2 -2
  78. package/dist/lib/components/ui/overlays/dialog/use-escape-forward.d.ts +17 -0
  79. package/dist/lib/components/ui/overlays/dialog/use-inert-others.d.ts +17 -0
  80. package/dist/lib/components/ui/overlays/dialog/use-modal-content.d.ts +32 -0
  81. package/dist/lib/components/ui/overlays/drawer/DrawerContent.vue.d.ts +2 -2
  82. package/dist/lib/components/ui/overlays/dropdown-menu/DropdownMenu.vue.d.ts +3 -1
  83. package/dist/lib/components/ui/overlays/dropdown-menu/DropdownMenuContent.vue.d.ts +2 -2
  84. package/dist/lib/components/ui/overlays/popover/PopoverContent.vue.d.ts +2 -2
  85. package/dist/lib/components/ui/overlays/sheet/SheetContent.vue.d.ts +2 -2
  86. package/dist/lib/components/ui/primitives/card/CardTitle.vue.d.ts +11 -4
  87. package/dist/lib/components/ui/primitives/card/index.d.ts +1 -0
  88. package/dist/lib/components/ui/primitives/tabs/Tabs.vue.d.ts +6 -2
  89. package/dist/lib/components/ui/primitives/tabs/TabsList.vue.d.ts +2 -2
  90. package/dist/lib/components/ui/primitives/tabs/TabsTrigger.vue.d.ts +6 -4
  91. package/dist/lib/components/ui/primitives/tabs/tabs-classes.d.ts +27 -0
  92. package/dist/lib/components/ui/primitives/tabs/tabs-indicator.vue.d.ts +25 -0
  93. package/dist/lib/components/ui/primitives/tabs/tabs-variant.d.ts +5 -8
  94. package/dist/lib/components/ui/primitives/toggle/Toggle.vue.d.ts +1 -1
  95. package/dist/lib/components/ui/primitives/toggle/index.d.ts +1 -1
  96. package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-box.vue.d.ts +2 -0
  97. package/dist/lib/components/ui/selection/select/SelectContent.vue.d.ts +2 -2
  98. package/dist/lib/components/ui/selection/select-chip-field/select-chip-field.vue.d.ts +1 -0
  99. package/dist/lib/components/ui/widgets/emoji/types.d.ts +1 -6
  100. package/dist/lib/i18n/resources/en/en.d.ts +23 -0
  101. package/dist/lib/i18n/resources/en/material.json.d.ts +28 -5
  102. package/dist/lib/i18n/resources/fr/fr.d.ts +23 -0
  103. package/dist/lib/i18n/resources/fr/material.json.d.ts +28 -5
  104. package/dist/lib/i18n/resources/resources.d.ts +46 -0
  105. package/dist/lib/i18n/translator.d.ts +1 -1
  106. package/dist/lib/lib/field-name-attribute.d.ts +7 -0
  107. package/dist/lib/lib/focus-field.d.ts +17 -0
  108. package/dist/lib/lib/index.d.ts +1 -0
  109. package/dist/lib.js +76 -74
  110. package/dist/main.js +13 -13
  111. package/dist/map.js +4 -4
  112. package/dist/material-vue.css +1 -1
  113. package/dist/{pdf-outline-sidebar-D3vq2qfS.js → pdf-outline-sidebar-BXGTDISI.js} +2 -2
  114. package/dist/{pdf-page-BrJbyjQj.js → pdf-page-BR4G03kN.js} +1 -1
  115. package/dist/{pdf-page-list-BCgYEszV.js → pdf-page-list-bjQm6_kf.js} +2 -2
  116. package/dist/{pdf-reader-BAzevSJU.js → pdf-reader-DGVEFNc0.js} +9 -9
  117. package/dist/{pdf-sidebar-DSh2Mend.js → pdf-sidebar-CiM6dwAm.js} +3 -3
  118. package/dist/{pdf-thumbnail-sidebar-D_EMcbKF.js → pdf-thumbnail-sidebar-fM3DGJzD.js} +3 -3
  119. package/dist/{pdf-toolbar-BcL7HlfS.js → pdf-toolbar-mWlGTWU1.js} +5 -5
  120. package/dist/pdf.js +2 -2
  121. package/dist/{sortable-provider-DMpmM7Ke.js → sortable-provider-Dml11nQY.js} +2 -2
  122. package/dist/{stored-language-Bxhsx4iB.js → stored-language-UnInBB6T.js} +56 -10
  123. package/dist/{stx-player-MldK_ZAZ.js → stx-player-BS7-CXFx.js} +2 -2
  124. package/dist/styles.css +10 -368
  125. package/dist/{translator-CLI_7Crg.js → translator-J3_vaV_A.js} +1 -1
  126. package/dist/{use-heading-spy-VPuFj2Gt.js → use-heading-spy-_dk8kGwL.js} +19 -19
  127. package/dist/use-inert-others-CtGqZKR8.js +22 -0
  128. package/dist/{use-material-translation-CONqD9wJ.js → use-material-translation-Do6ztdtV.js} +3 -3
  129. package/docs/components/buttons-and-actions.md +71 -16
  130. package/docs/components/data-display.md +13 -2
  131. package/docs/components/data-table-filter-and-lists.md +85 -5
  132. package/docs/components/date-and-time.md +25 -1
  133. package/docs/components/feedback-and-status.md +86 -11
  134. package/docs/components/form-fields.md +13 -4
  135. package/docs/components/media-and-files.md +7 -0
  136. package/docs/components/navigation-and-layout.md +19 -1
  137. package/docs/components/overlays-and-menus.md +20 -4
  138. package/docs/components/primitives.md +97 -20
  139. package/docs/components/rich-widgets.md +10 -0
  140. package/docs/components/selection-and-choice.md +12 -1
  141. package/docs/getting-started.md +2 -2
  142. package/docs/guides/app-shell.md +102 -6
  143. package/docs/guides/forms.md +58 -5
  144. package/docs/guides/helpers.md +2 -1
  145. package/docs/guides/i18n.md +24 -4
  146. package/docs/guides/ssr.md +1 -1
  147. package/docs/guides/theming.md +148 -6
  148. package/docs/previews/data-display-codegroup.webp +0 -0
  149. package/docs/previews/data-display-description.webp +0 -0
  150. package/docs/previews/feedback-status-toast.webp +0 -0
  151. package/docs/previews/primitives-tabs.webp +0 -0
  152. package/docs/previews/rich-widgets-emojipicker.webp +0 -0
  153. package/docs/roadmap.md +30 -1
  154. package/docs/troubleshooting.md +889 -12
  155. package/lib/components/base-content.css +139 -0
  156. package/lib/components/base.css +150 -0
  157. package/lib/components/components.css +2 -0
  158. package/lib/components/reduced-motion.css +56 -0
  159. package/lib/components/theme-tokens.css +116 -0
  160. package/lib/components/ui/actions/button-group/button-group.css +1 -1
  161. package/lib/components/ui/feedback/toast/toast.css +21 -0
  162. package/lib/components/ui/primitives/label/label.css +33 -0
  163. package/lib/styles.css +10 -368
  164. package/package.json +2 -2
  165. package/dist/Button-16J7vTpE.js +0 -665
  166. package/dist/HoverCardTrigger-C41002JS.js +0 -455
  167. package/dist/icon-button-CDdNsZuZ.js +0 -227
  168. package/dist/lib/components/ui/widgets/emoji/emoji.groups.d.ts +0 -6
@@ -163,7 +163,9 @@ import {
163
163
 
164
164
  Parts: `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`,
165
165
  `CardContent`, `CardFooter` — each a styled `<div>` taking `class` and the
166
- element's attributes. `CardAction` sits top-right of the header.
166
+ element's attributes, except `CardTitle`, a heading: an `h3` by default,
167
+ another level or a `div` with `as` (`<CardTitle as="h2">`), so
168
+ `getByRole('heading', { name })` finds it. `CardAction` sits top-right of the header.
167
169
 
168
170
  **Storybook:** `Primitives/Card` · **React:** `Card` (same parts)
169
171
 
@@ -246,12 +248,24 @@ import { Input, Label } from '@nxgt/material-vue/components';
246
248
 
247
249
  | Prop | Type | Default | Effect |
248
250
  | --- | --- | --- | --- |
249
- | `with-asterisk` | `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 |
251
+ | `with-asterisk` | `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 |
250
252
  | `for` | `string` | — | the control it labels; clicking the label focuses it |
251
253
 
252
254
  Fields on the [Form fields](form-fields.md) page render it for you through
253
255
  their `label` prop.
254
256
 
257
+ To restyle the mark, target `[data-slot="label-text"][data-required]::after`:
258
+ `background-color` sets its colour (default `var(--color-error)`), `width` and
259
+ `height` its size. It stays visible in forced-colors mode and in print.
260
+
261
+ ```css
262
+ [data-slot="label-text"][data-required]::after {
263
+ background-color: #b45309;
264
+ width: 0.75em;
265
+ height: 0.75em;
266
+ }
267
+ ```
268
+
255
269
  **Storybook:** `Primitives/Label` · **React:** `Label`
256
270
 
257
271
  ---
@@ -570,12 +584,12 @@ use `DataTable` on the [Data table, filter & lists](data-table-filter-and-lists.
570
584
 
571
585
  ![Tabs](../previews/primitives-tabs.webp)
572
586
 
573
- Tabs whose triggers can take an icon. `variant="line"` is the quieter pill style.
587
+ Tabs whose triggers can take an icon, in two looks: `underline` (the default) and the `segmented` pill.
574
588
 
575
589
  ```vue
576
590
  <script setup lang="ts">
591
+ import { Calendar, User } from '@lucide/vue';
577
592
  import {
578
- Icon,
579
593
  Tabs,
580
594
  TabsContent,
581
595
  TabsList,
@@ -586,8 +600,11 @@ import {
586
600
  <template>
587
601
  <Tabs default-value="account" class="w-md">
588
602
  <TabsList>
589
- <TabsTrigger value="account"><Icon name="user" />Account</TabsTrigger>
590
- <TabsTrigger value="calendar"><Icon name="calendar" />Calendar</TabsTrigger>
603
+ <TabsTrigger value="account" :icon="User">Account</TabsTrigger>
604
+ <TabsTrigger value="calendar">
605
+ <template #icon><Calendar /></template>
606
+ Calendar
607
+ </TabsTrigger>
591
608
  </TabsList>
592
609
  <TabsContent value="account">Your profile.</TabsContent>
593
610
  <TabsContent value="calendar">Your events.</TabsContent>
@@ -595,36 +612,96 @@ import {
595
612
  </template>
596
613
  ```
597
614
 
598
- Parts: `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`. An icon goes in the
599
- trigger's slot, before the label; there is no `icon` prop in Vue. By default
600
- (`variant="bar"`) the list is one muted pill with the active trigger raised.
601
- `variant="line"` is React's quieter style: a full-width, scrolling strip of
602
- light tabs, the active one on the background — `TabsList` and `TabsTrigger`
603
- read it from `Tabs`.
615
+ Parts: `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`. `TabsList` and
616
+ `TabsTrigger` read the look from `Tabs`. A trigger's icon goes before its
617
+ label, as `@nxgt/material`'s `icon`: pass a component to the `icon` prop, or
618
+ fill the `#icon` slot. An icon written in the default slot still renders
619
+ beside the label.
620
+
621
+ **`underline`** (the default) lays the tabs over a border; the active one
622
+ takes the primary colour, and a bar under it slides to the tab clicked or
623
+ reached with the arrow keys. The list scrolls sideways when the tabs do not
624
+ fit (its scrollbar hidden), which suits a phone or a narrow popover.
625
+
626
+ **`segmented`** is a neutral pill track: the active tab sits on a raised
627
+ surface that slides between tabs, and its label turns to the foreground
628
+ colour and semibold — the state never rests on the surface alone. It suits a
629
+ few short choices, such as a view switch.
630
+
631
+ ```vue
632
+ <template>
633
+ <Tabs default-value="week" variant="segmented">
634
+ <TabsList>
635
+ <TabsTrigger value="day">Day</TabsTrigger>
636
+ <TabsTrigger value="week">Week</TabsTrigger>
637
+ <TabsTrigger value="month">Month</TabsTrigger>
638
+ </TabsList>
639
+ <TabsContent value="day">Today.</TabsContent>
640
+ <TabsContent value="week">This week.</TabsContent>
641
+ <TabsContent value="month">This month.</TabsContent>
642
+ </Tabs>
643
+ </template>
644
+ ```
645
+
646
+ `orientation="vertical"` lays the tabs in a column, the indicator on their
647
+ left side; the arrow keys become Up and Down.
604
648
 
605
649
  ```vue
606
650
  <template>
607
- <Tabs default-value="ts" variant="line">
651
+ <Tabs default-value="profile" orientation="vertical">
608
652
  <TabsList>
609
- <TabsTrigger value="ts">TypeScript</TabsTrigger>
610
- <TabsTrigger value="bash">Bash</TabsTrigger>
653
+ <TabsTrigger value="profile">Profile</TabsTrigger>
654
+ <TabsTrigger value="security">Security</TabsTrigger>
611
655
  </TabsList>
612
- <TabsContent value="ts">const n = 1;</TabsContent>
613
- <TabsContent value="bash">bun install</TabsContent>
656
+ <TabsContent value="profile">Your profile.</TabsContent>
657
+ <TabsContent value="security">Your sessions.</TabsContent>
614
658
  </Tabs>
615
659
  </template>
616
660
  ```
617
661
 
662
+ The sliding indicator is reka's `TabsIndicator`, which renders once mounted:
663
+ server HTML, and a page without JavaScript, show a static indicator under (or
664
+ behind) the active tab instead, and hydrating swaps one for the other without
665
+ a mismatch. The list then carries `data-indicator="ready"`. The focus ring is
666
+ an outline in the primary colour inside the tab, so a scrolling list never
667
+ clips it.
668
+
669
+ With `TabsList asChild`, only the static indicator is drawn.
670
+
671
+ `dir="rtl"` on `Tabs` (or reka's `ConfigProvider`) runs the tabs from the
672
+ right: the indicator follows them, and the Left arrow key moves to the next
673
+ tab. Under `prefers-reduced-motion: reduce` the indicator jumps to the active
674
+ tab instead of sliding.
675
+
676
+ **A tab with only an icon needs a name.** A screen reader announces a
677
+ `TabsTrigger` by its text; with an icon and no label it is only "tab". Give it
678
+ `aria-label`, and `title` for sighted users who hover it:
679
+
680
+ ```vue
681
+ <TabsTrigger value="recent" :icon="Clock" aria-label="Recent" title="Recent" />
682
+ ```
683
+
684
+ The emoji picker's group tabs are named this way, from
685
+ `material.emoji.groups.*`.
686
+
687
+ **The default look changed.** A `Tabs` with no `variant` used to render the
688
+ `bar` look (one muted pill with the active trigger raised); it now renders
689
+ `underline`. `variant="bar"` and `variant="line"` are deprecated aliases:
690
+ `bar` renders as `underline`, `line` as `segmented`. Both still type-check and
691
+ render; write `underline` or `segmented` instead.
692
+
618
693
  | Prop | Type | Default | Effect |
619
694
  | --- | --- | --- | --- |
620
- | `variant` (`Tabs`) | `'bar' \| 'line'` | `'bar'` | the look of the list and its triggers |
695
+ | `variant` (`Tabs`) | `'underline' \| 'segmented' \| 'bar' \| 'line'` | `'underline'` | the look; `bar` and `line` are deprecated aliases of `underline` and `segmented` |
621
696
  | `default-value` / `v-model` (`Tabs`) | `string \| number` | — | the selected tab, uncontrolled / controlled |
622
- | `orientation` (`Tabs`) | `'horizontal' \| 'vertical'` | `'horizontal'` | arrow-key direction |
697
+ | `orientation` (`Tabs`) | `'horizontal' \| 'vertical'` | `'horizontal'` | a row or a column, and the arrow keys' direction |
698
+ | `dir` (`Tabs`) | `'ltr' \| 'rtl'` | `'ltr'` | the reading direction: which way the tabs run and the arrow keys move |
623
699
  | `activation-mode` (`Tabs`) | `'automatic' \| 'manual'` | `'automatic'` | select on focus, or only on Enter / click |
624
700
  | `value` (`TabsTrigger`, `TabsContent`) | `string \| number` | — (required) | pairs a trigger with its panel |
701
+ | `icon` (`TabsTrigger`) | `Component` | — | shown before the label; or the `#icon` slot |
625
702
 
626
703
  **Emits:** `update:modelValue` on `Tabs` — `string | number`.
627
- **Storybook:** `Primitives/Tabs` (story `Line`) · **React:** `Tabs` (same parts)
704
+ **Storybook:** `Primitives/Tabs` (stories `Underline`, `Segmented`, `WithIcons`, `Scrolling`, `RightToLeft`, `Vertical`, `DeprecatedAliases`, `Hydrated`) · **React:** `Tabs` (same parts, the same variants)
628
705
 
629
706
  ---
630
707
 
@@ -272,6 +272,16 @@ const emojis: EmojiItem[] = [
272
272
  Parts: `EmojiPicker` (a button opening a popover), `EmojiList` (the searchable
273
273
  grid on its own, same `emojis` / `@select`), `expandEmojiShortcodes`.
274
274
 
275
+ The category tabs are icon-only. Each is named from
276
+ `material.emoji.groups.<group>` (for example `people_body`, `flags`): that name
277
+ is its `aria-label` and `title`, in the provider's language (see
278
+ [i18n](../guides/i18n.md)). An `EmojiItem` with no `group` falls under
279
+ `general`; a `group` is lowercased, with ` & ` and spaces turned into `_`
280
+ ("Food & Drink" becomes `food_drink`). A group the bundles do not name gets
281
+ its words, capitalised, as an English name (`hand_signs` becomes "Hand
282
+ signs"), and an icon of its own is not provided: it shows the generic grid
283
+ icon.
284
+
275
285
  | Prop | Type | Default | Effect |
276
286
  | --- | --- | --- | --- |
277
287
  | `emojis` | `EmojiItem[]` | — (required) | the set offered; `group` drives the category tabs, `shortcodes` the search |
@@ -31,6 +31,8 @@ value type each one writes. Everything is imported from
31
31
  ![Select](../previews/selection-choice-select.webp)
32
32
 
33
33
  Select primitives with groups, labels and scroll buttons, plus a styled native `<select>`.
34
+ Its list is modal: while it is open the rest of the page is `inert` (a
35
+ `Select` in a `Dialog` makes the dialog inert too, until the list closes).
34
36
 
35
37
  ```vue
36
38
  <script setup lang="ts">
@@ -140,7 +142,7 @@ const framework = ref('nuxt');
140
142
  | `options` | `{ value: string; label: string }[]` | — | required; the items |
141
143
  | `v-model` | `string` | `''` | the selected `value`; `''` for nothing selected |
142
144
  | `placeholder` | `string` | `label` | the trigger's text while nothing is selected |
143
- | `required` | `boolean` | — | an asterisk after the label, and the select's `required` |
145
+ | `required` | `boolean` | — | an asterisk after the label, and the select's `required`; the trigger is named by the label |
144
146
  | `label`, `helper-text`, `error`, `disabled` | | — | the field's text and state |
145
147
 
146
148
  `#option` renders each option's content in the list (React's
@@ -161,6 +163,10 @@ the picked option's text.
161
163
 
162
164
  `SelectFormField` passes the slot through.
163
165
 
166
+ **Accessibility:** the trigger is named by `label`, described by the help or
167
+ error text (`aria-describedby`) and marked `aria-invalid` on error, so
168
+ `getByRole('combobox', { name: 'Framework' })` finds it and
169
+ `toHaveAccessibleDescription(…)` / `toBeInvalid()` match.
164
170
  **Emits:** `update:modelValue` with `string`.
165
171
  **Form wrapper:** `SelectFormField` — `name` inside a vee-validate form, see
166
172
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
@@ -571,6 +577,11 @@ const picked = ref<string[]>([]);
571
577
  | `options` | `{ value: string; label: string }[]` | — | required; the chips |
572
578
  | `label`, `helper-text`, `error`, `disabled` | | — | the field's text and state |
573
579
 
580
+ **Accessibility:** the chips are toggle buttons in a `role="group"` named by
581
+ `label` and described by the help text: a chosen chip is
582
+ `aria-pressed="true"` (never `aria-selected`), in `single` mode too.
583
+ Query `getByRole('group', { name: 'Topics' })`, then
584
+ `getByRole('button', { name: 'Design' })`; there is no `option` role.
574
585
  **Emits:** `update:modelValue` with `string[]`. `SelectChipFormField` stores
575
586
  `string[]` in both modes (React stores `string | null` in `single` mode).
576
587
  **Form wrapper:** `SelectChipFormField` — `name` inside a vee-validate form, see
@@ -21,7 +21,7 @@ the **required peers**. The app must resolve a single copy of each, otherwise
21
21
  | `vue-router` | `^4.5 \|\| ^5` | the app shell, `LinkButton`, `useSearchParam`, `useNavigationHandler` |
22
22
  | `vee-validate` | `^4.15` | every `*FormField` wrapper and the built-in forms ([forms](guides/forms.md)) |
23
23
  | `@tanstack/vue-table` | `^9` | `DataTable` |
24
- | `@vueuse/core` | `^13 \|\| ^14` | `useTheme`, `useIsMobile`, `ScrollProgress`, `CopyButton`, prop forwarding in primitives |
24
+ | `@vueuse/core` | `^13 \|\| ^14` | `useTheme`, `useIsMobile`, `ScrollProgress`, prop forwarding in primitives |
25
25
 
26
26
  **Optional peers** are only needed by the entry that uses them. An app that
27
27
  does not import that entry never has them in its tree.
@@ -350,7 +350,7 @@ touched, and validated, when focus leaves it, so a required field shows its
350
350
  error as the user moves past it rather than only on submit
351
351
  ([details](troubleshooting.md#metatouched-stays-false-after-leaving-a-field)).
352
352
  An `AlertDialogContent` whose `@open-auto-focus` calls `event.preventDefault()`
353
- now keeps focus where it was, as the other modals do
353
+ now keeps focus off its Cancel, as the other modals do
354
354
  ([details](troubleshooting.md#an-alertdialog-focuses-its-cancel-although-open-auto-focus-prevented-it)).
355
355
  A `DrawerContent` hands focus back, on close, to the element that opened it,
356
356
  where it used to fall to `<body>`
@@ -66,7 +66,7 @@ and `definePageMeta({ breadcrumb: 'Invoices' })` in each page.
66
66
 
67
67
  <a id="it-needs-a-data-router"></a>
68
68
 
69
- The sidebar navigates with `useRouter().push()`, and the sidebar and
69
+ The sidebar entries are `RouterLink`s, and the sidebar and
70
70
  `AppBreadcrumb` read `useRoute()`. Install vue-router before mount
71
71
  (`app.use(router)`, see [getting started](../getting-started.md#pinia-and-router));
72
72
  without it the shell cannot render. Any history works: `createWebHistory`,
@@ -175,12 +175,39 @@ partitionMenu(items); // { content, footer }
175
175
  | --- | --- | --- | --- |
176
176
  | `items` | `MenuItem[]` | — (required) | Sidebar entries, and the mobile menu sheet |
177
177
  | `avatar-src` | `string` | — | End of the header: the image URL of an `Avatar` |
178
- | `class` | `string` | — | Classes for the inner layout box |
178
+ | `link-component` | `AppShellLinkComponent` (a `Component`) | `RouterLink` | The link each menu entry renders, given `href` (see below) |
179
+ | `content-as` | `'main' \| 'div'` | `'main'` | The content region's element: the page's `<main>` landmark, or a `div` when your app renders its own `<main>` |
180
+ | `scroll` | `'document' \| 'container'` | `'document'` | Who scrolls: the page (header and sidebar sticky), or the content region inside a shell one screen high |
181
+ | `class` | `string` | — | Classes for the outer element (`[data-slot=activity-layout]`), which takes the other attributes too |
179
182
  | `#brand` | slot | an atom icon | Replaces the sidebar header: a logo, a product name. Always fill it; the default is a placeholder |
180
183
  | `#header-actions` | slot | — | In the header, before the avatar: notifications, help, `LanguageSwitcher`, `ThemeToggle` |
181
184
  | `#avatar` | slot | the `avatar-src` image | Replaces the avatar entirely |
182
185
  | default | slot | — | The routed content, usually `<RouterView />` |
183
186
 
187
+ Each menu entry is a real link, `<a href="/invoices">`, so it works before
188
+ hydration on a server-rendered page, opens in a new tab on middle-click, and
189
+ is announced as a link, named by its label alone ("Identities": the icon beside
190
+ it is decorative); the current page's entry carries `aria-current="page"`.
191
+ By default it is a `RouterLink` (client-side navigation, no reload), so
192
+ `link-component` is only needed for another router or a custom link: pass it a
193
+ component that takes `href` and renders a single root element with its default slot. Vue falls the rest of the
194
+ attributes (`class`, `aria-current`, the tooltip's handlers, `data-state`)
195
+ through to that root, so the root must be one element, not a fragment, and must
196
+ take them: do not set `inheritAttrs: false` without binding `$attrs` yourself.
197
+ A link that forwards only a `DataTable`'s link props is not enough here.
198
+
199
+ The prop's type is exported as `AppShellLinkComponent` (any `Component`):
200
+
201
+ ```ts
202
+ import type { AppShellLinkComponent } from '@nxgt/material-vue/components';
203
+ import { defineComponent, h } from 'vue';
204
+
205
+ export const MyLink: AppShellLinkComponent = defineComponent({
206
+ props: { href: { type: String, required: true } },
207
+ setup: (props, { slots }) => () => h('a', { href: props.href }, slots.default?.()),
208
+ });
209
+ ```
210
+
184
211
  The shell holds no session: read the user wherever your app keeps it and fill
185
212
  the slots.
186
213
 
@@ -219,10 +246,56 @@ defineProps<{ items: MenuItem[]; user: { name: string; picture?: string } }>();
219
246
  </template>
220
247
  ```
221
248
 
222
- The layout fills the viewport (it sits on `Background`, which is
223
- `100lvw × 100lvh`). On screens below 768 px the sidebar is hidden and the
224
- header shows a menu button that opens the same entries in a sheet.
225
- Breadcrumbs are hidden there.
249
+ On screens below 768 px the sidebar is hidden and the header shows a menu
250
+ button ("Open menu") that opens the same entries in a sheet. Breadcrumbs are
251
+ hidden there. The sheet is a dialog named "Navigation"
252
+ (`material.app-shell.navigation`) with a visible "Close" button. Following an
253
+ entry closes it — so does any route change, Back included — and focus goes to
254
+ the page's `<main>`; closed with Escape, the close button or the overlay,
255
+ focus returns to the menu button. Its entries show their labels, so they have
256
+ no tooltip: none opens over the first entry as focus moves in.
257
+
258
+ ## Landmarks, headings and scrolling
259
+
260
+ - **One `<main>`.** The content region is the page's `<main>` landmark
261
+ (`getByRole('main')`). If your app already renders a `<main>` around the
262
+ shell, pass `content-as="div"`: a page has only one.
263
+ - **The screen's title is the `h1`.** `ActivityContent`'s `title` is a heading,
264
+ level 1 by default (`title-as`); the shell itself renders no heading, so a
265
+ screen's title is the page's top one. A `CardTitle` is an `h3` (its `as`
266
+ changes the level).
267
+ - **The document scrolls.** The shell sits on `Background` but is `100%` wide
268
+ and at least `100dvh` high, so it never adds a scrollbar's width of
269
+ horizontal scroll and its bottom is never under a phone's toolbar. The page
270
+ grows with the screen and the browser scrolls it; the header and the
271
+ desktop sidebar are sticky. `scroll="container"` is the opt-in for a screen
272
+ that manages its own height (a full-height map, a board): the shell is then
273
+ exactly `100dvh` high and the content region scrolls inside it.
274
+ - **A new screen starts at the top.** With the document scrolling, the
275
+ router's `scrollBehavior` handles positions as for any page. With
276
+ `scroll="container"` the content region outlives navigations, and
277
+ `scrollBehavior` only knows the window, so the shell does it for that box: a
278
+ new path opens at the top, and Back or Forward returns to where that entry
279
+ was left (kept per history entry — `history.state.position`, or the
280
+ `fullPath` with a memory history — for the session). A change of query or
281
+ hash only (a filter, a tab, an `#anchor`) keeps the position.
282
+
283
+ ```vue
284
+ <script setup lang="ts">
285
+ import { ActivityLayout, type MenuItem } from '@nxgt/material-vue/components';
286
+
287
+ defineProps<{ items: MenuItem[] }>();
288
+ </script>
289
+
290
+ <template>
291
+ <!-- The app renders its own <main>, and the board below needs a fixed height. -->
292
+ <main>
293
+ <ActivityLayout :items="items" content-as="div" scroll="container">
294
+ <RouterView />
295
+ </ActivityLayout>
296
+ </main>
297
+ </template>
298
+ ```
226
299
 
227
300
  ## `ActivityContent`
228
301
 
@@ -231,6 +304,7 @@ Breadcrumbs are hidden there.
231
304
  | `title` | `string` | — | Header card title |
232
305
  | `subtitle` | `string` | — | Under the title |
233
306
  | `hide-header` | `boolean` | `false` | No header card |
307
+ | `title-as` | `'h1'` … `'h6'` \| `'div'` | `'h1'` | The title's element: the screen's heading, `'h2'` under a heading of yours, `'div'` for none |
234
308
  | `#actions` | slot | — | Right side of the header card: buttons |
235
309
  | `#header` | slot | — | Replaces the header card entirely |
236
310
  | default | slot | — | The screen's content |
@@ -250,6 +324,9 @@ import { ActivityContent, Button } from '@nxgt/material-vue/components';
250
324
  </template>
251
325
  ```
252
326
 
327
+ The header card wraps: on a narrow screen the actions go under the title
328
+ instead of squeezing it, and a long title wraps.
329
+
253
330
  ## Breadcrumbs
254
331
 
255
332
  `AppBreadcrumb` (rendered by `ActivityLayout`) starts with a home icon linking
@@ -302,3 +379,22 @@ takes `expanded` (`v-model:expanded`) and `with-toggle-icon`, and has the
302
379
  bar. The lower-level `Sidebar`, `TopAppBar`, `NavigationRail` and `Breadcrumb`
303
380
  primitives are on the
304
381
  [navigation & layout](../components/navigation-and-layout.md) page.
382
+
383
+ `AppHeader` and `AppSidebar` also work without Vue Router, given a
384
+ `link-component` of your own (the default, `RouterLink`, needs one). Without a
385
+ router no entry is marked active, and the header's menu sheet closes only when
386
+ an entry is followed rather than on every route change. `AppBreadcrumb` is
387
+ built from the route and needs Vue Router.
388
+
389
+ ```vue
390
+ <script setup lang="ts">
391
+ import { AppHeader, type MenuItem } from '@nxgt/material-vue/components';
392
+ import PlainLink from './plain-link.vue'; // renders <a :href="href"><slot /></a>
393
+
394
+ defineProps<{ items: MenuItem[] }>();
395
+ </script>
396
+
397
+ <template>
398
+ <AppHeader :items="items" :link-component="PlainLink" />
399
+ </template>
400
+ ```
@@ -52,7 +52,7 @@ What a wrapper does for you, through `useField(() => props.name)`:
52
52
  | `error` | `true` when the field has an error message, or is touched and invalid (the red border) |
53
53
  | `helperText` | the field's error message, or your own `helperText` when there is none |
54
54
  | `name` | the `name` you passed, also used as the native `name` |
55
- | `id` | passed through; `RadioGroupField` and `CheckboxGroup` generate one when absent, and a group's option *i* is `` `${id}-${i}` `` |
55
+ | `id` | passed through; when absent, every field generates one (`useId`), never the field `name`, so two forms on one page sharing a field name keep their labels apart; a group's option *i* is `` `${id}-${i}` `` |
56
56
  | everything else | your props, passed through unchanged (`label`, `disabled`, `options`, …) |
57
57
 
58
58
  `RadioGroupFormField` and `CheckboxGroupFormField` need nothing more for a
@@ -201,8 +201,17 @@ const onSubmit = handleSubmit((values) => props.onSave(values));
201
201
  validates, and calls `fn` only when the form is valid. `isSubmitting` stays
202
202
  `true` until the promise `fn` returns settles.
203
203
  - `Button` defaults to `type="button"`: the submit button needs
204
- `type="submit"`. `loading` shows a spinner and disables it; pass it
205
- `isSubmitting`.
204
+ `type="submit"`. With `:loading="isSubmitting"` it is busy (`aria-busy`,
205
+ `aria-disabled`, a spinner) from the first click until the promise settles,
206
+ and cancels every click meanwhile — a double click, or Enter pressed twice
207
+ in a field, submits once. It stays focusable. For a `loading` that turns
208
+ true only after an `await`, add `lock-on-click`
209
+ ([Button](../components/buttons-and-actions.md#button)). The guard sees
210
+ clicks, Space and Enter on the button, and Enter in a field when it is the
211
+ form's first submit button; `form.requestSubmit()` and `form.submit()` fire
212
+ no click, so a submit your code starts (an OTP step that submits on its
213
+ last digit, a shortcut) checks `isSubmitting` itself. A `lock-on-click`
214
+ whose `loading` never turns true releases after one second.
206
215
  - Initial values must have the type the field emits. See
207
216
  [What each field emits](#what-each-field-emits).
208
217
 
@@ -273,8 +282,9 @@ Then, for these schemas:
273
282
 
274
283
  | Input | English | French |
275
284
  | --- | --- | --- |
276
- | `email: 'nope'` for `z.email()` | `Invalid email address.` | `adresse e-mail invalide.` |
277
- | `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.` |
285
+ | `email: 'nope'` for `z.email()` | `Invalid email address.` | `Adresse e-mail invalide.` |
286
+ | `name: 'ab'` for `z.string().min(3)` | `Must contain at least 3 characters.` | `Doit contenir au moins 3 caractères.` |
287
+ | `bio` too long for `z.string().max(80)` | `Must contain at most 80 characters.` | `Doit contenir au plus 80 caractères.` |
278
288
  | a missing key, or `null` / `''` / `0` given to a schema of another type (`seats: null` for `z.number()`) | `This field is required.` | `Ce champ est requis.` |
279
289
 
280
290
  A message set on the schema wins over the error map, so override one field
@@ -313,6 +323,13 @@ How it picks the language, and what that implies:
313
323
  - The message is computed when validation runs. An error already on screen
314
324
  keeps its language after a switch until the field is validated again
315
325
  (`validate()` from `useForm` re-runs it).
326
+ - A string length reads as a length (`Must contain at most 80 characters.`);
327
+ a number or a list keeps the size wording (`This value is too large: it
328
+ must be at most 2 items.`). The strings are `material.zod.too_big_string_*`
329
+ and `material.zod.too_small_string_*` (`inclusive` / `exclusive`): an app
330
+ that overrode `too_big_inclusive` for strings overrides these now.
331
+ - A format message that opens with its noun (`Invalid email address.`,
332
+ `Adresse e-mail invalide.`) starts with a capital letter in every language.
316
333
  - `invalid_type` with an empty input (`undefined`, `null`, `''`, `0`,
317
334
  `false`) says `This field is required.`; with any other input it says
318
335
  what was expected (`z.number()` given `'x'` gives
@@ -576,6 +593,42 @@ props, and `Field` does neither.
576
593
  barrel cannot render it. Use `LocationFormField` from
577
594
  `@nxgt/material-vue/map`.
578
595
 
596
+ ## Focusing the first invalid field
597
+
598
+ vee-validate never moves focus: a failed `handleSubmit` reports
599
+ `{ errors }` to its second callback and that is all (React Hook Form's
600
+ `shouldFocusError` has no equivalent). Every `*FormField` marks the element it
601
+ renders with `data-field-name="<name>"`, and two helpers from
602
+ `@nxgt/material-vue/lib` use it to focus the first control a user can reach in
603
+ a field, whatever its shape (an input, a select trigger, the first chip, a
604
+ file picker's button, the first of two date inputs):
605
+
606
+ ```vue
607
+ <script setup lang="ts">
608
+ import { focusField, focusFirstInvalid } from '@nxgt/material-vue/lib';
609
+ import { useForm } from 'vee-validate';
610
+
611
+ const { handleSubmit, setFieldError } = useForm<Values>();
612
+
613
+ // A failed submit: the first field, in page order, that has an error.
614
+ const onSubmit = handleSubmit(save, ({ errors }) => focusFirstInvalid(errors));
615
+
616
+ // A server error on one field:
617
+ function reject() {
618
+ setFieldError('email', 'Already used');
619
+ focusField('email');
620
+ }
621
+ </script>
622
+ ```
623
+
624
+ - `focusFirstInvalid(errors, scope?)` takes the first field in **page order**
625
+ whose name is in `errors`, not the first key of `errors`. Pass the form
626
+ element as `scope` on a page with several forms.
627
+ - `focusField(name, scope?)` focuses one field. Both return whether a control
628
+ took focus: a field that is not mounted (a closed tab) cannot.
629
+ - A field of your own that wants the same calls `useFieldBlur` (it stamps the
630
+ `name` prop) or sets `data-field-name` itself.
631
+
579
632
  ## Server errors
580
633
 
581
634
  Map an API's field errors onto the form with `setErrors` (or `setFieldError`
@@ -41,6 +41,7 @@ differences are listed under each table. React's `/hooks` is
41
41
  | `STYLES_UTILS.cssSize(value)` | `240` → `'240px'`, a string unchanged, `undefined` for empty. Vue does not add `px` to a numeric inline style the way React does. |
42
42
  | `searchParams(request)`, `searchParam(request, key)` | Reads the query string of a Fetch `Request` (a server handler, an edge function). |
43
43
  | `isExternalHref(href)` | True for `http(s):`, protocol-relative `//` and `mailto:`: open in a new tab, not in-app. |
44
+ | `focusFirstInvalid(errors, scope?)`, `focusField(name, scope?)` | Focus the first control a user can reach in the first field, in page order, that has an error in `errors` (vee-validate's `onInvalid` `{ errors }`), or in the field `name`; each `*FormField` marks its element with its name for them. `scope` limits the lookup to one form; both return whether a control took focus. See [forms](forms.md#focusing-the-first-invalid-field). |
44
45
  | `buildFieldOptions(values, { valueKey, label })` | Maps records to `{ value, label }` options; `valueKey` and a string `label` are paths (`'owner.name'`), or `label` is a function. |
45
46
  | `zodLocaleError(options?)` | Zod v4 `customError` that translates issues through `material.zod.*`: `z.config({ customError: zodLocaleError() })`. Options `storageKey`, `language`, `translate` (type `ZodLocaleErrorOptions`): see [below](#zodlocaleerror-limits). |
46
47
  | `parsedType(data)` | Runtime type name used for Zod's `{received}` placeholder. |
@@ -56,7 +57,7 @@ defineProps<{
56
57
  </script>
57
58
 
58
59
  <template>
59
- <tr :class="cn('border-b', order.late && 'text-error')">
60
+ <tr :class="cn('border-b', order.late && 'text-error-text')">
60
61
  <td>
61
62
  <template v-for="(part, i) in splitMatch(order.ref, query)" :key="i">
62
63
  <mark v-if="part.match">{{ part.text }}</mark>
@@ -278,10 +278,13 @@ previous and next buttons, `DateRangeField`'s included (`material.calendar.*`),
278
278
  checkboxes, the filter's presets and `Min`/`Max` fields, the `variant="4"` spinner's label,
279
279
  `ScrollToTop`, the network upload field's `Retry`/`Remove`, the map's
280
280
  `You are here` and zoom buttons, and the close button of dialogs, sheets and
281
- `DialogFooter`. The other icon-only buttons are named from it too: the
281
+ `DialogFooter`. `CopyButton` reads `material.copy.copy`, `copied` and `failed` ("Copy", "Copied",
282
+ "Copy failed"). The other icon-only buttons are named from it too: the
282
283
  `Sidebar` toggle (`material.sidebar.toggle`), the data table's row expander
283
284
  (`material.table.labels.toggle-row`), its loading status, "Loading data…" /
284
- "Chargement des données…" (`material.table.loading`), a `Chip`'s remove button, which the
285
+ "Chargement des données…" (`material.table.loading`), its scroll region,
286
+ "Scrollable table" / "Tableau défilant"
287
+ (`material.table.labels.scroll-region`, or the `scroll-label` prop), a `Chip`'s remove button, which the
285
288
  `Autocomplete` chips use ("Remove {label}", `material.chip.dismiss-label`),
286
289
  the `LocationPreview` trigger (`material.map.preview` / `preview-named`),
287
290
  `DurationField`'s inputs, "Hours" and "Minutes"
@@ -402,10 +405,27 @@ and description and against the English ones, ignoring accents: in French,
402
405
 
403
406
  Some strings are still English in every language, among them the filter's
404
407
  validation messages, the camera's error messages, the editor's callout type
405
- names and default block content, and the names of the emoji picker's category
406
- tabs. File sizes (`2.0 KB`) and the filter chips' dates are not formatted for
408
+ names and default block content. File sizes (`2.0 KB`) and the filter chips' dates are not formatted for
407
409
  the provider's language.
408
410
 
411
+ The emoji picker's category tabs are icon-only; their name, read by screen
412
+ readers and shown as a tooltip, comes from `material.emoji.groups.<group>`.
413
+ Both languages ship `all`, `recent`, `general`, `people_body`, `components`,
414
+ `animals_nature`, `food_drink`, `travel_places`, `activities`, `objects`,
415
+ `symbols` and `flags`:
416
+
417
+ ```ts
418
+ // English / French values of the shipped keys
419
+ material.emoji.groups.people_body; // "People & body" / "Personnes et corps"
420
+ material.emoji.groups.food_drink; // "Food & drink" / "Nourriture et boissons"
421
+ material.emoji.groups.flags; // "Flags" / "Drapeaux"
422
+ ```
423
+
424
+ A group your `EmojiItem`s name that is not in this list (`hand_signs`) reads
425
+ as its words in English (`Hand signs`). As for every `material.*` key above,
426
+ the components read their own bundle: your own translator sees a key you add,
427
+ the tab does not.
428
+
409
429
  That override is visible only through **your** translator. The components
410
430
  translate through the package's own bundle, so defining
411
431
  `material.command.title` in your resources changes what
@@ -275,7 +275,7 @@ export default defineNuxtPlugin((nuxtApp) => {
275
275
  | `getActivePinia()` was called but there was no active Pinia | a widget rendered on the server before Pinia was installed | `app.use(createPinia())` in the server entry too; in Nuxt the module does it |
276
276
  | Text flips language right after load | no `initial-language` | Pass the request's language ([i18n](i18n.md#server-rendering)) |
277
277
  | Hydration mismatch around an icon | an `<svg><title>` in your markup | `role="img"` + `aria-label` |
278
- | Hydration mismatch on a phone around a `ResponsiveButton`, the app shell or a `DataTable`, or on a dark screen around a code block, 0.21.0 or earlier | the server answered "desktop, light", the phone's first render its own screen | upgrade ([troubleshooting](../troubleshooting.md#hydration-completed-but-contains-mismatches)) |
278
+ | Hydration mismatch on a phone around the app shell or a `DataTable`, or on a dark screen around a code block, 0.21.0 or earlier | the server answered "desktop, light", the phone's first render its own screen | upgrade ([troubleshooting](../troubleshooting.md#hydration-completed-but-contains-mismatches)) |
279
279
  | Hydration mismatch on a page with a `TimeField`, 0.21.0 or earlier | the server rendered the phone (dialog) trigger, a desktop client the popover one | upgrade ([troubleshooting](../troubleshooting.md#hydration-completed-but-contains-mismatches)) |
280
280
 
281
281
  More entries, with the full message and fix, are in