@nxgt/material-vue 0.26.1 → 0.27.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 (156) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/README.md +8 -5
  3. package/dist/Button-DEUkxkrJ.js +728 -0
  4. package/dist/{DropdownMenuTrigger-BHAhXGUO.js → DropdownMenuTrigger-BsuyNVzV.js} +48 -41
  5. package/dist/HoverCardTrigger-TDll55LP.js +510 -0
  6. package/dist/{camera-DvKuqT4L.js → camera-UJXnnFeb.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-BZ1XAK37.js} +19198 -18568
  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-BFGca0h6.js} +1 -1
  21. package/dist/{i18n-provider-CaaZCmaR.js → i18n-provider-DBusa725.js} +1 -1
  22. package/dist/i18n.js +4 -4
  23. package/dist/icon-button-jHB4MhQ5.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/toggle/Toggle.vue.d.ts +1 -1
  89. package/dist/lib/components/ui/primitives/toggle/index.d.ts +1 -1
  90. package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-box.vue.d.ts +2 -0
  91. package/dist/lib/components/ui/selection/select/SelectContent.vue.d.ts +2 -2
  92. package/dist/lib/components/ui/selection/select-chip-field/select-chip-field.vue.d.ts +1 -0
  93. package/dist/lib/i18n/resources/en/en.d.ts +9 -0
  94. package/dist/lib/i18n/resources/en/material.json.d.ts +13 -4
  95. package/dist/lib/i18n/resources/fr/fr.d.ts +9 -0
  96. package/dist/lib/i18n/resources/fr/material.json.d.ts +13 -4
  97. package/dist/lib/i18n/resources/resources.d.ts +18 -0
  98. package/dist/lib/i18n/translator.d.ts +1 -1
  99. package/dist/lib/lib/field-name-attribute.d.ts +7 -0
  100. package/dist/lib/lib/focus-field.d.ts +17 -0
  101. package/dist/lib/lib/index.d.ts +1 -0
  102. package/dist/lib.js +76 -74
  103. package/dist/main.js +13 -13
  104. package/dist/map.js +4 -4
  105. package/dist/material-vue.css +1 -1
  106. package/dist/{pdf-outline-sidebar-D3vq2qfS.js → pdf-outline-sidebar-ChjAXoUX.js} +2 -2
  107. package/dist/{pdf-page-BrJbyjQj.js → pdf-page-D4OTPcpF.js} +1 -1
  108. package/dist/{pdf-page-list-BCgYEszV.js → pdf-page-list-CFgKYLur.js} +2 -2
  109. package/dist/{pdf-reader-BAzevSJU.js → pdf-reader-B1hw81cG.js} +9 -9
  110. package/dist/{pdf-sidebar-DSh2Mend.js → pdf-sidebar-Do5HzrDq.js} +3 -3
  111. package/dist/{pdf-thumbnail-sidebar-D_EMcbKF.js → pdf-thumbnail-sidebar-CMge0C9-.js} +3 -3
  112. package/dist/{pdf-toolbar-BcL7HlfS.js → pdf-toolbar-BBio9baU.js} +5 -5
  113. package/dist/pdf.js +2 -2
  114. package/dist/{sortable-provider-DMpmM7Ke.js → sortable-provider-DMVbbmUU.js} +2 -2
  115. package/dist/{stored-language-Bxhsx4iB.js → stored-language-AJpwOMsA.js} +26 -8
  116. package/dist/{stx-player-MldK_ZAZ.js → stx-player-gtyStl7e.js} +2 -2
  117. package/dist/styles.css +10 -368
  118. package/dist/{translator-CLI_7Crg.js → translator-D2_swsMz.js} +1 -1
  119. package/dist/{use-heading-spy-VPuFj2Gt.js → use-heading-spy-BbEbAfPK.js} +19 -19
  120. package/dist/use-inert-others-CtGqZKR8.js +22 -0
  121. package/dist/{use-material-translation-CONqD9wJ.js → use-material-translation-BHtgyMz3.js} +3 -3
  122. package/docs/components/buttons-and-actions.md +71 -16
  123. package/docs/components/data-display.md +12 -1
  124. package/docs/components/data-table-filter-and-lists.md +85 -5
  125. package/docs/components/date-and-time.md +25 -1
  126. package/docs/components/feedback-and-status.md +86 -11
  127. package/docs/components/form-fields.md +13 -4
  128. package/docs/components/media-and-files.md +7 -0
  129. package/docs/components/navigation-and-layout.md +19 -1
  130. package/docs/components/overlays-and-menus.md +20 -4
  131. package/docs/components/primitives.md +16 -2
  132. package/docs/components/selection-and-choice.md +12 -1
  133. package/docs/getting-started.md +2 -2
  134. package/docs/guides/app-shell.md +102 -6
  135. package/docs/guides/forms.md +58 -5
  136. package/docs/guides/helpers.md +2 -1
  137. package/docs/guides/i18n.md +5 -2
  138. package/docs/guides/ssr.md +1 -1
  139. package/docs/guides/theming.md +134 -6
  140. package/docs/previews/data-display-description.webp +0 -0
  141. package/docs/previews/feedback-status-toast.webp +0 -0
  142. package/docs/roadmap.md +21 -0
  143. package/docs/troubleshooting.md +854 -12
  144. package/lib/components/base-content.css +139 -0
  145. package/lib/components/base.css +150 -0
  146. package/lib/components/components.css +2 -0
  147. package/lib/components/reduced-motion.css +56 -0
  148. package/lib/components/theme-tokens.css +116 -0
  149. package/lib/components/ui/actions/button-group/button-group.css +1 -1
  150. package/lib/components/ui/feedback/toast/toast.css +21 -0
  151. package/lib/components/ui/primitives/label/label.css +33 -0
  152. package/lib/styles.css +10 -368
  153. package/package.json +2 -2
  154. package/dist/Button-16J7vTpE.js +0 -665
  155. package/dist/HoverCardTrigger-C41002JS.js +0 -455
  156. package/dist/icon-button-CDdNsZuZ.js +0 -227
@@ -751,6 +751,13 @@ Playback uses the browser's `<video>` / `<audio>`: a format the browser cannot
751
751
  decode (H.264 MP4 in a Chromium build without proprietary codecs) shows the
752
752
  player's error state with a Retry button. `StxPlayer` mounts on the client only.
753
753
 
754
+ The speed and captions menus are radio groups: each entry is a
755
+ `menuitemradio` and the chosen one `aria-checked="true"` (captions: "Off" or
756
+ one track's `label`). `c` toggles the captions; turned on before a language
757
+ was chosen, they show the current track's first caption track, which the menu
758
+ then checks. In a test, pick one with
759
+ `getByRole('menuitemradio', { name: 'English' })`.
760
+
754
761
  **Storybook:** `Media & files/StxPlayer` · **React:** `StxPlayer` (same parts)
755
762
 
756
763
  ---
@@ -72,11 +72,14 @@ which work alone; `findActiveMenuItem`, `isMenuItemActive` and
72
72
  | Prop | Type | Default | Effect |
73
73
  | --- | --- | --- | --- |
74
74
  | `items` | `MenuItem[]` | — | `label` (already translated), `url`, `icon` (a Font Awesome name), `footer` |
75
+ | `linkComponent` | `Component` | `RouterLink` | The link each menu entry renders, given `href`; pass it as `link-component` (see the [app shell guide](../guides/app-shell.md)) |
75
76
  | `avatarSrc` | `string` | — | avatar image in the header |
76
77
  | `#brand` | slot | — | sidebar header |
77
78
  | `#header-actions`, `#avatar` | slots | — | header, before the avatar; `#avatar` replaces it |
78
79
  | `title`, `subtitle`, `#actions` | `string`, `string`, slot | — | `ActivityContent`'s header card |
79
80
  | `#header` / `hideHeader` | slot / `boolean` | — / `false` | `ActivityContent`: replace or drop the header card |
81
+ | `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)) |
82
+ | `titleAs` | `'h1'` … `'h6'` \| `'div'` | `'h1'` | `ActivityContent`: the title's heading level |
80
83
 
81
84
  The active entry is the longest match across the whole menu, segment-aware.
82
85
  `AppBreadcrumb` reads `meta.breadcrumb` (a string, or a function of the
@@ -86,6 +89,11 @@ to the router's root, so a history base is kept; a crumb with params links to
86
89
  its path with them filled in) and the current page is text with
87
90
  `aria-current="page"`.
88
91
 
92
+ `AppSidebar` gives each entry its label as a tooltip **only while it is
93
+ collapsed**: expanded (and in the phone menu sheet) the label is on screen
94
+ and no tooltip opens. A `SidebarMenuItem` of your own shows the `tooltip` you
95
+ pass, collapsed or expanded.
96
+
89
97
  **Storybook:** `Navigation & layout/AppShell` · **React:** `ActivityLayout`, `ActivityContent`, `AppSidebar`, `AppHeader`, `AppBreadcrumb`
90
98
 
91
99
  ---
@@ -135,9 +143,19 @@ const expanded = ref(false);
135
143
  ```
136
144
 
137
145
  Parts: `Sidebar`, `SidebarHeader`, `SidebarContent`, `SidebarFooter`,
138
- `SidebarMenuLabel` (`#actions` slot), `SidebarMenuItem` (a button: `#icon`
146
+ `SidebarMenuLabel` (`#actions` slot), `SidebarMenuItem` (a button, or a link with `href` and `link-component`: `#icon`
139
147
  slot, `active`, `tooltip`, `@click`).
140
148
 
149
+ A `SidebarMenuItem` stays a `<button>` without `href`. With `href` it renders a
150
+ real link, a plain `<a>` unless you pass `link-component` (given the `href`):
151
+
152
+ ```vue
153
+ <SidebarMenuItem href="/inbox" :link-component="MyLink">
154
+ <template #icon><Icon name="inbox" /></template>
155
+ Inbox
156
+ </SidebarMenuItem>
157
+ ```
158
+
141
159
  | Prop | Type | Default | Effect |
142
160
  | --- | --- | --- | --- |
143
161
  | `v-model:expanded` | `boolean` | `false` | labels beside the icons; collapsed shows icons with tooltips |
@@ -9,6 +9,19 @@ uncontrolled. Dialogs that exist to confirm something live on
9
9
  [Feedback & status](feedback-and-status.md). Everything is imported from
10
10
  `@nxgt/material-vue/components`.
11
11
 
12
+ **The page behind a modal layer is inert.** While a `Dialog`, `Sheet`,
13
+ `Drawer`, `AlertDialog` or `CommandDialog` is open — and a `Select` list, a
14
+ `ContextMenu`, or a `DropdownMenu` or `Popover` given `modal` — everything else
15
+ on the page is `inert` as well as `aria-hidden`: its controls leave the tab
16
+ order and the accessibility tree, so a keyboard or screen-reader user stays in
17
+ the layer and axe's `aria-hidden-focus` passes. Live regions stay reachable:
18
+ a toast shown over a dialog is announced, and its action can be used. Layers
19
+ stack: a `Select` open in a `Dialog` makes the dialog inert too, until it
20
+ closes. Focus left on the page when the layer opens (an `@open-auto-focus`
21
+ that prevents the default and focuses nothing) goes to the layer's content.
22
+ Non-modal layers (`:modal="false"`, and `DropdownMenu` and `Popover` by
23
+ default) leave the page as it is.
24
+
12
25
  | | Component | What it does |
13
26
  | --- | --- | --- |
14
27
  | <img src="../previews/overlays-menus-dialog.webp" width="96" alt=""> | [Dialog](#dialog) | Modal dialog with a close button |
@@ -74,7 +87,7 @@ overlay, for a dialog taller than the screen).
74
87
  | Prop | Type | Default | Effect |
75
88
  | --- | --- | --- | --- |
76
89
  | `default-open` / `v-model:open` (`Dialog`) | `boolean` | `false` | open state, uncontrolled / controlled |
77
- | `modal` (`Dialog`) | `boolean` | `true` | `false` drops the backdrop and lets the page stay interactive |
90
+ | `modal` (`Dialog`) | `boolean` | `true` | `false` drops the backdrop and lets the page stay interactive; `true` makes the page behind `inert` while it is open |
78
91
  | `as-child` (`DialogTrigger`) | `boolean` | `false` | the trigger is its child — pass it with a `Button`, or the trigger renders its own `<button>` around it |
79
92
  | `show-close-button` (`DialogContent`) | `boolean` | `true` | the top-right X, named "Close" (`material.buttons.close`, translated) |
80
93
  | `show-close-button` (`DialogFooter`) | `boolean` | `false` | adds an outlined "Close" button (`material.buttons.close`, translated) |
@@ -86,7 +99,10 @@ Unlike React, `DialogContent` renders the X by itself (turn it off with
86
99
  On open, `DialogContent` moves focus into the dialog: to its first control
87
100
  (the built-in X when there is nothing before it), or to the content itself
88
101
  when it has none. An `@open-auto-focus` that calls `event.preventDefault()`
89
- keeps focus where it was. `SheetContent`, `DrawerContent` and
102
+ keeps focus off that control: a modal dialog then focuses its content, as the
103
+ page behind it is inert, and a non-modal one leaves focus where it was
104
+ ([details](../troubleshooting.md#focus-moves-into-a-dialog-although-open-auto-focus-prevented-it)).
105
+ `SheetContent`, `DrawerContent` and
90
106
  `AlertDialogContent` do the same; an alert dialog focuses its
91
107
  `AlertDialogCancel`.
92
108
 
@@ -371,7 +387,7 @@ shows.
371
387
 
372
388
  ![DropdownMenu](../previews/overlays-menus-dropdownmenu.webp)
373
389
 
374
- Menu with checkbox and radio items, submenus, shortcuts and a destructive item.
390
+ Menu with checkbox and radio items, submenus, shortcuts and a destructive item. It is non-modal by default: Reka'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, an outside click or Tab. Pass `modal` for a modal one: the page behind it is then `inert` too, so its controls leave the tab order and axe passes.
375
391
 
376
392
  ```vue
377
393
  <script setup lang="ts">
@@ -421,7 +437,7 @@ Parts: `DropdownMenu`, `DropdownMenuTrigger`, `DropdownMenuContent`,
421
437
  | `variant` (`DropdownMenuItem`) | `'default' \| 'error'` | `'default'` | error colour for a destructive item; `'destructive'` is a deprecated alias of `'error'`; both render `data-variant="error"`, so style or test with `[data-variant=error]` |
422
438
  | `inset` (item, label, sub-trigger) | `boolean` | — | indents to line up with checkbox and radio items |
423
439
  | `@select` (`DropdownMenuItem`) | `(event: Event) => void` | — | the item's action; `event.preventDefault()` keeps the menu open |
424
- | `default-open` / `v-model:open`, `modal` (`DropdownMenu`) | `boolean` | —, `true` | open state; a modal menu blocks the page behind it |
440
+ | `default-open` / `v-model:open`, `modal` (`DropdownMenu`) | `boolean` | —, `false` | open state; `modal` brings back the scroll lock, swallowed outside clicks and an `aria-hidden`, `inert` page behind the menu |
425
441
  | `to` (`DropdownMenuContent`) | `string \| HTMLElement` | `'body'` | where the menu is teleported; for a menu opened in fullscreen, pass the fullscreen element, read when the menu opens (see [troubleshooting](../troubleshooting.md#a-dropdownmenu-opened-in-fullscreen-does-not-show)) |
426
442
 
427
443
  Vue's `DropdownMenuItem` has no `active` prop. CSS or a test that selected
@@ -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
  ---
@@ -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"
@@ -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