@nxgt/material 3.11.0 → 3.12.1

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 (66) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +15 -9
  3. package/dist/components/components.css +1 -0
  4. package/dist/components/ui/feedback/toast/toast-expressive.css +17 -0
  5. package/dist/components/ui/layout/app-shell/activity-backdrop-shapes.css +49 -0
  6. package/dist/components/ui/layout/app-shell/activity-backdrop.css +0 -17
  7. package/dist/components/ui/layout/sidebar/sidebar-tokens.css +7 -2
  8. package/dist/{components-1GK7YIhY.js → components-yx1t_5_P.js} +13981 -13660
  9. package/dist/components.js +3 -3
  10. package/dist/editor.js +1 -1
  11. package/dist/lib/components/ui/display/badge/badge.d.ts +7 -2
  12. package/dist/lib/components/ui/display/entity-header/entity-header.d.ts +9 -1
  13. package/dist/lib/components/ui/display/entity-header/entity-stats.d.ts +16 -0
  14. package/dist/lib/components/ui/display/entity-header/partials/entity-header-actions.d.ts +6 -0
  15. package/dist/lib/components/ui/display/entity-header/partials/entity-header-body.d.ts +14 -0
  16. package/dist/lib/components/ui/display/entity-header/partials/entity-header-icon.d.ts +6 -0
  17. package/dist/lib/components/ui/display/entity-header/partials/shown.d.ts +2 -0
  18. package/dist/lib/components/ui/display/list-tile/list-tile.d.ts +8 -1
  19. package/dist/lib/components/ui/display/list-tile/partials/list-tile-body.d.ts +2 -1
  20. package/dist/lib/components/ui/display/stat-card/index.d.ts +1 -1
  21. package/dist/lib/components/ui/display/stat-card/partials/stat-card-compact.d.ts +12 -0
  22. package/dist/lib/components/ui/display/stat-card/stat-card-classes.d.ts +3 -2
  23. package/dist/lib/components/ui/display/stat-card/stat-card.d.ts +2 -2
  24. package/dist/lib/components/ui/display/stat-card/types.d.ts +9 -0
  25. package/dist/lib/components/ui/feedback/alert/alert-variants.d.ts +18 -0
  26. package/dist/lib/components/ui/feedback/alert/alert.d.ts +2 -6
  27. package/dist/lib/components/ui/layout/app-shell/activity-column.d.ts +17 -0
  28. package/dist/lib/components/ui/layout/app-shell/activity-layout.d.ts +3 -106
  29. package/dist/lib/components/ui/layout/app-shell/activity-layout.types.d.ts +113 -0
  30. package/dist/lib/components/ui/layout/app-shell/app-breadcrumb.d.ts +2 -1
  31. package/dist/lib/components/ui/layout/app-shell/app-sidebar-icon-item.d.ts +13 -0
  32. package/dist/lib/components/ui/layout/app-shell/app-sidebar-label-item.d.ts +11 -0
  33. package/dist/lib/components/ui/layout/app-shell/keep-shapes-inside.d.ts +6 -0
  34. package/dist/lib/components/ui/layout/app-shell/observe-layout.d.ts +11 -0
  35. package/dist/lib/components/ui/layout/app-shell/shape-units.d.ts +16 -0
  36. package/dist/lib/components/ui/layout/app-shell/shapes-clear-content.d.ts +13 -0
  37. package/dist/lib/components/ui/layout/app-shell/types.d.ts +10 -0
  38. package/dist/lib/components/ui/layout/app-shell/use-keep-shapes-inside.d.ts +14 -0
  39. package/dist/lib/components/ui/layout/app-shell/watch-content.d.ts +19 -0
  40. package/dist/lib/components/ui/tones.d.ts +1 -1
  41. package/dist/main.js +3 -3
  42. package/dist/material.css +1 -1
  43. package/dist/{use-chat-composer-Bt2ULsv6.js → use-chat-composer-B0_STpQN.js} +1 -1
  44. package/docs/components/data-display.md +40 -9
  45. package/docs/components/feedback-and-status.md +12 -1
  46. package/docs/components/navigation-and-layout.md +3 -2
  47. package/docs/guides/app-shell.md +52 -8
  48. package/docs/guides/backgrounds.md +2 -0
  49. package/docs/guides/theming.md +4 -3
  50. package/docs/previews/data-display-badge.webp +0 -0
  51. package/docs/previews/data-display-entityheader.webp +0 -0
  52. package/docs/previews/data-display-listtile.webp +0 -0
  53. package/docs/previews/data-display-statcard.webp +0 -0
  54. package/docs/previews/feedback-status-banner.webp +0 -0
  55. package/docs/previews/navigation-layout-appshell-footer-icon.webp +0 -0
  56. package/docs/previews/navigation-layout-appshell-menu-badge.webp +0 -0
  57. package/docs/previews/navigation-layout-appshell.webp +0 -0
  58. package/docs/previews/navigation-layout-sidebar.webp +0 -0
  59. package/docs/roadmap.md +18 -0
  60. package/docs/troubleshooting.md +17 -2
  61. package/lib/components/components.css +1 -0
  62. package/lib/components/ui/feedback/toast/toast-expressive.css +17 -0
  63. package/lib/components/ui/layout/app-shell/activity-backdrop-shapes.css +49 -0
  64. package/lib/components/ui/layout/app-shell/activity-backdrop.css +0 -17
  65. package/lib/components/ui/layout/sidebar/sidebar-tokens.css +7 -2
  66. package/package.json +2 -1
@@ -6912,7 +6912,7 @@ function ov({ className: e, side: t = "bottom", sideOffset: r, align: i = "start
6912
6912
  function sv({ className: e, ...t }) {
6913
6913
  return /* @__PURE__ */ G(Zg, {
6914
6914
  "data-slot": "combobox-list",
6915
- className: n("grid gap-0.5 max-h-[min(calc(--spacing(96)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto p-1 data-empty:p-0", e),
6915
+ className: n("grid auto-rows-max gap-0.5 max-h-[min(calc(--spacing(96)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto p-1 data-empty:p-0", e),
6916
6916
  ...t
6917
6917
  });
6918
6918
  }
@@ -9,7 +9,7 @@ from `@nxgt/material/components`.
9
9
  | <img src="../previews/data-display-avatar.webp" width="96" alt=""> | [Avatar](#avatar) | Image with a fallback and an optional online / offline dot |
10
10
  | <img src="../previews/data-display-avatargroup.webp" width="96" alt=""> | [AvatarGroup](#avatargroup) | Overlapping avatars with a `+N` overflow |
11
11
  | <img src="../previews/data-display-shape.webp" width="96" alt=""> | [Shape](#shape) | Material 3 Expressive's shapes as a backdrop, an icon container or a mask for a photo |
12
- | <img src="../previews/data-display-badge.webp" width="96" alt=""> | [Badge](#badge) | Pill label in seven tones |
12
+ | <img src="../previews/data-display-badge.webp" width="96" alt=""> | [Badge](#badge) | Status pill, 20 px, in seven tones, soft or outlined |
13
13
  | <img src="../previews/data-display-countbadge.webp" width="96" alt=""> | [CountBadge](#countbadge) | Count in the corner of its children |
14
14
  | <img src="../previews/data-display-dayseparator.webp" width="96" alt=""> | [DaySeparator](#dayseparator) | "Today", "Yesterday" or the date between two hairlines |
15
15
  | <img src="../previews/data-display-periodcards.webp" width="96" alt=""> | [PeriodCards](#periodcards) | Consecutive date ranges (terms, sprints) as cards, with a current marker and an optional link per card |
@@ -210,20 +210,25 @@ and no lazy loading: give `src` the size you need.
210
210
 
211
211
  ![Badge](../previews/data-display-badge.webp)
212
212
 
213
- Pill label: default, secondary, error, success, info, warning, outlined.
213
+ Status badge: a 20 px pill with the `label-sm` text, compact padding and no hover or pointer affordance. Use `soft` (a tonal fill from the tone tokens) for a state; `outlined` is a thin 1 px line at 40 % of the tone. Every tone stays at 4.5:1 or more in the light and dark themes and follows the theme presets.
214
214
 
215
215
  ```tsx
216
216
  import { Badge } from '@nxgt/material/components';
217
217
 
218
- <Badge variant="success">Paid</Badge>
218
+ <Badge variant="soft" tone="success">Paid</Badge>
219
+ <Badge variant="outlined" tone="error">Late</Badge>
220
+ <Badge variant="success">New</Badge>
219
221
  ```
220
222
 
221
223
  | Prop | Type | Default | Effect |
222
224
  | --- | --- | --- | --- |
223
- | `variant` | `'default' \| 'secondary' \| 'error' \| 'success' \| 'info' \| 'warning' \| 'outlined'` | `'default'` | tone |
225
+ | `variant` | `'default' \| 'secondary' \| 'error' \| 'success' \| 'info' \| 'warning' \| 'soft' \| 'outlined'` | `'default'` | the filled tones, `soft` (recommended for a state) or `outlined` |
226
+ | `tone` | the eight named tones, or a tone of your own declared in `AppTones` | `'neutral'` | the colour of a `soft` or `outlined` badge; ignored by the filled variants |
224
227
  | `asChild` | `boolean` | `false` | renders its child (a link) with the badge styles |
225
228
 
226
- **Storybook:** `Data display/Badge` · **Vue:** `Badge`
229
+ A `Badge` link (`asChild`) no longer fades on hover: a badge is a status, not a button. Put a `Chip` or a `Button` where the user acts.
230
+
231
+ **Storybook:** `Data display/Badge` (Soft, SoftContrast) · **Vue:** `Badge` (without `soft` and `tone` yet)
227
232
 
228
233
  ---
229
234
 
@@ -428,12 +433,22 @@ import { StatCard } from '@nxgt/material/components';
428
433
  | `tone` | the eight named tones, or a tone of your own declared in `AppTones` | — | the value is drawn in the tone's text colour and the icon sits on a chip of its container. Without one the card is as before |
429
434
  | `variant` | `'outlined' \| 'tonal' \| 'filled'` | `'outlined'` | `tonal` paints the card with the tone's container and a fine outline in its text colour (3:1 against the page); `filled` paints it with the tone and draws everything on it in the on-colour. Both use `neutral` when there is no `tone`. Every text is 4.5:1 or more. In a [calm zone](../guides/theming.md#calm-zone), a `tonal` or `filled` card is the card's own surface with the tone's ring, and its icon chip a ring |
430
435
  | `valueSize` | `'default' \| 'display'` | `'default'` | `display` draws the value larger (`headline-large`) |
436
+ | `size` | `'default' \| 'sm'` | `'default'` | `sm` is a compact tile for a band or a header (see below); it ignores `hint`, `delta` and `valueSize` |
431
437
 
432
438
  ```tsx
433
439
  <StatCard tone="success" variant="tonal" valueSize="display" label="Revenue" value="€48,200" delta={12.4} icon={<CreditCard />} />
434
440
  ```
435
441
 
436
- Parts: the root carries `data-slot="stat-card"`, `data-variant` and, when a tone applies, `data-tone` (the tone, or `neutral` on a `tonal` or `filled` card without one); the icon wrapper carries `data-slot="stat-card-icon"`. The types `StatCardProps`, `StatCardVariant` and `StatCardValueSize` are exported.
442
+ **Compact tile (`size="sm"`).** For figures in a header band: a tile 40 to 56 px tall that hugs its content, the optional icon chip, then the value (tabular figures) over its label. It takes the same `tone` and `variant` (`tonal`, `outlined`, `filled`); `hint`, `delta` and `valueSize` are ignored (nothing is drawn for them, and `loading` shows a skeleton in place of the value only). Tiles in a `flex flex-wrap gap-2` row sit side by side and wrap on a phone.
443
+
444
+ ```tsx
445
+ <div className="flex flex-wrap gap-2">
446
+ <StatCard size="sm" variant="tonal" tone="primary" label="Students" value={22} />
447
+ <StatCard size="sm" variant="tonal" tone="tertiary" label="Classes" value={6} />
448
+ </div>
449
+ ```
450
+
451
+ Parts: the root carries `data-slot="stat-card"`, `data-variant` and, when a tone applies, `data-tone` (the tone, or `neutral` on a `tonal` or `filled` card without one); the icon wrapper carries `data-slot="stat-card-icon"`. The compact tile adds `data-size="sm"`. The types `StatCardProps`, `StatCardVariant`, `StatCardValueSize` and `StatCardSize` are exported.
437
452
 
438
453
  In `filled`, the delta and its trend arrow take the on-colour too (the arrow and the sign still say up or down). The outline's share is `--stat-card-ring-share` (`68%`, or `--nxgt-tone-ring-share` for every ring); it draws the tonal card's border, the filled card's edge in a calm zone and the hairline round the icon chip. Known limit: the dark theme's primary `filled` card has an edge of about 2.3:1 on a `Card` and 2.5:1 on the page (its text is 7:1); a `tonal` card avoids it. See [App tones](../guides/theming.md#app-tones-on-shape-fab-avatar-statcard-and-the-metric-cards).
439
454
 
@@ -618,10 +633,24 @@ import { Badge, EntityHeader } from '@nxgt/material/components';
618
633
  | `title` | `ReactNode` | — | the `h1`; optional: without it no heading is rendered (see below) |
619
634
  | `icon`, `status` | `ReactNode` | — | before and after the title; `icon` is decoration, shown in an `aria-hidden` badge, so put nothing meaningful or focusable there |
620
635
  | `metadata` | `{ label: string; value: ReactNode }[]` | `[]` | facts under the title, a description list: a muted label beside its value, no colon; a link in a value is drawn in the primary text colour (`text-primary-text`), not the prose link's info blue, and `nxgt-plain-link` still opts out |
636
+ | `stats` | `EntityStat[]` | `[]` | figures (counts, a mean) as compact `size="sm"` [StatCard](#statcard) tiles in a wrapping row under the metadata; a `tone` paints a tile, `0` is a figure, an empty value drops the tile (see Stats below) |
621
637
  | `actions` | `ResponsiveButtonProps[] \| ReactNode` | `[]` | an array becomes responsive buttons (icon-only on phones) |
622
638
 
623
639
  The root carries `data-slot="entity-header"`; the metadata is a `dl` with `data-slot="entity-metadata"` (a muted `dt` label beside its `dd` value, no colon), and the actions sit in `data-slot="entity-header-actions"` (absent when there are none). Inside an EntityHeader that replaces the old `summary-data` slot and its `Label:` text, so update a selector or a test that read "Label: value".
624
640
 
641
+ **Stats.** Figures (counts, a mean) go in `stats`, shown as compact [StatCard](#statcard) tiles (`size="sm"`) in a wrapping row under the metadata; facts (dates, names) stay in `metadata`. A `tone` paints a tile (tonal), none leaves it outlined; `0` is a figure, an empty value drops the tile. The row is `data-slot="entity-stats"`, and the type `EntityStat` is exported.
642
+
643
+ ```tsx
644
+ <EntityHeader
645
+ title="Class 6A"
646
+ metadata={[{ label: 'Year', value: '2026-2027' }]}
647
+ stats={[
648
+ { label: 'Students', value: 24, tone: 'primary' },
649
+ { label: 'Coefficient', value: 2 },
650
+ ]}
651
+ />
652
+ ```
653
+
625
654
  `title` is optional. A record page whose `h1` is the app bar's title (`handle.title`, `AppShellHandle.title`, see the [app shell guide](../guides/app-shell.md#the-pages-title-in-the-app-bar)) uses EntityHeader without it, so the page keeps a single heading:
626
655
 
627
656
  ```tsx
@@ -653,15 +682,17 @@ With a title, in a narrow container (under `@sm`) the order is title, metadata,
653
682
  Row with leading / trailing slots, title and subtitle; becomes a link or button when interactive. `size="sm"` is the dense row.
654
683
 
655
684
  ```tsx
656
- import { ChevronRight } from 'lucide-react';
657
685
  import { ListTile } from '@nxgt/material/components';
658
686
 
659
- <ListTile title="Notifications" subtitle="Email and push" trailing={<ChevronRight />} href="/settings/notifications" />
687
+ <ListTile title="Notifications" subtitle="Email and push" href="/settings/notifications" />
660
688
  ```
661
689
 
690
+ A tile with an `href` ends with a discreet chevron after `trailing`: decorative (`aria-hidden`, so the link's name stays its text) and flipped in right-to-left. `chevron={false}` opts out; `chevron` on a button tile (an `onClick` that navigates) opts in.
691
+
662
692
  | Prop | Type | Default | Effect |
663
693
  | --- | --- | --- | --- |
664
- | `href` | `string` | — | renders an `<a>` |
694
+ | `href` | `string` | — | renders an `<a>`, with a chevron by default |
695
+ | `chevron` | `boolean` | `true` with an `href`, else `false` | the decorative chevron after `trailing` |
665
696
  | `linkComponent` | `AppShellLinkComponent` | plain `<a>` | the link element, as in the app shell: takes `href`, spreads every other prop, forwards `ref` |
666
697
  | `onClick` | `MouseEventHandler` | — | renders a `<button>` |
667
698
  | `selected` | `boolean` | `false` | highlighted row |
@@ -7,7 +7,7 @@ centre.
7
7
  | | Component | What it does |
8
8
  | --- | --- | --- |
9
9
  | <img src="../previews/feedback-status-alert.webp" width="96" alt=""> | [Alert](#alert) | Callout with icon, title and description in seven tones |
10
- | <img src="../previews/feedback-status-banner.webp" width="96" alt=""> | [Banner](#banner) | Dismissible banner with an action |
10
+ | <img src="../previews/feedback-status-banner.webp" width="96" alt=""> | [Banner](#banner) | Dismissible banner with an action, in the look of Alert |
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 |
@@ -63,6 +63,13 @@ The root carries `data-slot="alert"`, the title `data-slot="alert-title"` and th
63
63
 
64
64
  Dismissible info / success / warning / error banner with an action.
65
65
 
66
+ A Banner looks like an [Alert](#alert): the same light tone surface, start
67
+ accent bar, tone-coloured icon, title and description typography and padding.
68
+ They are built on one shared variants module, so they cannot drift. What a
69
+ Banner adds is its role: page level (full width), an `action` and a dismiss
70
+ button. Before 3.12 it was a solid tinted fill with a full tone border and
71
+ rounded corners.
72
+
66
73
  ```tsx
67
74
  import { Banner, Button } from '@nxgt/material/components';
68
75
 
@@ -754,6 +761,10 @@ longer than the toast (a URL) breaks at its edge instead of pushing the
754
761
  action out. With a long message and an action, the action and the X go under
755
762
  the text; the icon stays 40px and never squeezes the text.
756
763
 
764
+ In a collapsed stack only the front toast's action and X are reachable; the
765
+ ones behind it appear when the stack opens on hover or when focus is inside
766
+ the toaster.
767
+
757
768
  The toast id defaults to the title, so two toasts with the same title
758
769
  replace each other; pass `options: { id }` to show both. `Toaster` wraps
759
770
  Sonner's and takes its props (`position`, `expand`, `richColors`…), all
@@ -7,7 +7,7 @@ documentation-site shell. Everything is imported from
7
7
 
8
8
  | | Component | What it does |
9
9
  | --- | --- | --- |
10
- | <img src="../previews/navigation-layout-appshell.webp" width="96" alt=""> | [ActivityLayout](#activitylayout) | App shell: sidebar, header, breadcrumbs, titled page; a route's `handle.title` is the page's `h1` in the app bar (it labels `main`; `usePageTitleFocus()` focuses it after a navigation), `breadcrumbRoot` a text root crumb, `context` the tenant line atop the phone's menu sheet; `menuOpen` / `onMenuOpenChange` control the phone's menu sheet, which closes itself on a followed link or any location change |
10
+ | <img src="../previews/navigation-layout-appshell.webp" width="96" alt=""> | [ActivityLayout](#activitylayout) | App shell: sidebar, header, breadcrumbs, titled page; a route's `handle.title` is the page's `h1` in the app bar (it labels `main`; `usePageTitleFocus()` focuses it after a navigation), `breadcrumbRoot` a text root crumb, `breadcrumb={false}` no trail, `context` the tenant line atop the phone's menu sheet; `menuOpen` / `onMenuOpenChange` control the phone's menu sheet, which closes itself on a followed link or any location change |
11
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 |
12
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` |
13
13
  | <img src="../previews/navigation-layout-sidebar.webp" width="96" alt=""> | [Sidebar](#sidebar) | Collapsible navigation sidebar |
@@ -64,7 +64,7 @@ phone's menu sheet pad clear of `env(safe-area-inset-*)`, as classes; see the
64
64
 
65
65
  | Prop | Type | Default | Effect |
66
66
  | --- | --- | --- | --- |
67
- | `items` | `MenuItem[]` | — | `label` (already translated), `url`, `icon`, `footer`, `badge` (`true` a dot, a number a count up to "99+"), `badgeLabel` (see [Badges](../guides/app-shell.md#badges)) |
67
+ | `items` | `MenuItem[]` | — | `label` (already translated), `url`, `icon`, `footer`, `display` (`'icon'`: a round icon-only button, the label as tooltip and name; see the [app shell guide](../guides/app-shell.md)), `badge` (`true` a dot, a number a count up to "99+"), `badgeLabel` (see [Badges](../guides/app-shell.md#badges)) |
68
68
  | `linkComponent` | `AppShellLinkComponent` | React Router `Link` | The link each menu entry renders, given `href`; see the [app shell guide](../guides/app-shell.md) |
69
69
  | `logo`, `name` | `AppLogo` (`ReactNode \| 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 |
70
70
  | `context` | `ReactNode` | — | a line at the top of the phone's menu sheet, above the brand: the current tenant's name; `name` stays the app's brand; `AppHeader` and `AppSidebar` take it too (see the [app shell guide](../guides/app-shell.md#the-brand-logo-and-name)) |
@@ -72,6 +72,7 @@ phone's menu sheet pad clear of `env(safe-area-inset-*)`, as classes; see the
72
72
  | `avatar` | `ReactNode` | — | an image URL or a node, in the header |
73
73
  | `headerTitle` | `ReactNode` | — | 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) |
74
74
  | `handle.title` | `ReactNode \| (match: UIMatch) => ReactNode` | — | on a route: the page's title in the app bar, its one `h1`, in the first server HTML; `ActivityContent` leaves its own title out while it is set (see the [app shell guide](../guides/app-shell.md#the-pages-title-in-the-app-bar)) |
75
+ | `breadcrumb` | `boolean` | `true` | `false` hides the breadcrumb trail (an error page); it is also hidden when every matched `handle.breadcrumb` renders `null`; see the [app shell guide](../guides/app-shell.md#breadcrumbs) |
75
76
  | `breadcrumbRoot` | `{ label: ReactNode; to: string }` | — | the breadcrumb's first crumb, a text link in place of the home icon (`AppBreadcrumb`'s `root`); see the [app shell guide](../guides/app-shell.md#breadcrumbs) |
76
77
  | `headerActions` | `ReactNode` | — | header, before the avatar |
77
78
  | `title`, `subtitle`, `actions` | `ReactNode` | — | `ActivityContent`'s header card; `title` is left out while a route's `handle.title` puts the page's title in the app bar |
@@ -82,6 +82,7 @@ type MenuItem = {
82
82
  url: string;
83
83
  icon: IconName; // a symbol of the duotone sprite, see Getting started › Icons
84
84
  footer?: boolean; // pin to the sidebar footer
85
+ display?: 'icon'; // a round, icon-only button; see below
85
86
  badge?: number | boolean | ReactNode; // see Badges
86
87
  badgeLabel?: string; // the badge's text in the link's name, already translated
87
88
  };
@@ -103,6 +104,22 @@ type MenuItem = {
103
104
 
104
105
  - **`footer: true` pins an entry to the footer.** Being last in the list does
105
106
  not: appending an entry never moves another one out of the footer.
107
+ - **`display: 'icon'` draws the entry as a round, icon-only button.** Meant
108
+ for the footer's "Settings": in the collapsed rail, the expanded sidebar and
109
+ the phone's menu sheet alike, it is an `IconButton` (the Button family's
110
+ look, ghost, tonal when it is the active entry) with the label as its
111
+ tooltip and, visually hidden, its accessible name, `aria-current="page"`
112
+ when active, and a 44px target on a coarse pointer. The entries without it
113
+ keep their icon and label. An icon entry draws no `badge`.
114
+
115
+ ```tsx
116
+ { label: t('menu.settings'), url: '/settings', icon: 'gear', footer: true, display: 'icon' }
117
+ ```
118
+
119
+ - The active entry's icon and label are in primary on the rail: a darker
120
+ primary in light (85 % of its lightness, so the label reads 4.5:1 on its
121
+ pill for every theme preset) and a lavender of its hue in dark; see
122
+ [the sidebar tokens](./theming.md).
106
123
  - The icons are drawn with `Icon type="duotone"`, so the duotone sprite must
107
124
  be served ([getting started](../getting-started.md#icons)).
108
125
 
@@ -183,6 +200,7 @@ partitionMenu(items); // { content, footer }
183
200
  | `avatar` | `ReactNode` | — | End of the header. A `string` is used as the image URL of an `Avatar`; anything else is rendered as-is |
184
201
  | `linkComponent` | `AppShellLinkComponent` | React Router `Link` | The link each menu entry renders, given `href` (see below) |
185
202
  | `headerTitle` | `ReactNode` | — | 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) |
203
+ | `breadcrumb` | `boolean` | `true` | `false` hides the breadcrumb trail, for an error page whose matched routes would show a misleading one (see [Breadcrumbs](#breadcrumbs)) |
186
204
  | `breadcrumbRoot` | `AppBreadcrumbRoot` (`{ label: ReactNode; to: string }`) | — | The breadcrumb's first crumb, in place of the home icon: a text link to `to` (see [Breadcrumbs](#breadcrumbs)) |
187
205
  | `headerActions` | `ReactNode` | — | In the header, before the avatar: notifications, help, `ThemeToggle` |
188
206
  | `children` | `ReactNode` | — | The routed content, usually `<Outlet />` |
@@ -498,11 +516,23 @@ or an `NxgtTheme` zone around it; see [Themes](./theming.md#the-provider-and-zon
498
516
  | `backdrop={<Background … />}` | that node, for this screen |
499
517
  | `backdrop={false}` | no background and no blobs: a plain page (`data-plain` on the root) |
500
518
 
501
- The theme's shapes (the motif and illustrated presets) stay whole: a shape that
502
- would cross the layer's edge, or reach the app bar, the breadcrumb, the header
503
- card or an `EntityHeader`, is not drawn, and nothing shows until that first
504
- check has run. Full-width textures such as `waves` and the gradient are not
505
- shapes and are not filtered. A `Background` passed as `backdrop` is kept whole the same way; any other node is left as it is.
519
+ **Where the shapes go.** The theme's shapes (the motif and illustrated presets)
520
+ are drawn whole or not at all, and only in empty space. A shape that would cross the layer's edge,
521
+ or reach the app bar, the breadcrumb, the header card or an `EntityHeader`, is
522
+ not drawn (a permanent exclusion). A shape that lies over (or within 8px of) the page's text or
523
+ controls (inputs, buttons, links, images, and every line of text) is hidden: it
524
+ fades out in 150ms (none under `prefers-reduced-motion`) and fades back once
525
+ the content moves away, for instance when the page scrolls or a list changes.
526
+ Shapes are hidden, never moved. The check reads the content when it changes or
527
+ resizes and keeps the boxes, so a scroll only shifts them (one `requestAnimationFrame`-throttled
528
+ pass, well under a millisecond on a typical page; text past the first 6000 boxes is not collected, so a shape may be drawn over it). A moon, a planet or an eclipse counts as one shape: it is hidden whole, with its halo. Limits: a sticky element is measured where it is laid out, not where it sticks, and a late font or image, or an inner scroller, triggers a new check a moment later. Nothing shows until the first
529
+ check has run, so no shape flashes over the content. On a phone, where the
530
+ content fills the width, the shapes you see are the ones in the empty part of
531
+ the page, usually below the content. Full-width textures such as `waves` and the
532
+ gradient are not shapes and are not filtered. A `Background` passed as
533
+ `backdrop` follows the same rule; any other node is left as it is. The layer is
534
+ below the content in z-order (`-z-10` in the column's own stacking context), so
535
+ a dialog, a sheet or a sticky header is always above it.
506
536
 
507
537
  `backdrop={false}` used to draw the built-in gradient, as if it were left out;
508
538
  it now removes it. To keep the gradient, leave `backdrop` out. A theme's
@@ -522,7 +552,7 @@ to know the theme on the server (a cookie, passed to `defaultTheme`).
522
552
 
523
553
  The shell draws the gradient with three blurred blobs behind its frame. To use
524
554
  one of the 50 presets of `@nxgt/material/backgrounds` instead, pass it as
525
- `backdrop`:
555
+ `backdrop` (behind the shell its shapes show only in empty space, see [Where the shapes go](#the-themes-background)):
526
556
 
527
557
  ```tsx
528
558
  import { Outlet } from 'react-router';
@@ -559,7 +589,7 @@ export function Shell({ items }: { items: MenuItem[] }) {
559
589
  `scroll="container"` alike.
560
590
  - **The chrome is lightly translucent and frosted.** Under a `backdrop`, the
561
591
  app bar, the breadcrumb (a pill as wide as its crumbs) and the tabs strip
562
- show a shape faintly through their fill, over a 6px blur that keeps it from
592
+ show the backdrop faintly through their fill, over a 6px blur that keeps it from
563
593
  competing with a text. The lists and tables (band and rows), the segmented
564
594
  `ToggleGroup`, the list bar, the phone tiles and the cards stay opaque, so no
565
595
  shape runs behind a row. The sidebar is frosted too (87%), but the layer is
@@ -797,7 +827,21 @@ to the router's root, then adds one crumb per matched route that has a
797
827
  the trail. The last crumb is text (`aria-current="page"`); the others are
798
828
  links, each inside its `<li>`. The home icon is itself the link, named "Home"
799
829
  (`material.app-shell.home`, translated): `getByRole('link', { name: 'Home' })`.
800
- The trail is hidden when no route has a breadcrumb.
830
+ The trail is hidden when no route has a breadcrumb. A crumb whose
831
+ `handle.breadcrumb` returns `null` (or `undefined`, `false`) is left out with
832
+ its separator, as `handle.title` accepts `null`, so the trail is hidden too
833
+ when every matched route returns `null`.
834
+
835
+ An error page rendered by `ActivityLayout` still matches the failed route, so
836
+ it would show its trail ("Home > Section" on a 404). Pass `breadcrumb={false}`
837
+ to render the layout without the trail:
838
+
839
+ ```tsx
840
+ // the root route's errorElement
841
+ <ActivityLayout items={items} breadcrumb={false}>
842
+ <ActivityContent title="Page not found" />
843
+ </ActivityLayout>
844
+ ```
801
845
 
802
846
  A multi-tenant app whose root is the current tenant passes `breadcrumbRoot`
803
847
  (`AppBreadcrumb`'s `root`) to replace the home icon with a text link,
@@ -181,6 +181,8 @@ of path data; `low-poly` is about 20 KB (210 nodes); `circuit` about 9 KB (90 no
181
181
 
182
182
  <img src="../previews/backgrounds-motifs.webp" width="480" alt="The shapes preset behind a card">
183
183
 
184
+ **Behind an `ActivityLayout`** a preset's discrete shapes are hidden wherever they would lie over the page's text or controls, and fade back when the content moves away (see [Where the shapes go](./app-shell.md#the-themes-background)). A `Background` used on its own, on a page you lay out, draws all its shapes.
185
+
184
186
  `shapes`, `confetti-grid`, `constellation`, `halftone`, `truchet`, `floating` and `scatter-icons` draw seeded, deterministic geometry from
185
187
  the tone's three colours: the same `seed` gives the same composition on the server and in the
186
188
  browser, and a new `seed` redraws it.
@@ -578,15 +578,16 @@ rail and menu sheet) is drawn with three tokens, in light and in dark:
578
578
  | --- | --- | --- | --- |
579
579
  | `--sidebar-active` | `bg-sidebar-active` | primary at 15 % | primary at 30 % |
580
580
  | `--sidebar-active-hover` | `bg-sidebar-active-hover` | primary at 20 % | primary at 35 % |
581
- | `--sidebar-active-foreground` | `text-sidebar-active-foreground` | `--primary` | `oklch(from var(--primary) 0.72 0.15 h)`, 6.3:1 or more on the pill |
581
+ | `--sidebar-active-foreground` | `text-sidebar-active-foreground` | `oklch(from var(--primary) calc(l * 0.85) c h)`, 4.5:1 or more on the pill for every theme preset | `oklch(from var(--primary) 0.72 0.15 h)`, 6.3:1 or more on the pill |
582
582
 
583
583
  The pill is the icon's (collapsed rail) or the whole row (expanded), a pill in the collapsed rail and, in the expanded row, an `--nxgt-item-radius` (6px) shape that carries the 3px start bar every chosen row has, drawn in `--sidebar-active-foreground` (a `Highlight` bar in forced colours); the
584
584
  foreground is the label and the icon, whose duotone layers both follow it (in
585
585
  dark too: the icon of `AppSidebar` is the lifted lavender like its label). The
586
586
  label is read on the pill (expanded) or on the sidebar (collapsed rail); the
587
587
  icon, on the pill. In browsers without relative colour syntax (before Chrome
588
- 119, Safari 18, Firefox 128) the dark foreground falls back to `--primary`
589
- itself, as the dark lift needs `oklch(from …)`.
588
+ 119, Safari 18, Firefox 128) the foreground falls back to `--primary`
589
+ itself in light and dark, as both the light darkening and the dark lift need
590
+ `oklch(from …)`.
590
591
 
591
592
  The defaults are derived where the entry is drawn, from the `--primary` it
592
593
  sits under, so a `--primary` set on a wrapper recolours the rail inside it.
package/docs/roadmap.md CHANGED
@@ -308,6 +308,24 @@ version they shipped in, and planned entries may name the major they target.
308
308
 
309
309
  ## Shipped
310
310
 
311
+ - **Display polish** — `Badge` is a status badge: a 20 px pill with the
312
+ `label-sm` text and no hover, a `soft` variant (the tone's container under
313
+ the tone's text, 4.5:1) and a `tone` prop for `soft` and `outlined`, taking
314
+ your own tones too; `CountBadge` keeps its own size. `ListTile` with an
315
+ `href` ends with a discreet chevron (`chevron={false}` opts out). `StatCard`
316
+ gets a compact `size="sm"` tile and `EntityHeader` a `stats` prop that shows
317
+ figures as these tiles. `Banner` takes `Alert`'s look. A footer `MenuItem`
318
+ can be `display: 'icon'`, a round icon-only button with a tooltip, 44 px on
319
+ a touch screen; the rail's active label is drawn in a darker primary so it
320
+ reads 4.5:1 on every theme preset. `breadcrumb={false}` hides the trail, and
321
+ so does a trail whose crumbs all render `null` (#1475). `Combobox` rows are
322
+ touch-sized on a touch screen (#1474). The backdrop's shapes are hidden while
323
+ they lie over the page's text or controls, never moved (#1478). Nothing
324
+ changes in core. React only; the
325
+ Vue port is a `vue` issue. Ships in 3.12.0.
326
+ ([Data display](components/data-display.md),
327
+ [App shell](guides/app-shell.md))
328
+
311
329
  - **Buttons and the type scale in Material 3 Expressive** — `Typography` takes
312
330
  the Material 3 scale (`display`, `headline`, `title`, `body`, `label`, each
313
331
  `lg`, `md` and `sm`, plus `emphasized`) as Tailwind tokens: `text-title-md`
@@ -109,6 +109,7 @@ words in your error.
109
109
 
110
110
  **Navigation**
111
111
 
112
+ - [A `ListTile` link shows two chevrons](#a-listtile-link-shows-two-chevrons)
112
113
  - [A `ListTile` link reloads the whole page](#a-listtile-link-reloads-the-whole-page)
113
114
  - [After a link in the phone menu, the focus goes to `<main>`, not to the page's title](#after-a-link-in-the-phone-menu-the-focus-goes-to-main-not-to-the-pages-title)
114
115
  - [The phone menu needs two Escapes to close, or a tooltip shows over it](#the-phone-menu-needs-two-escapes-to-close-or-a-tooltip-shows-over-it)
@@ -979,8 +980,9 @@ your token against the page at 4.5:1:
979
980
  <Button variant="tonal" color="error">Delete</Button>
980
981
  ```
981
982
 
982
- A `Badge` link of your own with a hover fade such as `[a&]:hover:bg-error/90`
983
- in its `className` puts the fade back: darken instead, as in
983
+ A `Badge` no longer changes on hover; a hover fade of your own such as
984
+ `[a&]:hover:bg-error/90` in its `className` puts the fade back: darken
985
+ instead, as in
984
986
  `[a&]:hover:bg-[color-mix(in_oklab,var(--color-error)_90%,black)]` (with
985
987
  your badge's colour in place of `error`). A
986
988
  subtitle colour of your own on a selected `ListTile` must reach 4.5:1 on
@@ -1974,6 +1976,19 @@ tree.
1974
1976
 
1975
1977
  ## Navigation
1976
1978
 
1979
+ ### A `ListTile` link shows two chevrons
1980
+
1981
+ **When:** a `ListTile` with an `href` and a `trailing={<ChevronRight />}` of
1982
+ your own draws a second chevron since the tile adds one.
1983
+ **Why:** a link tile ends with a decorative chevron by default.
1984
+ **Fix:** drop your own, or keep it and pass `chevron={false}`:
1985
+
1986
+ ```tsx
1987
+ <ListTile title="Notifications" href="/settings/notifications" />
1988
+ // or, with your own icon
1989
+ <ListTile title="Notifications" href="/settings/notifications" chevron={false} trailing={<MyArrow />} />
1990
+ ```
1991
+
1977
1992
  ### A `ListTile` link reloads the whole page
1978
1993
 
1979
1994
  **When:** a `ListTile` with an `href` is clicked inside a router app: the
@@ -55,6 +55,7 @@
55
55
  /* ActivityLayout: the route transition (cross-fade, none under reduced motion). */
56
56
  @import "./ui/layout/app-shell/route-transition.css";
57
57
  @import "./ui/layout/app-shell/activity-backdrop.css";
58
+ @import "./ui/layout/app-shell/activity-backdrop-shapes.css";
58
59
  /* DataTable cards and tiles: the item shell (hairline, tint, start bar, columns by the list's width). */
59
60
  @import "./ui/collections/data-table/data-table-items.css";
60
61
  @import "./ui/collections/data-table/data-table-items-compact.css";
@@ -107,6 +107,23 @@
107
107
  gap: 8px;
108
108
  margin-inline-start: auto;
109
109
  }
110
+ /* A stacked toast behind the front one (`data-expanded="false"`,
111
+ * `data-front="false"`) sits under it, scaled and offset by a few pixels:
112
+ * its buttons are covered, so a tap lands on the front toast, and a covered,
113
+ * scaled close button fails axe's `target-size` (#1480). Its buttons are
114
+ * hidden until the stack opens (sonner opens it on hover, not on focus) or
115
+ * focus is inside the toaster, so a Tab from the front toast still reaches
116
+ * them. Sonner's own `opacity: 0` for these does not apply to our unstyled
117
+ * (`data-styled="false"`) toasts. */
118
+ [data-sonner-toast][data-expanded="false"][data-front="false"]
119
+ .nxgt-toast-buttons {
120
+ visibility: hidden;
121
+ }
122
+ [data-sonner-toaster]:focus-within
123
+ [data-sonner-toast][data-expanded="false"][data-front="false"]
124
+ .nxgt-toast-buttons {
125
+ visibility: visible;
126
+ }
110
127
  .nxgt-toast-title {
111
128
  color: var(--toast-fg);
112
129
  }
@@ -0,0 +1,49 @@
1
+ /* Which shapes of the backdrop show (observe-layout.ts, watch-content.ts).
2
+ * Plain CSS in `@layer components`, beside activity-backdrop.css. */
3
+ @layer components {
4
+ /* The theme's backdrop keeps its shapes whole (keep-shapes-inside.ts):
5
+ * the scene waits for the first pass, then a shape that would cross the
6
+ * layer's edge or reach the header band is hidden, never cut. */
7
+ [data-keep-inside]:not([data-checked]) .mo,
8
+ [data-slot="activity-layout-backdrop"]
9
+ [data-background]:not([data-checked])
10
+ .mo {
11
+ visibility: hidden;
12
+ }
13
+ [data-keep-inside] .mo > [data-outside],
14
+ [data-slot="activity-layout-backdrop"]
15
+ [data-background]
16
+ .mo
17
+ > [data-outside] {
18
+ visibility: hidden;
19
+ }
20
+
21
+ /* A shape over the page's text or controls fades out and comes back once
22
+ * clear (watch-content.ts): it is hidden, never moved. A compound body (the
23
+ * moon, the planet, the eclipse: `.pt.mg`) is hidden whole, with its halo
24
+ * (`.pt.mh`). The scene stays invisible until the first check, so nothing
25
+ * flashes over the content. */
26
+ [data-keep-inside] .mo > *,
27
+ [data-keep-inside] .pt,
28
+ [data-slot="activity-layout-backdrop"] [data-background] .mo > *,
29
+ [data-slot="activity-layout-backdrop"] [data-background] .pt {
30
+ transition: opacity 150ms ease;
31
+ }
32
+ /* Important: a preset sets its own opacity on a body (`.md`, the dark
33
+ * moon's, a celestial body's); in a layer, `!important` beats every normal
34
+ * declaration of those. The 150 ms transition still runs. */
35
+ [data-keep-inside] .mo > [data-hidden],
36
+ [data-keep-inside] .pt[data-hidden],
37
+ [data-slot="activity-layout-backdrop"] [data-background] .mo > [data-hidden],
38
+ [data-slot="activity-layout-backdrop"] [data-background] .pt[data-hidden] {
39
+ opacity: 0 !important;
40
+ }
41
+ @media (prefers-reduced-motion: reduce) {
42
+ [data-keep-inside] .mo > *,
43
+ [data-keep-inside] .pt,
44
+ [data-slot="activity-layout-backdrop"] [data-background] .mo > *,
45
+ [data-slot="activity-layout-backdrop"] [data-background] .pt {
46
+ transition: none;
47
+ }
48
+ }
49
+ }
@@ -77,23 +77,6 @@
77
77
  display: none;
78
78
  }
79
79
 
80
- /* The theme's backdrop keeps its shapes whole (use-keep-shapes-inside.ts):
81
- * the scene waits for the first pass, then a shape that would cross the
82
- * layer's edge or reach the header band is hidden, never cut. */
83
- [data-keep-inside]:not([data-checked]) .mo,
84
- [data-slot="activity-layout-backdrop"]
85
- [data-background]:not([data-checked])
86
- .mo {
87
- visibility: hidden;
88
- }
89
- [data-keep-inside] .mo > [data-outside],
90
- [data-slot="activity-layout-backdrop"]
91
- [data-background]
92
- .mo
93
- > [data-outside] {
94
- visibility: hidden;
95
- }
96
-
97
80
  /* A preset fills the layer: the layer's size, not `100lvw x 100lvh`, which
98
81
  * counts the scrollbar and a phone's toolbar. */
99
82
  [data-slot="activity-layout-backdrop"] > [data-background] {
@@ -3,13 +3,18 @@
3
3
  :root {
4
4
  /* The active entry of the sidebar and the navigation rail: its tint, the
5
5
  * tint hovered, the start bar (the label's colour), and the icon and label drawn on it. Translucent primary
6
- * (these shares, 15 % and 20 % hovered); the label is primary itself. The
6
+ * (these shares, 15 % and 20 % hovered); the label is primary at 85 % of its lightness in light (a lavender of its hue in dark). The
7
7
  * utilities (`bg-sidebar-active`...) derive them on the element, so they
8
8
  * follow a `--primary` set below :root. To theme the rail set
9
9
  * `--sidebar-active`, `--sidebar-active-hover` and
10
10
  * `--sidebar-active-foreground` (all three, or the first two and the
11
11
  * foreground); the derived colours are not declared here, on purpose. */
12
12
  --nxgt-sidebar-active-share: 15%;
13
+ /* Light: primary itself reads 4.5:1 on the page but not on its own 15 % pill
14
+ * for every preset (corail 3.7:1), so the label is primary darkened to 85 %
15
+ * of its lightness (4.99:1 on the worst preset, hovered). The preset spec
16
+ * (sidebar-active-contrast.test.ts) reads this number back. */
17
+ --nxgt-sidebar-active-l: calc(l * 0.85);
13
18
  /* The height of the app bar and of the rail's header from `md` up, so the
14
19
  * rail's logo and the bar's title share one centre line. Phones keep the
15
20
  * bar alone at this height; the menu sheet's header is unconstrained. */
@@ -24,7 +29,7 @@
24
29
  /* The dark theme's primary is too dark to read on its surfaces (2.5:1), so
25
30
  * the rail's label is a saturated lavender of its hue (5.3:1: lightness
26
31
  * 0.72, chroma 0.15) on a denser pill (30 %, 35 % hovered). Light keeps the
27
- * lightness and chroma of primary (the `l` and `c` fallbacks). */
32
+ * chroma of primary and 85 % of its lightness. */
28
33
  --nxgt-sidebar-active-share: 30%;
29
34
  --nxgt-sidebar-active-hover-share: 35%;
30
35
  --nxgt-sidebar-active-l: 0.72;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "3.11.0",
3
+ "version": "3.12.1",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"
@@ -465,6 +465,7 @@
465
465
  "@vitejs/plugin-react": "6.0.4",
466
466
  "@vitest/browser-playwright": "4.1.10",
467
467
  "ajv": "^8.20.0",
468
+ "axe-core": "4.12.1",
468
469
  "globals": "^17.7.0",
469
470
  "hls.js": "^1.7.3",
470
471
  "leaflet": "^1.9.4",