@nxgt/material-vue 0.41.0 → 0.43.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 (161) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +20 -6
  3. package/dist/{Button-BG3oDkrl.js → Button-C3K6dhqd.js} +4 -4
  4. package/dist/{HoverCardTrigger-Dh1ar_W4.js → HoverCardTrigger-Bga4YjEr.js} +69 -62
  5. package/dist/backgrounds/backgrounds.css +8 -0
  6. package/dist/backgrounds/css/base.css +169 -0
  7. package/dist/backgrounds/css/gradient.css +53 -0
  8. package/dist/backgrounds/css/grain.css +19 -0
  9. package/dist/backgrounds/css/motion.css +30 -0
  10. package/dist/backgrounds.js +111 -0
  11. package/dist/{camera-DV9jcl_3.js → camera-BbF9_-ys.js} +4 -4
  12. package/dist/components/base.css +5 -2
  13. package/dist/components/components.css +5 -0
  14. package/dist/components/theme-tokens.css +21 -0
  15. package/dist/{components-C1JYYoQ4.js → components-yCdbry6Q.js} +13880 -13101
  16. package/dist/components.js +8 -8
  17. package/dist/composables.js +3 -3
  18. package/dist/dnd.js +1 -1
  19. package/dist/{form-scope-CeD0v_mw.js → form-scope-CkWJrBCu.js} +1 -1
  20. package/dist/{i18n-provider-D5XyLQ3P.js → i18n-provider-tL27-a8y.js} +1 -1
  21. package/dist/i18n.js +4 -4
  22. package/dist/{icon-button-DXrmfs6e.js → icon-button-MyFFJpXA.js} +4 -4
  23. package/dist/lib/backgrounds/background/background.vue.d.ts +22 -0
  24. package/dist/lib/backgrounds/background/index.d.ts +2 -0
  25. package/dist/lib/backgrounds/background/partials/background-layers.vue.d.ts +9 -0
  26. package/dist/lib/backgrounds/background/partials/scene-svg.d.ts +10 -0
  27. package/dist/lib/backgrounds/background/types.d.ts +24 -0
  28. package/dist/lib/backgrounds/index.d.ts +13 -0
  29. package/dist/lib/components/ui/actions/button/button-tones.d.ts +3 -3
  30. package/dist/lib/components/ui/actions/responsive-button/responsive-button.vue.d.ts +3 -3
  31. package/dist/lib/components/ui/collections/data-table/data-table-context.d.ts +4 -0
  32. package/dist/lib/components/ui/collections/data-table/data-table.vue.d.ts +8 -0
  33. package/dist/lib/components/ui/collections/data-table/partials/data-table-body.vue.d.ts +8 -0
  34. package/dist/lib/components/ui/collections/data-table/partials/data-table-empty.vue.d.ts +5 -1
  35. package/dist/lib/components/ui/collections/data-table/partials/data-table-toolbar.d.ts +16 -3
  36. package/dist/lib/components/ui/collections/data-table/select-checkbox.d.ts +36 -0
  37. package/dist/lib/components/ui/collections/data-table/types.d.ts +15 -0
  38. package/dist/lib/components/ui/collections/data-table/use-data-table.d.ts +2 -0
  39. package/dist/lib/components/ui/feedback/circular-progress/circular-progress.vue.d.ts +1 -1
  40. package/dist/lib/components/ui/feedback/empty-state/empty-state.vue.d.ts +21 -8
  41. package/dist/lib/components/ui/feedback/empty-state/index.d.ts +1 -1
  42. package/dist/lib/components/ui/feedback/error-state/error-state.vue.d.ts +54 -0
  43. package/dist/lib/components/ui/feedback/error-state/index.d.ts +3 -0
  44. package/dist/lib/components/ui/feedback/error-state/partials/error-state-details.vue.d.ts +7 -0
  45. package/dist/lib/components/ui/feedback/index.d.ts +1 -0
  46. package/dist/lib/components/ui/feedback/state-frame/state-frame.vue.d.ts +36 -0
  47. package/dist/lib/components/ui/feedback/state-frame/state-icon.vue.d.ts +7 -0
  48. package/dist/lib/components/ui/feedback/state-frame/state-illustration.vue.d.ts +8 -0
  49. package/dist/lib/components/ui/feedback/state-frame/state-tones.d.ts +11 -0
  50. package/dist/lib/components/ui/fields/contacts/consts.d.ts +1 -1
  51. package/dist/lib/components/ui/layout/account-menu/account-menu-avatar.vue.d.ts +9 -0
  52. package/dist/lib/components/ui/layout/account-menu/account-menu.vue.d.ts +38 -0
  53. package/dist/lib/components/ui/layout/account-menu/index.d.ts +3 -0
  54. package/dist/lib/components/ui/layout/account-menu/types.d.ts +10 -0
  55. package/dist/lib/components/ui/layout/app-shell/activity-layout.vue.d.ts +21 -5
  56. package/dist/lib/components/ui/layout/app-shell/app-brand-logo.vue.d.ts +20 -0
  57. package/dist/lib/components/ui/layout/app-shell/app-brand.vue.d.ts +30 -0
  58. package/dist/lib/components/ui/layout/app-shell/app-header.vue.d.ts +13 -5
  59. package/dist/lib/components/ui/layout/app-shell/app-sidebar.vue.d.ts +12 -2
  60. package/dist/lib/components/ui/layout/app-shell/index.d.ts +1 -1
  61. package/dist/lib/components/ui/layout/app-shell/types.d.ts +10 -0
  62. package/dist/lib/components/ui/layout/app-shell/use-truncated-title.d.ts +10 -0
  63. package/dist/lib/components/ui/layout/background/background.vue.d.ts +18 -3
  64. package/dist/lib/components/ui/layout/doc/partials/doc-empty.vue.d.ts +7 -4
  65. package/dist/lib/components/ui/layout/index.d.ts +1 -0
  66. package/dist/lib/components/ui/media/stx-player/stx-player-viewport.vue.d.ts +2 -2
  67. package/dist/lib/components/ui/overlays/command/CommandSeparator.vue.d.ts +3 -1
  68. package/dist/lib/components/ui/overlays/dialog/in-dialog.d.ts +1 -1
  69. package/dist/lib/components/ui/overlays/dialog/use-escape-forward.d.ts +13 -0
  70. package/dist/lib/components/ui/overlays/dialog/use-modal-content.d.ts +2 -0
  71. package/dist/lib/components/ui/primitives/nonce-provider/nonce-provider.vue.d.ts +4 -1
  72. package/dist/lib/components/ui/selection/autocomplete/autocomplete-keyboard.d.ts +22 -4
  73. package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-box.vue.d.ts +6 -2
  74. package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-list.vue.d.ts +7 -0
  75. package/dist/lib/components/ui/selection/autocomplete/use-autocomplete-active.d.ts +17 -0
  76. package/dist/lib/components/ui/selection/autocomplete/use-autocomplete-keydown.d.ts +21 -0
  77. package/dist/lib/components/ui/selection/autocomplete/use-autocomplete-open.d.ts +3 -3
  78. package/dist/lib/components/ui/selection/autocomplete/use-autocomplete.d.ts +2 -1
  79. package/dist/lib/components/ui/selection/autocomplete/use-drawer-room.d.ts +11 -0
  80. package/dist/lib/components/ui/selection/autocomplete/use-query-change.d.ts +8 -0
  81. package/dist/lib/components/ui/widgets/charts/chart-container.vue.d.ts +2 -2
  82. package/dist/lib/components/ui/widgets/kanban/partials/kanban-empty.vue.d.ts +1 -0
  83. package/dist/lib/composables/use-background-activity.d.ts +8 -0
  84. package/dist/lib/i18n/resources/en/en.d.ts +28 -0
  85. package/dist/lib/i18n/resources/en/material.json.d.ts +29 -1
  86. package/dist/lib/i18n/resources/fr/fr.d.ts +28 -0
  87. package/dist/lib/i18n/resources/fr/material.json.d.ts +29 -1
  88. package/dist/lib/i18n/resources/resources.d.ts +56 -0
  89. package/dist/lib/i18n/translator.d.ts +1 -1
  90. package/dist/lib/lib/index.d.ts +1 -0
  91. package/dist/lib/nuxt/component-names.d.ts +1 -1
  92. package/dist/lib/nuxt/module.d.ts +2 -11
  93. package/dist/lib/nuxt/pdf-fonts.d.ts +1 -1
  94. package/dist/lib/nuxt/plugin-options.d.ts +58 -0
  95. package/dist/lib/vite/index.d.ts +4 -8
  96. package/dist/lib.js +52 -51
  97. package/dist/main.js +17 -17
  98. package/dist/map.js +4 -4
  99. package/dist/material-vue.css +1 -1
  100. package/dist/{pdf-outline-sidebar-CgmJjhUz.js → pdf-outline-sidebar-Kqsfd_cO.js} +2 -2
  101. package/dist/{pdf-page-BjQNWV72.js → pdf-page-Dx3jwxu6.js} +1 -1
  102. package/dist/{pdf-page-list-nG0l4SjH.js → pdf-page-list-BsQmEHaY.js} +2 -2
  103. package/dist/{pdf-reader-Dekd4Ymw.js → pdf-reader-CJo8SQVl.js} +15 -15
  104. package/dist/{pdf-sidebar-gDj63dRH.js → pdf-sidebar-CcCUATl-.js} +3 -3
  105. package/dist/{pdf-thumbnail-sidebar-C5ikii1Z.js → pdf-thumbnail-sidebar-BRxNRGfG.js} +4 -4
  106. package/dist/{pdf-toolbar-DmmrSlCq.js → pdf-toolbar-B0ckYBJ2.js} +4 -4
  107. package/dist/pdf.js +2 -2
  108. package/dist/{print-format.utils-CiUiQE-i.js → print-format.utils-51joHpFb.js} +2 -2
  109. package/dist/{sortable-provider-lK0srHJF.js → sortable-provider-BEx1B0Ky.js} +2 -2
  110. package/dist/{stored-language-dM2e5qaS.js → stored-language-DVP9KzRu.js} +58 -2
  111. package/dist/{stx-player-C2VQCXFc.js → stx-player-KPp9c7vS.js} +5 -5
  112. package/dist/styles.css +4 -0
  113. package/dist/{translator-DXL5RDRg.js → translator-Bm7i7dv-.js} +1 -1
  114. package/dist/use-background-activity-DdTBbNpX.js +17 -0
  115. package/dist/{use-heading-spy-Btwc0yad.js → use-heading-spy-jn8f2ioE.js} +13 -13
  116. package/dist/{use-material-translation-Du3Wvsk1.js → use-material-translation-CfFZPSe0.js} +3 -3
  117. package/dist/vite.js +7 -2
  118. package/docs/README.md +3 -0
  119. package/docs/components/README.md +2 -1
  120. package/docs/components/buttons-and-actions.md +2 -0
  121. package/docs/components/data-table-filter-and-lists.md +59 -8
  122. package/docs/components/feedback-and-status.md +79 -8
  123. package/docs/components/media-and-files.md +2 -0
  124. package/docs/components/navigation-and-layout.md +68 -5
  125. package/docs/components/overlays-and-menus.md +20 -3
  126. package/docs/components/primitives.md +5 -3
  127. package/docs/components/rich-widgets.md +1 -1
  128. package/docs/components/selection-and-choice.md +14 -4
  129. package/docs/getting-started.md +22 -3
  130. package/docs/guides/app-shell.md +99 -19
  131. package/docs/guides/backgrounds.md +143 -0
  132. package/docs/guides/csp.md +48 -6
  133. package/docs/guides/helpers.md +1 -0
  134. package/docs/guides/i18n.md +52 -2
  135. package/docs/guides/migrate-to-vite-plugin.md +259 -0
  136. package/docs/guides/pdf.md +11 -4
  137. package/docs/guides/ssr.md +1 -1
  138. package/docs/guides/theming.md +52 -4
  139. package/docs/guides/vite-plugin.md +194 -0
  140. package/docs/previews/backgrounds-matrix.webp +0 -0
  141. package/docs/previews/feedback-status-emptystate.webp +0 -0
  142. package/docs/previews/feedback-status-errorstate.webp +0 -0
  143. package/docs/previews/navigation-layout-accountmenu.webp +0 -0
  144. package/docs/roadmap.md +69 -32
  145. package/docs/troubleshooting.md +251 -7
  146. package/lib/backgrounds/backgrounds.css +8 -0
  147. package/lib/backgrounds/css/base.css +169 -0
  148. package/lib/backgrounds/css/gradient.css +53 -0
  149. package/lib/backgrounds/css/grain.css +19 -0
  150. package/lib/backgrounds/css/motion.css +30 -0
  151. package/lib/components/base.css +5 -2
  152. package/lib/components/components.css +5 -0
  153. package/lib/components/theme-tokens.css +21 -0
  154. package/lib/nuxt/component-names.ts +2 -0
  155. package/lib/nuxt/module.ts +29 -15
  156. package/lib/nuxt/pdf-fonts.ts +1 -1
  157. package/lib/nuxt/plugin-options.ts +92 -0
  158. package/lib/nuxt/runtime/inter.css +6 -0
  159. package/lib/styles.css +4 -0
  160. package/lib/vite/index.ts +19 -2
  161. package/package.json +24 -2
@@ -11,7 +11,8 @@ centre. Everything is imported from `@nxgt/material-vue/components`.
11
11
  | <img src="../previews/feedback-status-alertdialog.webp" width="96" alt=""> | [AlertDialog](#alertdialog) | Parts for a blocking confirmation dialog |
12
12
  | <img src="../previews/feedback-status-confirmdialog.webp" width="96" alt=""> | [ConfirmDialog](#confirmdialog) | Confirm / cancel dialog, `ConfirmationDialog`, `CustomAlertDialog` |
13
13
  | <img src="../previews/feedback-status-customdialog.webp" width="96" alt=""> | [CustomDialog](#customdialog) | Ready-made dialog with trigger, icon, title, body and footer |
14
- | <img src="../previews/feedback-status-emptystate.webp" width="96" alt=""> | [EmptyState](#emptystate) | Icon, title, description and action, with presets |
14
+ | <img src="../previews/feedback-status-emptystate.webp" width="96" alt=""> | [EmptyState](#emptystate) | Illustration, title, description and action, by kind (empty, search, not-found, unauthorized) |
15
+ | <img src="../previews/feedback-status-errorstate.webp" width="96" alt=""> | [ErrorState](#errorstate) | Failure block with Retry and technical details |
15
16
  | <img src="../previews/feedback-status-progress.webp" width="96" alt=""> | [Progress](#progress) | Linear bar, circular ring and labelled progress field |
16
17
  | <img src="../previews/feedback-status-scrollprogress.webp" width="96" alt=""> | [ScrollProgress](#scrollprogress) | Hairline tracking the window or a scrollable element |
17
18
  | <img src="../previews/feedback-status-spinner.webp" width="96" alt=""> | [Spinner](#spinner) | Loading spinner in several styles and colours |
@@ -412,8 +413,7 @@ icon for that reason now needs an element such as a `Button` or `IconButton`.
412
413
 
413
414
  ![EmptyState](../previews/feedback-status-emptystate.webp)
414
415
 
415
- Icon in a tinted well, title, description and action; not-found,
416
- unauthorized, generic, empty, offline, search and maintenance presets.
416
+ A calm absence: an illustration (an icon when `size="compact"`), title, description and action, for the `empty`, `search`, `not-found` and `unauthorized` kinds. Text is `text-foreground` and reads at 7:1. For something that failed, use an error state instead.
417
417
 
418
418
  ```vue
419
419
  <script setup lang="ts">
@@ -423,7 +423,7 @@ function clearFilters() {}
423
423
  </script>
424
424
 
425
425
  <template>
426
- <EmptyState variant="search">
426
+ <EmptyState kind="search">
427
427
  <template #action>
428
428
  <Button variant="outlined" @click="clearFilters">Clear filters</Button>
429
429
  </template>
@@ -433,17 +433,88 @@ function clearFilters() {}
433
433
 
434
434
  | Prop | Type | Default | Effect |
435
435
  | --- | --- | --- | --- |
436
- | `variant` | `'not-found' \| 'unauthorized' \| 'generic' \| 'empty' \| 'offline' \| 'search' \| 'maintenance'` | `'generic'` | preset icon, well colour, title and description |
437
- | `#icon` slot | — | the preset's | replaces the icon |
438
- | `title` / `description` | `string` (or `#title` / `#description`) | the preset's (translated) | replace the preset text; see below |
436
+ | `kind` | `'empty' \| 'search' \| 'not-found' \| 'unauthorized'` | `'generic'` (legacy, until 1.0) | illustration, icon, title and description |
437
+ | `size` | `'default' \| 'compact'` | `'default'` | `compact` swaps the illustration for a small icon and tightens the spacing |
438
+ | `variant` | the four kinds, plus `'generic' \| 'offline' \| 'maintenance'` | — | **deprecated**, removed in 1.0: use `kind`; the error values render without `role="alert"` |
439
+ | `#illustration` slot | — | the kind's | replaces the illustration |
440
+ | `#icon` slot | — | the kind's | replaces the icon |
441
+ | `title` / `description` | `string` (or `#title` / `#description`) | the kind's (translated) | replace the kind's text; see below |
439
442
  | `#action` slot | — | — | shown under the text |
440
443
 
441
- The preset's description is the companion of its title: with your own `title` and no `description`, no description is rendered. Pass `description` to keep a line under your own title. A `#title` slot counts as your own title; a `#description` slot always renders.
444
+ The default description is the companion of its title: with your own `title` and no `description`, no description is rendered. Pass `description` to keep a line under your own title. A `#title` slot counts as your own title; a `#description` slot always renders.
445
+
446
+ `EmptyState` cannot hide its media yet, unlike React's `illustration={false}` ([#711](https://github.com/softistx/nxgt-material/issues/711)); `ErrorState` can, with `:illustration="false"`.
447
+
448
+ `<EmptyState />` with no `kind` or `variant` still renders the `generic` (error) copy until 1.0; pass `kind`.
449
+
450
+ ### Migrating from `variant`
451
+
452
+ `variant` still works and is removed in 1.0. Until then:
453
+
454
+ | Before | After |
455
+ | --- | --- |
456
+ | `variant="empty"`, `"search"`, `"not-found"`, `"unauthorized"` | `kind="…"` with the same value |
457
+ | `variant="generic"`, `"offline"`, `"maintenance"` | `<ErrorState kind="…">`: it is a `role="alert"` block with Retry and technical details; an `EmptyState` error `variant` has no `role` |
458
+ | no `variant` | pass `kind`: the default stays `generic` until 1.0, then becomes `empty` |
459
+ | class overrides to shrink it (`min-h-0`, smaller padding) | `size="compact"`: a small icon, no minimum height |
460
+ | `material.empty-state.generic\|offline\|maintenance.*` overrides | the same text under `material.error-state.<kind>.*` |
461
+
462
+ ```vue
463
+ <!-- before -->
464
+ <EmptyState variant="offline" />
465
+ <EmptyState variant="search" class="min-h-0 py-6" />
466
+
467
+ <!-- after -->
468
+ <ErrorState kind="offline" @retry="refetch" />
469
+ <EmptyState kind="search" size="compact" />
470
+ ```
471
+
472
+ In 1.0, `variant` goes, the error kinds leave `EmptyState`, and `kind` defaults to `empty`. The DOM keeps `data-slot="empty-state"` and `data-variant`, and adds `data-kind` and `data-size`.
442
473
 
443
474
  **Storybook:** `Feedback & status/EmptyState` · **React:** `EmptyState`
444
475
 
445
476
  ---
446
477
 
478
+ ## ErrorState
479
+
480
+ ![ErrorState](../previews/feedback-status-errorstate.webp)
481
+
482
+ Failure block with `role="alert"`, an illustration, an optional outlined Retry
483
+ and collapsed technical details (name, message and causes, never the stack).
484
+ Text reads at 7:1 in both themes.
485
+
486
+ ```vue
487
+ <script setup lang="ts">
488
+ import { ErrorState } from '@nxgt/material-vue/components';
489
+ </script>
490
+
491
+ <template>
492
+ <ErrorState kind="offline" :error="error" @retry="refetch" />
493
+ </template>
494
+ ```
495
+
496
+ | Prop | Type | Default | Effect |
497
+ | --- | --- | --- | --- |
498
+ | `kind` | `'generic' \| 'offline' \| 'maintenance'` | `'generic'` | illustration, title and description (`material.error-state.<kind>.*`) |
499
+ | `size` | `'default' \| 'compact'` | `'default'` | compact swaps the illustration for an icon |
500
+ | `title` / `description` | `string` | the kind's (translated) | replace the text; a custom `title` drops the default description |
501
+ | `illustration` | `boolean` | `true` | `:illustration="false"` hides the media |
502
+ | `#illustration` / `#icon` slots | — | the built-in illustration | replace the media |
503
+ | `#details` slot | — | the collapsed technical details | replaces them; React has no equivalent yet ([#712](https://github.com/softistx/nxgt-material/issues/712)) |
504
+ | `error` | `unknown` | none | the thrown value; its name, message and `cause` chain go in the details |
505
+ | `showDetails` | `boolean` | `true` when `error` is set | `false` hides the details |
506
+ | `@retry` | `() => unknown` | none | shows an outlined **Retry** button; a returned promise keeps it loading until it settles |
507
+ | `retrying` / `retryLabel` | `boolean` / `string` | derived / `Retry` | force the loading state; rename the button |
508
+ | `action` | the `#action` slot | none | extra actions after Retry |
509
+
510
+ The details are a native `<details>`: keyboard and server-render friendly.
511
+ Copy keys: `material.error-state.{generic,offline,maintenance}.{title,description}`,
512
+ `error-state.retry`, `error-state.details` and `error-state.details-name|message|cause`.
513
+
514
+ **Storybook:** `Feedback & status/ErrorState` · **React:** `ErrorState`
515
+
516
+ ---
517
+
447
518
  ## Progress
448
519
 
449
520
  ![Progress](../previews/feedback-status-progress.webp)
@@ -624,6 +624,8 @@ Parts: `PdfReader` (`PdfViewer` is a deprecated alias), `PdfToolbar`, `PdfSideba
624
624
  `PdfThumbnailSidebar`, `PdfOutlineSidebar`, `PdfPageList`, and the
625
625
  `usePdfReader` composable for building your own layout.
626
626
 
627
+ A failed load shows the reader's own error; the React reader adds a Retry button to it, which is React-only for now ([#702](https://github.com/softistx/nxgt-material/issues/702)).
628
+
627
629
  | Prop | Type | Default | Effect |
628
630
  | --- | --- | --- | --- |
629
631
  | `file` | `PdfSource \| null` | — (required) | a URL (`string` or `URL`), a `File` or `Blob`, an `ArrayBuffer` or `Uint8Array`, or `{ url, httpHeaders?, withCredentials? }`; `null` shows the empty state |
@@ -8,6 +8,7 @@ documentation-site shell. Everything is imported from
8
8
  | | Component | What it does |
9
9
  | --- | --- | --- |
10
10
  | <img src="../previews/navigation-layout-appshell.webp" width="96" alt=""> | [ActivityLayout](#activitylayout) | App shell: sidebar, header, breadcrumbs, titled page |
11
+ | <img src="../previews/navigation-layout-accountmenu.webp" width="96" alt=""> | [AccountMenu](#accountmenu) | The app bar's avatar menu: the account, the app's links, Sign out |
11
12
  | <img src="../previews/navigation-layout-authcard.webp" width="96" alt=""> | [AuthCard](#authcard) | Signed-out page frame: one `main`, a centred card, its title the `h1` |
12
13
  | <img src="../previews/navigation-layout-sidebar.webp" width="96" alt=""> | [Sidebar](#sidebar) | Collapsible navigation sidebar |
13
14
  | <img src="../previews/navigation-layout-topappbar.webp" width="96" alt=""> | [TopAppBar, BottomAppBar](#topappbar) | Top and bottom bars |
@@ -75,7 +76,9 @@ which work alone; `findActiveMenuItem`, `isMenuItemActive` and
75
76
  | `items` | `MenuItem[]` | — | `label` (already translated), `url`, `icon` (a Font Awesome name), `footer` |
76
77
  | `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)) |
77
78
  | `avatarSrc` | `string` | — | avatar image in the header |
78
- | `#brand` | slot | — | sidebar header |
79
+ | `logo` (or `#logo`), `name` | `AppLogo` (`string \| AppLogoImage`, `AppLogoImage` = `{ src, alt? }`), `string` | — | the app's logo alone in the collapsed rail, named by `name` (also its tooltip); logo and name side by side in the phone's menu sheet; `#logo` wins over `logo` |
80
+ | `#brand` | slot | — | replaces the sidebar header, in the rail and the menu sheet; wins over `logo` and `name` |
81
+ | `#header-title` | slot | — | the header's leading title (the context: an organisation's name), after the phone's menu button, truncated when long; when it is cut off, its full text is the `title` tooltip (#629) |
79
82
  | `#header-actions`, `#avatar` | slots | — | header, before the avatar; `#avatar` replaces it |
80
83
  | `title`, `subtitle`, `#actions` | `string`, `string`, slot | — | `ActivityContent`'s header card |
81
84
  | `#header` / `hideHeader` | slot / `boolean` | — / `false` | `ActivityContent`: replace or drop the header card |
@@ -100,6 +103,58 @@ entry, and an icon-only one (no label) falls back to its `aria-label`.
100
103
 
101
104
  ---
102
105
 
106
+ ## AccountMenu
107
+
108
+ ![AccountMenu](../previews/navigation-layout-accountmenu.webp)
109
+
110
+ The account menu of an app bar, for `ActivityLayout`'s `#avatar`: an avatar
111
+ button named "Account menu, Ada Lovelace" (the picture, else the initials on
112
+ `bg-primary`) that opens a menu aligned to its end, headed by the avatar, the
113
+ name and a second line, then the app's links and a destructive Sign out.
114
+
115
+ ```vue
116
+ <script setup lang="ts">
117
+ import { AccountMenu, ActivityLayout } from '@nxgt/material-vue/components';
118
+ </script>
119
+
120
+ <template>
121
+ <ActivityLayout :items="items">
122
+ <template #header-title>{{ school.name }}</template>
123
+ <template #avatar>
124
+ <AccountMenu
125
+ :name="user.name"
126
+ :secondary="user.email"
127
+ :src="user.picture"
128
+ :items="[{ label: t('menu.profile'), url: '/profile', icon: 'user' }]"
129
+ :signing-out="pending"
130
+ @sign-out="signOut"
131
+ />
132
+ </template>
133
+ <RouterView />
134
+ </ActivityLayout>
135
+ </template>
136
+ ```
137
+
138
+ | Prop / event | Type | Default | Effect |
139
+ | --- | --- | --- | --- |
140
+ | `name` | `string` | — | the trigger's name ("Account menu, {name}", `material.account-menu.trigger`), the menu's header, the initials |
141
+ | `secondary` | `string` | — | a second line under the name, the e-mail say |
142
+ | `src` | `string` | — | the avatar's picture; the initials until it loads, or without one |
143
+ | `items` | `AccountMenuItem[]` | `[]` | `{ label: string; url: string; icon?: IconName }`: links between the header and Sign out; `label` already translated, `icon` a [Font Awesome](https://fontawesome.com/icons) symbol id of the duotone sprite (see [Icon](primitives.md#icon)) |
144
+ | `linkComponent` | `Component` | `RouterLink` | the link each item renders, as in the app shell |
145
+ | `@sign-out` | event | — | Sign out was chosen; the menu stays open, so `signing-out` shows |
146
+ | `signOutLabel` | `string` | "Sign out" | `material.account-menu.sign-out` |
147
+ | `signingOut` | `boolean` | `false` | Sign out is busy (`aria-busy`, a spinner) and ignores a second selection |
148
+
149
+ Enter, Space or ArrowDown on the avatar opens the menu on its first item; the
150
+ arrows move, Escape closes it and gives the focus back to the avatar. The
151
+ items are real links (`<a href>`), named by their label. Its links need
152
+ `vue-router`, as the shell's do.
153
+
154
+ **Storybook:** `Navigation & layout/AccountMenu` · **React:** `AccountMenu` (prop `onSignOut`)
155
+
156
+ ---
157
+
103
158
  ## AuthCard
104
159
 
105
160
  ![AuthCard](../previews/navigation-layout-authcard.webp)
@@ -725,7 +780,7 @@ Parts: `Doc` (the shell), `DocPage` (header, content, see also and pager);
725
780
  the pieces it composes — `DocTopbar`, `DocSidebar`, `DocNav`, `DocArticle`,
726
781
  `DocToc`, `DocPager`, `DocSearch`, `DocSeeAlso`, `DocChildCards`,
727
782
  `DocProgress`, `DocSkipLink`, `DocHeader`, `DocCopyPage`, `DocCopyMarkdown`,
728
- `DocLink` — and `DocEmpty` for a 404.
783
+ `DocLink` — and `DocEmpty` for a 404. `DocEmpty` is an `EmptyState` (`kind` defaults to `not-found`; `size` is `default` or `compact`): see [EmptyState](./feedback-and-status.md#emptystate).
729
784
  `useDoc` / `provideDoc` / `useDocContext` expose the state for a shell of
730
785
  your own, built from those parts:
731
786
 
@@ -823,9 +878,17 @@ import { Background, Card } from '@nxgt/material-vue/components';
823
878
  </template>
824
879
  ```
825
880
 
826
- No props but `class`. It fills the viewport; pass a `class` such as
827
- `h-96 w-full` to use it as a band. `ActivityLayout` and `ThreadLayout` sit on
828
- it.
881
+ | Prop | Values | Default |
882
+ | --- | --- | --- |
883
+ | `preset` | `gradient` (the only preset on `/components`) | `gradient` |
884
+ | `tone` | `primary`, `secondary`, `tertiary`, `neutral`, `success`, `warning`, `error`, `info`, `mixed` (primary, secondary and tertiary), `custom` (reads `--nxgt-bg-custom-1..3`) | `primary` |
885
+ | `intensity` | `subtle`, `default`, `vivid` | `default` |
886
+ | `animated` | drifts the blobs; paused while the page is hidden or the background is off screen, and never under reduced motion | `false` |
887
+
888
+ It fills the viewport; pass a `class` such as `h-96 w-full` to use it as a band.
889
+ It is plain CSS from `styles.css`, with no inline `style`, and its colours lift
890
+ in dark. `ActivityLayout` and `ThreadLayout` sit on it. The superset with `grain`,
891
+ `seed`, `fill` and the other presets is `Background` from `/backgrounds`; see the [backgrounds guide](../guides/backgrounds.md).
829
892
 
830
893
  **Storybook:** `Navigation & layout/Background` · **React:** `Background`
831
894
 
@@ -39,7 +39,16 @@ default) leave the page as it is.
39
39
 
40
40
  ![Dialog](../previews/overlays-menus-dialog.webp)
41
41
 
42
- Modal dialog parts with a close button.
42
+ Modal dialog parts with a close button. An open `SelectField` list inside it has always taken the first Escape, as a layer of its own. So does an open `ComboboxField` or `Autocomplete` list: the second Escape closes the dialog. The same holds in a `Sheet`, a `Drawer`, an `AlertDialog` and a `Popover`.
43
+
44
+ Your own `@escape-key-down` on the content receives the Escape that only closes an open list with `event.defaultPrevented === true`; a handler that asks "close without saving?" returns early on it:
45
+
46
+ ```vue
47
+ <DialogContent @escape-key-down="(event) => {
48
+ if (event.defaultPrevented) return; // only an open list is closing
49
+ if (dirty) event.preventDefault(); // ask "close without saving?"
50
+ }">
51
+ ```
43
52
 
44
53
  ```vue
45
54
  <script setup lang="ts">
@@ -198,6 +207,8 @@ Vue.
198
207
 
199
208
  Swipeable bottom or side drawer (Reka UI's drawer).
200
209
 
210
+ An `Autocomplete` inside a Drawer keeps its list floating, stopped above the Drawer's footer, and a drag that starts on the list scrolls it instead of dragging the Drawer.
211
+
201
212
  ```vue
202
213
  <script setup lang="ts">
203
214
  import {
@@ -638,20 +649,26 @@ Parts: `Command`, `CommandDialog`, `CommandInput`, `CommandList`,
638
649
  `CommandEmpty`, `CommandGroup`, `CommandItem`, `CommandShortcut`,
639
650
  `CommandSeparator`.
640
651
 
652
+ `CommandEmpty` shows when the search matches nothing. With no children it
653
+ renders a compact `EmptyState` ("No results", `material.command.empty`); give it
654
+ children and they replace that, as before.
655
+
641
656
  | Prop | Type | Default | Effect |
642
657
  | --- | --- | --- | --- |
643
658
  | `value` (`CommandItem`) | `string` | — (required) | the item's value, emitted by `@select` and by `Command`'s `v-model` |
644
659
  | `checked` (`CommandItem`) | `boolean` | — | sets `aria-checked` and shows a checkbox indicator, for a multi-choice list; reka's `aria-selected` keeps meaning "selected value" (`multiple` is not used) |
645
660
  | `multiselectable` (`CommandList`) | `boolean` | — | sets `aria-multiselectable` on the list |
646
661
  | `@select` (`CommandItem`) | `(event) => void` | — | Enter or click on the item; `event.detail.value` is its `value` |
662
+ | `aria-label` (`CommandList`) | `string` | `"Suggestions"` (translated, `material.command.list`) | the list's accessible name; your own `aria-label` replaces the default |
663
+ | `decorative` (`CommandSeparator`) | `boolean` | `true` | the separator is purely visual (`role="none"`): a `role="separator"` is not allowed inside the listbox |
647
664
  | `heading` (`CommandGroup`) | `string` | — | the group's label |
648
665
  | `title` / `description` (`CommandDialog`) | `string` | `"Command Palette"` / `"Search for a command to run..."` (translated) | the dialog's title and description, visually hidden |
649
666
  | `v-model:open` (`CommandDialog`) | `boolean` | — | `CommandDialog` takes every `Dialog` prop |
650
667
 
651
668
  The filter matches the item's text, case- and accent-insensitive; there is no
652
669
  `shouldFilter` or `keywords` in Vue. `CommandDialog`'s default title and
653
- description are English, not translated — pass your own. It does not bind a
654
- keyboard shortcut: listen for ⌘K yourself (`useMagicKeys` from VueUse) and set
670
+ description follow `I18nProvider` (`material.command.title` / `description`).
671
+ It does not bind a keyboard shortcut: listen for ⌘K yourself (`useMagicKeys` from VueUse) and set
655
672
  `open`.
656
673
 
657
674
  **Storybook:** `Overlays & menus/Command` · **React:** `Command`, `CommandDialog` (same parts)
@@ -193,7 +193,7 @@ import { Typography } from '@nxgt/material-vue/components';
193
193
  | Prop | Type | Default | Effect |
194
194
  | --- | --- | --- | --- |
195
195
  | `variant` | `'huge-larger' \| 'huge-large' \| 'huge-medium' \| 'huge-small' \| 'headline-larger' \| 'headline-large' \| 'headline-medium' \| 'headline-small' \| 'title-large' \| 'title-medium' \| 'title-small' \| 'body-medium' \| 'body-small' \| 'caption' \| 'normal'` | `'normal'` | size and weight; `caption` is also muted |
196
- | `tone` | `'default' \| 'muted' \| 'primary' \| 'error'` | `'default'` | text colour: `muted` is `text-muted-foreground`, `primary` is `text-primary`, `error` is `text-error-text`; `default` inherits (a `class` colour still wins) |
196
+ | `tone` | `'default' \| 'muted' \| 'primary' \| 'error'` | `'default'` | text colour: `muted` is `text-muted-foreground`, `primary` is `text-primary-text` (primary lightened in dark), `error` is `text-error-text`; `default` inherits (a `class` colour still wins) |
197
197
  | `as` | a tag name or a component | `'div'` | the element to render, keeping the variant, e.g. `as="h2"` or `as="span"` |
198
198
  | `asChild` | `boolean` | `false` | merge the classes onto the single child instead of rendering an element |
199
199
 
@@ -364,6 +364,8 @@ Takes every `<input>` attribute. `v-model` carries `string | number`;
364
364
  the error outline. For a labelled field with helper text and a form wrapper,
365
365
  use `TextField` on the [Form fields](form-fields.md) page.
366
366
 
367
+ In an input group (`InputGroupInput`, `InputGroupField`) the fill tint follows the value: an empty field is not tinted, with or without a `placeholder` (one without renders a blank `placeholder=" "` so that CSS can tell). The bare `Input` and `Textarea` have no tint and get no placeholder.
368
+
367
369
  **Emits:** `update:modelValue` — `string | number`.
368
370
  **Storybook:** `Primitives/Input` · **React:** `Input`
369
371
 
@@ -461,7 +463,7 @@ import { Icon } from '@nxgt/material-vue/components';
461
463
 
462
464
  <template>
463
465
  <Icon name="bell" />
464
- <Icon name="bell" type="solid" class="size-6 text-primary" aria-hidden="true" />
466
+ <Icon name="bell" type="solid" class="size-6 text-primary-text" aria-hidden="true" />
465
467
  </template>
466
468
  ```
467
469
 
@@ -901,7 +903,7 @@ defineProps<{ nonce?: string }>();
901
903
 
902
904
  | Prop | Type | Default | Effect |
903
905
  | --- | --- | --- | --- |
904
- | `nonce` | `string` | `undefined` | the nonce of the page's `style-src-elem 'nonce-…'`; `undefined` or `''` inherits the nearest outer `NonceProvider`'s (or `ConfigProvider`'s) nonce (none at the root) |
906
+ | `nonce` | `string` | `undefined` | the nonce of the page's `style-src-elem 'nonce-…'`; `undefined` or `''` inherits the nearest outer `NonceProvider`'s (or `ConfigProvider`'s) nonce, else, on the client from the first render (read in `setup`), the page's own (`csp-nonce` meta tag, …); a server has no page, so it passes the prop |
905
907
 
906
908
  `useCspNonce()` (`@nxgt/material-vue/composables`) reads it in a component of
907
909
  your own; `getCspNonce()` and `setCspNonce()` (`@nxgt/material-vue/lib`) read
@@ -238,7 +238,7 @@ props below, and `option` for anything else.
238
238
  | `height` | `string \| number` | `320` | the container's height; the chart fills it |
239
239
  | `loading` | `boolean` | `false` | a skeleton instead of the chart |
240
240
  | `empty` | `boolean` | when `data` is empty | force the empty state on or off |
241
- | `#emptyState` | slot | built-in empty state | what an empty chart shows |
241
+ | `#emptyState` | slot | the built-in compact `EmptyState` | what an empty chart shows; a custom slot keeps its centring wrapper |
242
242
  | `colors` | `string[]` | from the `--chart-*` tokens | override the palette; any CSS colour, `oklch()` included (converted to `rgb()` for ECharts) |
243
243
  | `margin` | `ChartMargin` | — | plot padding |
244
244
  | `option` | `EChartsOption` | — | merged into the generated ECharts option |
@@ -234,6 +234,8 @@ options by label. Picking the selected option again clears it.
234
234
 
235
235
  Single or multiple (chips) autocomplete typed by its options, with free-solo values, accent-insensitive matching, a debounced server search and option slots.
236
236
 
237
+ The input is a `combobox` (`aria-expanded`, `aria-controls`, `aria-activedescendant`) and the list a `listbox` of `option`s, as in `@nxgt/material`; the list opens on a click or on typing, not on focus alone. In tests, use `getByRole('combobox', { name })` and `getByRole('option', { name })`.
238
+
237
239
  ```vue
238
240
  <script setup lang="ts">
239
241
  import { ref } from 'vue';
@@ -336,15 +338,23 @@ update them on `@query-change`, and pass `filter="none"` when the server
336
338
  already filtered.
337
339
 
338
340
  A disabled option (`disabled: true` on it, in `options` or in what `on-search`
339
- returns) is greyed out, carries `aria-disabled` and a native `disabled`, so Tab
340
- skips it, and is never picked, by click or by Enter (which picks the first
341
- enabled option), in `single` and `multiple` mode.
341
+ returns) is greyed out, carries `aria-disabled` and a native `disabled`, so the arrow keys skip it, and is never picked, by click or by Enter (which picks the
342
+ highlighted option, the first enabled one until the arrows move it), in `single` and `multiple` mode.
342
343
 
343
344
  **Accessibility.** A polite live region (`role="status"`) says "Loading…",
344
345
  then "3 results" or "No results" (or the failure) once the search answers or
345
346
  typing pauses — never on every keystroke. The list is `aria-busy` while
346
347
  loading. The strings are `material.autocomplete.loading`, `.results`,
347
- `.search-error` and `.min-query`. The keyboard works as before.
348
+ `.search-error` and `.min-query`.
349
+
350
+ **Keyboard.** The input keeps the focus and points at the highlighted option
351
+ with `aria-activedescendant`. `ArrowDown` and `ArrowUp` open a closed list
352
+ and move the highlight over the enabled options (wrapping at the ends); with
353
+ an empty query `Home` and `End` jump to the first and last one. `Enter`
354
+ picks the highlighted option (or adds the free-solo value when "Add …" stands
355
+ for it) and, with the list closed, submits the form; hovering an option moves
356
+ the highlight. `Escape` closes the list, and `Backspace` on an empty query removes
357
+ the last chip. The options are not tab stops: Tab leaves the field.
348
358
 
349
359
  It is a generic component: `T` is inferred from `options`, and the slots —
350
360
  React's render props — receive it whole:
@@ -35,7 +35,7 @@ does not import that entry never has them in its tree.
35
35
  The tarball contains the built `dist/` (with KaTeX's fonts in
36
36
  `dist/fonts/katex/`, which the compiled stylesheet loads),
37
37
  `lib/styles.css`, the component
38
- stylesheets it imports (`lib/components/**/*.css`), `lib/map/map.css`, the
38
+ stylesheets it imports (`lib/components/**/*.css`), `lib/map/map.css`, `lib/backgrounds/backgrounds.css`, `lib/backgrounds/css/*.css`, the
39
39
  Nuxt module (`lib/nuxt`, with the
40
40
  `lib/vite/index.ts` it imports), these docs and `CHANGELOG.md` (from the
41
41
  release after 0.25.1). There is no `postinstall` build step.
@@ -112,6 +112,18 @@ Your own components can use the same per-instance store factory
112
112
  (`defineLocalStore` and the status, filter and step-form stores) from
113
113
  `@nxgt/material-vue/lib`; see [helpers](guides/helpers.md#pinia-store-factories).
114
114
 
115
+ ## The Vite plugin
116
+
117
+ In `vite.config.ts`, add the plugin after `tailwindcss()`:
118
+
119
+ ```ts
120
+ import { nxgtMaterial } from '@nxgt/material-vue/vite';
121
+
122
+ export default defineConfig({ plugins: [/* …, */ tailwindcss(), ...nxgtMaterial()] });
123
+ ```
124
+
125
+ It sets up a strict Content Security Policy (the header is written to `dist/.vite/nxgt-csp.json`), keeps fonts out of `data:` URIs, loads Inter when `@fontsource-variable/inter` is installed and fails the build when the stylesheet above is not imported. Each part has an opt-out: [Vite plugin guide](guides/vite-plugin.md). On Nuxt, see the guide's note.
126
+
115
127
  ## Icons
116
128
 
117
129
  `Icon` renders a Font Awesome **sprite** referenced by a root-relative URL on
@@ -174,7 +186,9 @@ export default defineNuxtConfig({
174
186
  // prefix: 'Nxgt', // <NxgtButton />
175
187
  // components: true, // auto-import components
176
188
  // imports: true, // auto-import composables and i18n helpers
177
- // pdf: { bundleStandardFonts: true }, // a server with no node_modules that renders /pdf
189
+ // pdf: { bundle: true }, // a server with no node_modules that renders /pdf
190
+ // fonts: { sans: 'inter' }, // self-hosted Inter (bun add @fontsource-variable/inter)
191
+ // csp: { zodJitless: true }, // zod's jitless script in every head (needs nuxt-security's nonce or hash)
178
192
  },
179
193
  });
180
194
  ```
@@ -184,7 +198,10 @@ export default defineNuxtConfig({
184
198
  | `prefix` | `string` | `''` | Prepended to every auto-imported component name |
185
199
  | `components` | `boolean` | `true` | Registers the components of `@nxgt/material-vue/components` for use without an import |
186
200
  | `imports` | `boolean` | `true` | Auto-imports the composables (`useTheme`, `useIsMobile`, `useIsClient`, `useLocalizedDate`, `useFormatters`, `useNumberFormatter`, `useSearchParam`, …) and `useMaterialTranslation`, `createTypedTranslation`, `useOptionalI18nContext` |
187
- | `pdf.bundleStandardFonts` | `boolean` | `false` | Adds `pdfkitStandardFonts()` to the Nitro build, for a server that renders with `@nxgt/material-vue/pdf` and runs without `node_modules` ([PDF documents](guides/pdf.md#fonts-on-a-fully-inlined-server)) |
201
+ | `pdf.bundle` | `boolean` | `false` | Adds `pdfkitStandardFonts()` to the Nitro build, for a server that renders with `@nxgt/material-vue/pdf` and runs without `node_modules` ([PDF documents](guides/pdf.md#fonts-on-a-fully-inlined-server)). `pdf.bundleStandardFonts` is its deprecated name |
202
+ | `fonts` | `boolean \| { katex?, sans? }` | `{ katex: true, sans: false }` | `katex`: a font is never inlined as a `data:` URI (`font-src 'self'`). `sans: 'inter'`: loads self-hosted Inter and sets `--font-sans` through `css` (a missing `@fontsource-variable/inter` warns once and keeps the system font). `false` turns both off |
203
+ | `csp.zodJitless` | `boolean` | `false` | Puts zod's jitless inline script in every page's `<head>` (`tagPriority: 'critical'`). It is an inline script: a strict CSP needs `nuxt-security` with `nonce: true` (or its script hashes) to allow it. The module sets no CSP itself ([Vite plugin guide](guides/vite-plugin.md#nuxt)) |
204
+ | `styles` | `boolean \| { check? }` | `false` | The `vite build` check that the stylesheet is imported, off by default under Nuxt because the module cannot see how `css` entries are resolved. `true` opts in |
188
205
 
189
206
  The module also:
190
207
 
@@ -223,6 +240,7 @@ export default defineConfig({
223
240
  | `@nxgt/material-vue` | Everything from `/components`, `/composables`, `/i18n`, `/lib`, `/models` and `/types` in one barrel, plus the compiled `style` condition (`dist/material-vue.css`, for apps without Tailwind) |
224
241
  | `@nxgt/material-vue/components` | UI components and `*FormField` wrappers ([components](components/README.md)) |
225
242
  | `@nxgt/material-vue/map` | Leaflet maps, markers, a location field. **Its own entry, not in the barrel above** ([maps](guides/maps.md)) |
243
+ | `@nxgt/material-vue/backgrounds` | Background presets: `Background` (a superset of the `/components` one: `preset` `gradient` or `grain`, `tone`, `intensity`, `animated`, `grain`, `seed`, `fill`), and the types and tables re-exported from `@nxgt/material-core/backgrounds`; more presets are coming. **Its own entry**, with an opt-in stylesheet ([backgrounds](guides/backgrounds.md)) |
226
244
  | `@nxgt/material-vue/pdf` | PDF files from plain data: builders, `usePdf`, `PdfDownloadButton`, `pdfResponse`. **Its own entry, not in the barrel above** ([PDF documents](guides/pdf.md)) |
227
245
  | `@nxgt/material-vue/composables` | `useTheme`, `useIsClient`, `useIsMobile`, `useLocalizedDate`, `useSearchParam`, … ([helpers](guides/helpers.md#nxgtmaterial-vuecomposables)) |
228
246
  | `@nxgt/material-vue/dnd` | Drag-and-drop primitives ([helpers](guides/helpers.md#nxgtmaterial-vuednd)) |
@@ -232,6 +250,7 @@ export default defineConfig({
232
250
  | `@nxgt/material-vue/types` | Shared function types (`PromiseOr`, `FunctionOr`, …) |
233
251
  | `@nxgt/material-vue/styles.css` | Source Tailwind v4 tokens. **This** is the stylesheet a Tailwind app imports |
234
252
  | `@nxgt/material-vue/map.css` | Leaflet's stylesheet, for apps that use `/map`. Not part of `styles.css` |
253
+ | `@nxgt/material-vue/backgrounds.css` | Every background preset's CSS, for apps that use `/backgrounds`; `@nxgt/material-vue/backgrounds/<preset>.css` loads one. Not part of `styles.css` |
235
254
  | `@nxgt/material-vue/nuxt` | The Nuxt module ([above](#nuxt)) |
236
255
  | `@nxgt/material-vue/vite` | `pdfkitStandardFonts()`, a Vite plugin for a server that inlines `/pdf` ([PDF documents](guides/pdf.md#fonts-on-a-fully-inlined-server)). Build-time code for `vite.config.ts` |
237
256
 
@@ -17,10 +17,7 @@ const items: MenuItem[] = [
17
17
  </script>
18
18
 
19
19
  <template>
20
- <ActivityLayout :items="items" avatar-src="/me.png">
21
- <template #brand>
22
- <strong>Acme</strong>
23
- </template>
20
+ <ActivityLayout :items="items" logo="/logo.svg" name="Acme" avatar-src="/me.png">
24
21
  <RouterView />
25
22
  </ActivityLayout>
26
23
  </template>
@@ -179,7 +176,11 @@ partitionMenu(items); // { content, footer }
179
176
  | `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
177
  | `scroll` | `'document' \| 'container'` | `'document'` | Who scrolls: the page (header and sidebar sticky), or the content region inside a shell one screen high |
181
178
  | `class` | `string` | — | Classes for the outer element (`[data-slot=activity-layout]`), which takes the other attributes too |
182
- | `#brand` | slot | an atom icon | Replaces the sidebar header: a logo, a product name. Always fill it; the default is a placeholder |
179
+ | `logo` | `string \| { src, alt? }` | an atom badge | The app's logo, an image URL: alone in the collapsed rail, beside `name` in the phone's menu sheet (see [The brand](#the-brand-logo-and-name)) |
180
+ | `#logo` | slot | — | The logo as a node (an inline SVG), in place of `logo`; it wins when both are given |
181
+ | `name` | `string` | — | The app's name, already translated: the logo's accessible name and tooltip in the rail, shown beside it in the sheet |
182
+ | `#brand` | slot | — | Replaces the sidebar header entirely, in the rail and in the menu sheet; wins over `logo` and `name` |
183
+ | `#header-title` | slot | — | The header's leading title: the context the app is in (the active organisation's name), right after the phone's menu button, truncated when long; when it is cut off, its full text is the `title` tooltip (#629) |
183
184
  | `#header-actions` | slot | — | In the header, before the avatar: notifications, help, `LanguageSwitcher`, `ThemeToggle` |
184
185
  | `#avatar` | slot | the `avatar-src` image | Replaces the avatar entirely |
185
186
  | default | slot | — | The routed content, usually `<RouterView />` |
@@ -214,32 +215,26 @@ the slots.
214
215
  ```vue
215
216
  <script setup lang="ts">
216
217
  import {
218
+ AccountMenu,
217
219
  ActivityLayout,
218
- Avatar,
219
- AvatarFallback,
220
- AvatarImage,
221
220
  LanguageSwitcher,
222
221
  type MenuItem,
223
222
  ThemeToggle,
224
223
  } from '@nxgt/material-vue/components';
225
224
 
226
- defineProps<{ items: MenuItem[]; user: { name: string; picture?: string } }>();
225
+ defineProps<{ items: MenuItem[]; user: { name: string; email: string; picture?: string } }>();
226
+ const emit = defineEmits<{ 'sign-out': [] }>();
227
227
  </script>
228
228
 
229
229
  <template>
230
- <ActivityLayout :items="items">
231
- <template #brand>
232
- <img src="/logo.svg" alt="Acme" class="h-8" />
233
- </template>
230
+ <ActivityLayout :items="items" logo="/logo.svg" name="Acme">
231
+ <template #header-title>Northwind school</template>
234
232
  <template #header-actions>
235
233
  <LanguageSwitcher />
236
234
  <ThemeToggle />
237
235
  </template>
238
236
  <template #avatar>
239
- <Avatar>
240
- <AvatarImage :src="user.picture ?? ''" :alt="user.name" />
241
- <AvatarFallback>{{ user.name.slice(0, 2) }}</AvatarFallback>
242
- </Avatar>
237
+ <AccountMenu :name="user.name" :secondary="user.email" :src="user.picture" @sign-out="emit('sign-out')" />
243
238
  </template>
244
239
  <RouterView />
245
240
  </ActivityLayout>
@@ -254,7 +249,92 @@ entry closes it — so does any route change, Back included — and focus goes t
254
249
  the page's `<main>`, unless the new screen already focused an element inside it
255
250
  (an error page's `h1`), which keeps it; closed with Escape, the close button or the overlay,
256
251
  focus returns to the menu button. Its entries show their labels, so they have
257
- no tooltip: none opens over the first entry as focus moves in.
252
+ no tooltip: none opens over the first entry as focus moves in. It shows the
253
+ app's `logo` and `name` (or `#brand`) above the entries, as the expanded
254
+ sidebar does.
255
+
256
+ ### The app bar: a title, the actions, the account
257
+
258
+ The header reads, from the start: the phone's menu button, `#header-title`
259
+ (the context the app is in, the active school or organisation), then
260
+ `#header-actions` (language, notifications) and `#avatar` at the end. The
261
+ title takes the room left and ends in an ellipsis when it does not fit, so
262
+ the avatar always stays on screen. It is not a heading: the screen's `h1` is
263
+ `ActivityContent`'s title.
264
+
265
+ `AccountMenu` is the `#avatar` most apps want: an avatar button named
266
+ "Account menu, Ada Lovelace" (`material.account-menu.trigger`) that opens a
267
+ menu aligned to its end, headed by the avatar, the name and `secondary`, then
268
+ the app's `items` and a destructive Sign out
269
+ (`material.account-menu.sign-out`). Choosing Sign out emits `sign-out` and
270
+ keeps the menu open: pass `signing-out` while the request runs, and the item
271
+ shows a spinner and ignores a second selection. Without `src`, or until it
272
+ loads, the avatar shows the initials on `bg-primary`.
273
+
274
+ ```vue
275
+ <AccountMenu
276
+ :name="user.name"
277
+ :secondary="user.email"
278
+ :src="user.picture"
279
+ :items="[
280
+ { label: t('menu.profile'), url: '/profile', icon: 'user' },
281
+ { label: t('menu.settings'), url: '/settings', icon: 'gear' },
282
+ ]"
283
+ :signing-out="pending"
284
+ @sign-out="signOut"
285
+ />
286
+ ```
287
+
288
+ Each entry is an `AccountMenuItem`, exported with it:
289
+
290
+ ```ts
291
+ type AccountMenuItem = {
292
+ label: string; // already translated
293
+ url: string; // rendered as a real link through `link-component`
294
+ icon?: IconName; // optional, decorative: the label names the entry
295
+ };
296
+ ```
297
+
298
+ `icon` is an `IconName`, a symbol id of the Font Awesome duotone sprite you serve (`user`, `gear`, `arrow-right-from-bracket`; browse the names at <https://fontawesome.com/icons>, and see [getting started](../getting-started.md#icons) for serving the sprite).
299
+
300
+ ### The brand: logo and name
301
+
302
+ `logo` (or the `#logo` slot) and `name` put the app's brand in the sidebar
303
+ header, in the shape each place has room for. `logo` is typed `AppLogo`
304
+ (`string | AppLogoImage`, where `AppLogoImage` is `{ src: string; alt?: string }`
305
+ and a string is the image URL), both exported from
306
+ `@nxgt/material-vue/components`:
307
+
308
+ - **The collapsed rail** (desktop, about 70 px wide) shows the **logo alone**.
309
+ `name` is its accessible name (an `<img alt>`, or `role="img"` with an
310
+ `aria-label` around the `#logo` node) and shows as a tooltip on hover, to
311
+ its right.
312
+ - **The expanded sidebar and the phone's menu sheet** show the **logo and the
313
+ name side by side**. The name is on screen there, so the logo is decorative
314
+ (`alt=""` or `aria-hidden`), and no tooltip repeats it.
315
+
316
+ ```vue
317
+ <ActivityLayout :items="items" logo="/logo.svg" :name="t('app.name')">…</ActivityLayout>
318
+ <!-- An image with its own alt, used when there is no name: -->
319
+ <ActivityLayout :items="items" :logo="{ src: '/logo.svg', alt: 'Acme' }">…</ActivityLayout>
320
+ <!-- A node, an inline SVG for instance: -->
321
+ <ActivityLayout :items="items" name="Acme">
322
+ <template #logo><AcmeMark class="size-8" /></template>
323
+ …
324
+ </ActivityLayout>
325
+ ```
326
+
327
+ Without a logo the header keeps its placeholder atom badge (named by `name`
328
+ in the rail when there is one); fill `logo` in a real app. `#brand` is the
329
+ full override: one node rendered as given, collapsed or expanded alike, in the
330
+ rail and in the menu sheet. `AppSidebar` takes the same props and slots, and
331
+ `AppHeader` takes them for its menu sheet.
332
+
333
+ `#brand` is rendered twice, once in the rail and once in the menu sheet (the
334
+ rail stays in the page on a phone, hidden): an `id` inside it appears twice
335
+ while the sheet is open, so leave ids out of it. A link in `#brand` (a home
336
+ link around the logo) is the sheet's first focusable element, so it takes
337
+ the focus when the sheet opens.
258
338
 
259
339
  ## Landmarks, headings and scrolling
260
340
 
@@ -375,7 +455,7 @@ home included, is a `RouterLink`, so it goes through the router's base: under
375
455
  `AppSidebar`, `AppHeader` and `AppBreadcrumb` are exported for a layout of your
376
456
  own. `AppSidebar` and `AppHeader` take the same `items`; `AppSidebar` also
377
457
  takes `expanded` (`v-model:expanded`) and `with-toggle-icon`, and has the
378
- `#brand` slot; `AppHeader` takes `avatar-src` and `show-menu-icon` (default
458
+ `#brand` slot, `logo`, `name` and `#logo`; `AppHeader` takes `avatar-src`, `logo`, `name` and `show-menu-icon` (default
379
459
  `true`), with `#actions`, `#avatar` and a default slot for the middle of the
380
460
  bar. The lower-level `Sidebar`, `TopAppBar`, `NavigationRail` and `Breadcrumb`
381
461
  primitives are on the