@nxgt/material 3.10.2 → 3.12.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 (158) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +10 -11
  3. package/dist/{async-slice-DDjaJY_F.js → async-slice-4qbER86I.js} +1 -1
  4. package/dist/button-BNjw-pnE.js +327 -0
  5. package/dist/button-group-context-CaP0X5p_.js +5 -0
  6. package/dist/{button-tones-BgZJ1tkD.js → button-tones-BwuVvAmd.js} +9 -1
  7. package/dist/{callout-view-DdJPdFrP.js → callout-view-C6voDS00.js} +1 -1
  8. package/dist/charts.js +5 -5
  9. package/dist/{chip-DpySRqCy.js → chip-DLgQTJBZ.js} +4 -4
  10. package/dist/{code-editor-BiA14xua.js → code-editor-BZbaYLvo.js} +8 -8
  11. package/dist/code-editor.js +7 -7
  12. package/dist/{code-language-items-C_dJ6GVs.js → code-language-items-BT5BO5w6.js} +2 -2
  13. package/dist/components/base.css +21 -6
  14. package/dist/components/components.css +6 -2
  15. package/dist/components/type-scale.css +194 -0
  16. package/dist/components/ui/actions/button/button.css +156 -0
  17. package/dist/components/ui/actions/button-group/button-group-standard.css +131 -0
  18. package/dist/components/ui/actions/button-group/button-group.css +4 -1
  19. package/dist/components/ui/actions/icon-button/icon-button.css +115 -0
  20. package/dist/components/ui/actions/morph-fill.css +62 -0
  21. package/dist/components/ui/feedback/toast/toast-expressive.css +17 -0
  22. package/dist/components/ui/layout/app-shell/activity-backdrop-shapes.css +49 -0
  23. package/dist/components/ui/layout/app-shell/activity-backdrop.css +0 -17
  24. package/dist/components/ui/layout/sidebar/sidebar-tokens.css +7 -2
  25. package/dist/{components-BkF9N-Yx.js → components-yx1t_5_P.js} +13740 -13422
  26. package/dist/components.js +24 -24
  27. package/dist/{dialog-CkIYpJXk.js → dialog-BQp5IdFl.js} +147 -147
  28. package/dist/dnd.js +1 -1
  29. package/dist/{dropdown-menu-BC5Tbhb9.js → dropdown-menu-BsH9W_3C.js} +3 -3
  30. package/dist/editor-viewer.js +1 -1
  31. package/dist/editor.js +13 -13
  32. package/dist/{empty-state-SeAg2TOX.js → empty-state-WaU4-T2c.js} +5 -5
  33. package/dist/escape-owner-C4W_sOAU.js +116 -0
  34. package/dist/{field-blur-DPLsMufs.js → field-blur-CJHzVSh9.js} +1 -1
  35. package/dist/{field-blur-context-1ZwJgsHE.js → field-blur-context-Cft9s7Hg.js} +2 -2
  36. package/dist/{hover-card-Cox-tPDz.js → hover-card-C0fw2kLK.js} +72 -71
  37. package/dist/i18n.js +12 -12
  38. package/dist/{icon-badge-q3Ve8gra.js → icon-badge-DUAq9f7H.js} +4 -4
  39. package/dist/icon-button-ifTpXt0A.js +355 -0
  40. package/dist/{illustration-BfHRpVzM.js → illustration-BmQmvW_4.js} +1 -1
  41. package/dist/illustrations.js +2 -2
  42. package/dist/lib/components/ui/actions/button/button-tones.d.ts +16 -0
  43. package/dist/lib/components/ui/actions/button/button.d.ts +13 -0
  44. package/dist/lib/components/ui/actions/button/button.variants.d.ts +4 -3
  45. package/dist/lib/components/ui/actions/button-group/button-group-context.d.ts +16 -0
  46. package/dist/lib/components/ui/actions/button-group/button-group-reset.d.ts +13 -0
  47. package/dist/lib/components/ui/actions/button-group/button-group.d.ts +19 -2
  48. package/dist/lib/components/ui/actions/icon-button/icon-button-shape.d.ts +3 -2
  49. package/dist/lib/components/ui/actions/icon-button/icon-button.d.ts +18 -4
  50. package/dist/lib/components/ui/actions/icon-button/icon-button.variants.d.ts +34 -4
  51. package/dist/lib/components/ui/display/badge/badge.d.ts +7 -2
  52. package/dist/lib/components/ui/display/entity-header/entity-header.d.ts +9 -1
  53. package/dist/lib/components/ui/display/entity-header/entity-stats.d.ts +16 -0
  54. package/dist/lib/components/ui/display/entity-header/partials/entity-header-actions.d.ts +6 -0
  55. package/dist/lib/components/ui/display/entity-header/partials/entity-header-body.d.ts +14 -0
  56. package/dist/lib/components/ui/display/entity-header/partials/entity-header-icon.d.ts +6 -0
  57. package/dist/lib/components/ui/display/entity-header/partials/shown.d.ts +2 -0
  58. package/dist/lib/components/ui/display/list-tile/list-tile.d.ts +8 -1
  59. package/dist/lib/components/ui/display/list-tile/partials/list-tile-body.d.ts +2 -1
  60. package/dist/lib/components/ui/display/stat-card/index.d.ts +1 -1
  61. package/dist/lib/components/ui/display/stat-card/partials/stat-card-compact.d.ts +12 -0
  62. package/dist/lib/components/ui/display/stat-card/stat-card-classes.d.ts +3 -2
  63. package/dist/lib/components/ui/display/stat-card/stat-card.d.ts +2 -2
  64. package/dist/lib/components/ui/display/stat-card/types.d.ts +9 -0
  65. package/dist/lib/components/ui/feedback/alert/alert-variants.d.ts +18 -0
  66. package/dist/lib/components/ui/feedback/alert/alert.d.ts +2 -6
  67. package/dist/lib/components/ui/feedback/typing-indicator/typing-indicator.d.ts +1 -1
  68. package/dist/lib/components/ui/fields/slider/split-slider-props.d.ts +1 -1
  69. package/dist/lib/components/ui/fields/slider/use-slider-control.d.ts +1 -1
  70. package/dist/lib/components/ui/layout/app-shell/activity-column.d.ts +17 -0
  71. package/dist/lib/components/ui/layout/app-shell/activity-layout.d.ts +3 -106
  72. package/dist/lib/components/ui/layout/app-shell/activity-layout.types.d.ts +113 -0
  73. package/dist/lib/components/ui/layout/app-shell/app-breadcrumb.d.ts +2 -1
  74. package/dist/lib/components/ui/layout/app-shell/app-sidebar-icon-item.d.ts +13 -0
  75. package/dist/lib/components/ui/layout/app-shell/app-sidebar-label-item.d.ts +11 -0
  76. package/dist/lib/components/ui/layout/app-shell/keep-shapes-inside.d.ts +6 -0
  77. package/dist/lib/components/ui/layout/app-shell/observe-layout.d.ts +11 -0
  78. package/dist/lib/components/ui/layout/app-shell/shape-units.d.ts +16 -0
  79. package/dist/lib/components/ui/layout/app-shell/shapes-clear-content.d.ts +13 -0
  80. package/dist/lib/components/ui/layout/app-shell/types.d.ts +10 -0
  81. package/dist/lib/components/ui/layout/app-shell/use-keep-shapes-inside.d.ts +14 -0
  82. package/dist/lib/components/ui/layout/app-shell/watch-content.d.ts +19 -0
  83. package/dist/lib/components/ui/overlays/dialog/dialog-variants.d.ts +1 -1
  84. package/dist/lib/components/ui/primitives/toggle/toggle.d.ts +1 -1
  85. package/dist/lib/components/ui/primitives/typography/typography.d.ts +17 -7
  86. package/dist/lib/components/ui/primitives/typography/typography.variants.d.ts +55 -0
  87. package/dist/lib/components/ui/selection/choice-group/use-choice-group-field.d.ts +2 -2
  88. package/dist/lib/components/ui/tones.d.ts +1 -1
  89. package/dist/lib/i18n/translator.d.ts +1 -1
  90. package/dist/lib.js +2 -2
  91. package/dist/{line-path-BQeLQZ-P.js → line-path-C5pZGC49.js} +1 -1
  92. package/dist/main.js +29 -29
  93. package/dist/map.js +9 -9
  94. package/dist/material.css +1 -1
  95. package/dist/pdf-reader.js +1 -1
  96. package/dist/print.js +1 -1
  97. package/dist/{public-CtpsxV4I.js → public-B0Nq_yYV.js} +524 -524
  98. package/dist/{render-json-DyFYFIkZ.js → render-json-BOhvPnSv.js} +1 -1
  99. package/dist/{rich-code-block-C2Fp_FDU.js → rich-code-block-v7WmeZp9.js} +5 -5
  100. package/dist/{rich-text-viewer-LO1IZCWs.js → rich-text-viewer-W15XIwah.js} +2 -2
  101. package/dist/{skeleton-Bmp4gWAH.js → skeleton-DSdtr2qc.js} +1 -1
  102. package/dist/{sortable-provider-pVaEZzty.js → sortable-provider-CgDTolQR.js} +3 -3
  103. package/dist/{spinner-CsHcmcpF.js → spinner-BtSIYVrC.js} +3 -3
  104. package/dist/{state-scenes-SGNyZjTz.js → state-scenes-BX03WmSu.js} +1 -1
  105. package/dist/styles.css +2 -0
  106. package/dist/theme-picker.js +3 -3
  107. package/dist/{theme-toggle-oU-fh6WE.js → theme-toggle-B2Pehdhn.js} +2 -2
  108. package/dist/{toggle-group-j8zaxEg1.js → toggle-group-BMJNcFAC.js} +1 -1
  109. package/dist/{toolbar-items-CqHvlTU8.js → toolbar-items-rUHdVqAq.js} +399 -398
  110. package/dist/{tooltip-rhbqNmr3.js → tooltip-CkttDIab.js} +1 -1
  111. package/dist/{translate-material-CGZljjp1.js → translate-material-Bd_KgfB8.js} +1 -1
  112. package/dist/{translator-DCYO4Ij3.js → translator-Dje8HrNz.js} +5 -5
  113. package/dist/{use-chat-composer-nWWfVyrK.js → use-chat-composer-B0_STpQN.js} +4173 -4173
  114. package/dist/{use-material-translation-DkxcLbLM.js → use-material-translation-B3V_bz-5.js} +1 -1
  115. package/dist/{utils-Bcd2Z3ea.js → utils-BLfntITJ.js} +14 -1
  116. package/docs/components/buttons-and-actions.md +64 -15
  117. package/docs/components/data-display.md +40 -9
  118. package/docs/components/feedback-and-status.md +12 -1
  119. package/docs/components/navigation-and-layout.md +4 -3
  120. package/docs/components/primitives.md +52 -10
  121. package/docs/guides/app-shell.md +52 -8
  122. package/docs/guides/backgrounds.md +2 -0
  123. package/docs/guides/theming.md +55 -3
  124. package/docs/previews/buttons-actions-button.webp +0 -0
  125. package/docs/previews/buttons-actions-iconbutton.webp +0 -0
  126. package/docs/previews/data-display-badge.webp +0 -0
  127. package/docs/previews/data-display-entityheader.webp +0 -0
  128. package/docs/previews/data-display-listtile.webp +0 -0
  129. package/docs/previews/data-display-statcard.webp +0 -0
  130. package/docs/previews/feedback-status-banner.webp +0 -0
  131. package/docs/previews/navigation-layout-appshell-footer-icon.webp +0 -0
  132. package/docs/previews/navigation-layout-appshell-menu-badge.webp +0 -0
  133. package/docs/previews/navigation-layout-appshell.webp +0 -0
  134. package/docs/previews/navigation-layout-sidebar.webp +0 -0
  135. package/docs/previews/primitives-typography.webp +0 -0
  136. package/docs/roadmap.md +53 -4
  137. package/docs/troubleshooting.md +75 -4
  138. package/lib/components/base.css +21 -6
  139. package/lib/components/components.css +6 -2
  140. package/lib/components/type-scale.css +194 -0
  141. package/lib/components/ui/actions/button/button.css +156 -0
  142. package/lib/components/ui/actions/button-group/button-group-standard.css +131 -0
  143. package/lib/components/ui/actions/button-group/button-group.css +4 -1
  144. package/lib/components/ui/actions/icon-button/icon-button.css +115 -0
  145. package/lib/components/ui/actions/morph-fill.css +62 -0
  146. package/lib/components/ui/feedback/toast/toast-expressive.css +17 -0
  147. package/lib/components/ui/layout/app-shell/activity-backdrop-shapes.css +49 -0
  148. package/lib/components/ui/layout/app-shell/activity-backdrop.css +0 -17
  149. package/lib/components/ui/layout/sidebar/sidebar-tokens.css +7 -2
  150. package/lib/styles.css +2 -0
  151. package/package.json +2 -1
  152. package/dist/button-DbfdITBB.js +0 -322
  153. package/dist/components/ui/actions/icon-button/icon-button-morph-media.css +0 -65
  154. package/dist/components/ui/actions/icon-button/icon-button-morph.css +0 -152
  155. package/dist/escape-owner-CripBce_.js +0 -57
  156. package/dist/icon-button-DJcWpIhu.js +0 -319
  157. package/lib/components/ui/actions/icon-button/icon-button-morph-media.css +0 -65
  158. package/lib/components/ui/actions/icon-button/icon-button-morph.css +0 -152
@@ -1,5 +1,5 @@
1
1
  import { r as e } from "./i18n-provider-C6HhBw34.js";
2
- import { t } from "./translate-material-CGZljjp1.js";
2
+ import { t } from "./translate-material-Bd_KgfB8.js";
3
3
  import { useCallback as n } from "react";
4
4
  //#region lib/i18n/use-material-translation.ts
5
5
  function r() {
@@ -1733,7 +1733,20 @@ var n = (e, t) => {
1733
1733
  }, Pe = (e, t, n) => {
1734
1734
  let r = t[n];
1735
1735
  r !== void 0 && (e[n] = e[n] ? e[n].concat(r) : r);
1736
- }, Fe = ((e, ...t) => typeof e == "function" ? k(Ae, e, ...t) : k(() => je(Ae(), e), ...t))({ extend: { theme: { radius: ["field", "field-inner"] } } });
1736
+ }, Fe = ((e, ...t) => typeof e == "function" ? k(Ae, e, ...t) : k(() => je(Ae(), e), ...t))({ extend: { theme: {
1737
+ radius: ["field", "field-inner"],
1738
+ text: [
1739
+ "display",
1740
+ "headline",
1741
+ "title",
1742
+ "body",
1743
+ "label"
1744
+ ].flatMap((e) => [
1745
+ "lg",
1746
+ "md",
1747
+ "sm"
1748
+ ].flatMap((t) => [`${e}-${t}`, `${e}-${t}-em`]))
1749
+ } } });
1737
1750
  function Ie(...e) {
1738
1751
  return Fe(t(e));
1739
1752
  }
@@ -6,9 +6,9 @@ and language switches. Everything is imported from `@nxgt/material/components`.
6
6
 
7
7
  | | Component | What it does |
8
8
  | --- | --- | --- |
9
- | <img src="../previews/buttons-actions-button.webp" width="96" alt=""> | [Button](#button) | Five variants in seven colours and eight sizes, with a loading state |
10
- | <img src="../previews/buttons-actions-iconbutton.webp" width="96" alt=""> | [IconButton](#iconbutton) | Icon-only button with an optional tooltip |
11
- | <img src="../previews/buttons-actions-buttongroup.webp" width="96" alt=""> | [ButtonGroup](#buttongroup) | Joins adjacent buttons into one control |
9
+ | <img src="../previews/buttons-actions-button.webp" width="96" alt=""> | [Button](#button) | Five variants in seven colours, six sizes, a round or square shape and a selected state, with a loading state |
10
+ | <img src="../previews/buttons-actions-iconbutton.webp" width="96" alt=""> | [IconButton](#iconbutton) | Icon-only button in five sizes and three widths, with an optional tooltip |
11
+ | <img src="../previews/buttons-actions-buttongroup.webp" width="96" alt=""> | [ButtonGroup](#buttongroup) | Joins adjacent buttons into one control; the expressive look widens the pressed one |
12
12
  | <img src="../previews/buttons-actions-splitbutton.webp" width="96" alt=""> | [SplitButton](#splitbutton) | Main action plus a menu of related ones, Material 3 Expressive |
13
13
  | <img src="../previews/buttons-actions-fab.webp" width="96" alt=""> | [Fab](#fab) | The screen's main action, floating; extended with its label, folds on scroll |
14
14
  | <img src="../previews/buttons-actions-fabmenu.webp" width="96" alt=""> | [FabMenu](#fabmenu) | A FAB that opens a few related actions as pills |
@@ -28,7 +28,7 @@ and language switches. Everything is imported from `@nxgt/material/components`.
28
28
 
29
29
  ![Button](../previews/buttons-actions-button.webp)
30
30
 
31
- Filled, tonal, outlined, ghost or link button in seven colours, four sizes and four square icon sizes, with a loading state.
31
+ Filled, tonal, outlined, ghost or link button in seven colours, six sizes (24 to 96 px) and four square icon sizes, round or square, with a selected state and a loading state.
32
32
 
33
33
  ```tsx
34
34
  import { Button } from '@nxgt/material/components';
@@ -43,6 +43,7 @@ import { Mail } from 'lucide-react';
43
43
  </Button>
44
44
  <Button loading>Saving</Button>
45
45
  <Button size="sm">Small</Button>
46
+ <Button size="xl" shape="square">Big and square</Button>
46
47
  <Button size="icon" aria-label="Email">
47
48
  <Mail />
48
49
  </Button>
@@ -52,8 +53,10 @@ import { Mail } from 'lucide-react';
52
53
  | Prop | Type | Default | Effect |
53
54
  | --- | --- | --- | --- |
54
55
  | `variant` | `'filled' \| 'tonal' \| 'outlined' \| 'ghost' \| 'link'` | `'filled'` | the style |
55
- | `color` | `'primary' \| 'secondary' \| 'success' \| 'info' \| 'warning' \| 'error' \| 'default'` | `'primary'` | the colour; `default` is the foreground colour. A `ghost` button in `secondary`, `success`, `info`, `warning` or `error` takes that colour's text and a tint of it on hover, at 4.5:1 on the page and on a card in both themes; in `primary` or `default` it keeps the foreground text |
56
- | `size` | `'xs' \| 'sm' \| 'default' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon' \| 'icon-lg'` | — | height: 24, 32, 36 and 40 px; the `icon*` sizes are square, for a button holding only an icon (give it an `aria-label`). Unset, the button keeps its content's height, as before sizes existed |
56
+ | `color` | `'primary' \| 'secondary' \| 'success' \| 'info' \| 'warning' \| 'error' \| 'default'` | `'primary'` | the colour; `default` is the foreground colour. A `ghost` button in `secondary`, `success`, `info`, `warning` or `error` takes that colour's text and a tint of it on hover, at 4.5:1 on the page and on a card in both themes; without a `color` (or with `default`) it keeps the foreground text, neutral; `color="primary"` set on a ghost button is tinted and takes the primary text |
57
+ | `size` | `'xs' \| 'sm' \| 'default' \| 'lg' \| 'xl' \| '2xl' \| 'icon-xs' \| 'icon-sm' \| 'icon' \| 'icon-lg'` | — | height: 24, 32, 36, 40, 56 and 96 px, with a label from the type scale (`label-md` to `headline-lg`, heavier as the size grows) and an icon that follows (14, 16, 18, 20, 28 and 44 px); the `icon*` sizes are square, for a button holding only an icon (give it an `aria-label`). Unset, the button keeps its content's height and its 16 px padding, as before sizes existed |
58
+ | `shape` | `'round' \| 'square'` | `'round'` | `round` is a pill (radius half the height); `square` has a corner of 6 px at `xs` to 28 px at `2xl`. A press springs to the other shape and scales to .97 (.985 at `2xl`); reduced motion keeps the colour change only |
59
+ | `selected` | `boolean` | — | makes it a toggle: `aria-pressed` and `data-selected`, the other shape and a solid fill of its `color` (a `ghost` toggle without a `color` is a neutral tint of the text colour). Unset: a plain button. Ignored with `asChild` (a warning in development): set `aria-pressed` on the child |
57
60
  | `loading` | `boolean` | — | busy: a spinner, `aria-busy="true"` and `aria-disabled="true"`, and every click cancelled — a submit button submits neither on a click nor on Enter in a field. It stays focusable (no native `disabled`), so focus is not lost |
58
61
  | `lockOnClick` | `boolean` | — | a click makes the button busy at once, until `loading` has turned true and back (or for one second when it never does): for a `loading` that turns true only after an `await` |
59
62
  | `spinnerVariant` | `SpinnerProps['variant']` | `'2'` | which spinner `loading` shows |
@@ -62,7 +65,19 @@ import { Mail } from 'lucide-react';
62
65
  | `asChild` | `boolean` | `false` | styles its only child instead of rendering a `<button>` |
63
66
  | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | not `submit` by default — pass it inside a form |
64
67
 
65
- The classes are exported as `buttonVariants` for styling your own element.
68
+ The classes are exported as `buttonVariants` for styling your own element (Pagination, AlertDialog and the calendar's date picker use them).
69
+
70
+ **The look.** The label is set in the type scale and is heavier than before (weights 560 at `xs` to 700 at `2xl`). The focus is a 2 px outline in the button's colour, 2 px off the button (the error colour with `aria-invalid`). A disabled button has neutral text (40 % of the text colour) and a neutral 12 % fill, none on `ghost` and `link`. `outlined` is a 1.5 px line inside the box at 55 % of the colour, and keeps a `ButtonText` outline in forced colours, where the inset shadow is removed. A `ghost` button is neutral without a `color`, and takes the tone of one it is given. On a coarse pointer every size has a 44 px hit area.
71
+
72
+ ```tsx
73
+ const [on, setOn] = useState(false);
74
+
75
+ <Button selected={on} onClick={() => setOn(!on)}>
76
+ <Star />
77
+ Favourite
78
+ </Button>;
79
+ <Button size="2xl" shape="square" variant="tonal">Start</Button>;
80
+ ```
66
81
 
67
82
  **On a phone.** A button is never wider than its container, and keeps its
68
83
  content's width in a column: a label too
@@ -71,7 +86,7 @@ accessible name — and a `title` you give it — keep the whole text. Give a
71
86
  label that must be read in full `wrap`, or a width of your own (`w-full`).
72
87
  The text is put in a `<span data-slot="button-label">` (one per run of
73
88
  text) that shrinks and ends in the ellipsis; it needs the text to be the button's own child; with `asChild`,
74
- put the label in a `<span>` inside the child element.
89
+ put the label in a `<span>` inside the child element. On a touch screen in Safari (no `overflow-clip-margin`) an `asChild` host does not clip, to keep its 44 px hit area, so it has no ellipsis at all: a label that needs one goes in a `<span>` with `truncate`.
75
90
 
76
91
  ```tsx
77
92
  <Button title="Continue to the payment details">Continue to the payment details</Button>;
@@ -110,7 +125,7 @@ A test finds a busy button by its state, not by `disabled`:
110
125
  `expect(button).toHaveAttribute('aria-busy', 'true')` (`toBeDisabled()` is
111
126
  false: it is still focusable).
112
127
 
113
- **Storybook:** `Buttons & actions/Button` (`Submits Once`, `Lock On Click`), `Buttons & actions/Button/Phone` · **Vue:** `Button`
128
+ **Storybook:** `Buttons & actions/Button` (`Submits Once`, `Lock On Click`), `Buttons & actions/Button/Expressive` (`SizesLadder`, `Shape`, `Selected`), `Buttons & actions/Button/Press and motion` (`PressMorph`, `ReducedMotion`), `Buttons & actions/Button/States` (`FocusOutline`, `Disabled`, `GhostSelectedNeutral`, `SelectedForcedColours`, `OutlinedForcedColours`, `PhoneBigSizes`), `Buttons & actions/Button/Phone`, `Guides/Buttons expressive` · **Vue:** `Button` (no `xl`, `2xl`, `shape` or `selected`)
114
129
 
115
130
  ---
116
131
 
@@ -136,14 +151,16 @@ import { Pencil, Trash2 } from 'lucide-react';
136
151
 
137
152
  | Prop | Type | Default | Effect |
138
153
  | --- | --- | --- | --- |
139
- | `variant` | `'filled' \| 'tonal' \| 'outlined' \| 'ghost' \| 'link'` | `'filled'` | the style; a round 36 px button |
154
+ | `variant` | `'filled' \| 'tonal' \| 'outlined' \| 'ghost' \| 'link'` | `'filled'` | the style; a round button |
140
155
  | `color` | as `Button` | `'primary'` | the colour; a `ghost` one in a colour other than `primary` or `default` draws its icon in it, as `Button` does |
156
+ | `size` | `'xs' \| 'sm' \| 'default' \| 'lg' \| 'xl'` | — | 24, 32, 36, 40 or 56 px, icons of 14, 16, 18, 20 or 28 px (for a glyph without its own `size-*` class; an `Icon` is 16 px unless sized). Unset: the 36 px button with a 16 px icon it always was, so an explicit `default` has an 18 px icon |
157
+ | `width` | `'narrow' \| 'default' \| 'wide'` | `'default'` | 0.78, 1 or 1.45 times the height, always a pill. Ignored with `shape` (a warning in development) |
141
158
  | `tooltip` | `ReactNode` | — | wraps it in a `Tooltip`; a string tooltip also becomes the `aria-label` |
142
159
  | `aria-label` | `string` | the tooltip, if a string | give one when there is no string tooltip |
143
160
  | `shape` | `ShapeName` | — | draws the button as one of the expressive shapes (`cookie-9`, `sunny`, `squircle`…) instead of a circle; changing it morphs. Unset: the round button, unchanged |
144
161
  | `morph` | `'round' \| 'pill' \| 'none'` | `'pill'` | what a **toggle** (`aria-pressed` set) does when pressed: `pill` fills with its colour and widens by 14 px into a pill, `round` fills and stays a circle, `none` keeps the look your classes give it. Not set on a button without `aria-pressed`, one with a `shape`, or a Toolbar item (the Toolbar's own look) |
145
162
 
146
- **A toggle button** takes the Toolbar morph's motion (`--nxgt-spring-fast`) and tokens: the fill is the button's `color` (`primary` by default), a pointer down squeezes the corners, the focus ring follows the pill. Its neighbours move by the 14 px, any size class works (`size-8`, `size-14`). Below 600 px wide only the colour changes, so a phone row never reflows; `prefers-reduced-motion` keeps the colour change only; in forced colours the pressed fill is `SelectedItem`. The pressed fill and corners are the button's own (a `bg-*` or `rounded-*` class on a pressed toggle does not apply); a ghost toggle without a `color` fills with `primary`. The morph works with a `tooltip` too (the styles match `data-icon-button`, which the tooltip trigger does not replace). The date picker's input-mode switch uses `morph="round"`; `PasswordField`'s eye, which sits inside its field, and `RichChatComposer`'s format toggle, which draws its own pressed look, use `morph="none"`; the mini player's move button and the Gallery viewer's repeat are pills too, white with a black icon on their dark chrome. `CodeEditor`'s word-wrap pill takes the editor theme's tint.
163
+ **A toggle button** fills with the button's `color` (`primary` by default) through `data-selected` and the fill `Button` shares, and a pointer down squeezes the corners; `pill` widens the button itself by 14 px (its neighbours move by the 14 px), at any `size` (give the button a `size`, not a `size-*` class: the pressed width follows the `size`, see [troubleshooting](../troubleshooting.md#a-pill-iconbutton-toggle-is-the-wrong-width-with-a-size--class)). Below 600 px wide only the colour changes, so a phone row never reflows; `prefers-reduced-motion` keeps the colour change only; in forced colours the pressed fill is `SelectedItem`. The pressed fill and corners are the button's own (a `bg-*` or `rounded-*` class on a pressed toggle does not apply); a ghost toggle without a `color` is neutral when selected (a tint of the text colour), and primary only with a `color`. The focus is a 2 px outline 2 px off the button, a disabled button has a neutral 12 % fill (none on `ghost` and `link`), and on a coarse pointer every size has a 44 px hit area. The morph works with a `tooltip` too (the styles match `data-icon-button`, which the tooltip trigger does not replace). The date picker's input-mode switch uses `morph="round"`; `PasswordField`'s eye, which sits inside its field, and `RichChatComposer`'s format toggle, which draws its own pressed look, use `morph="none"`; the mini player's move button and the Gallery viewer's repeat are pills too, white with a black icon on their dark chrome. `CodeEditor`'s word-wrap pill takes the editor theme's tint.
147
164
 
148
165
  ```tsx
149
166
  import { IconButton } from '@nxgt/material/components';
@@ -214,7 +231,9 @@ built this way. A `Contacts` badge is `role="img"`, named by the contact type.
214
231
 
215
232
  The classes are exported as `iconButtonVariants`.
216
233
 
217
- **Storybook:** `Buttons & actions/IconButton` (`Shapes`, `ShapeToggle`), `Buttons & actions/IconButton/Morph` (`Frozen`, `WidensOrStaysRound`, `Contrast`, `Unaffected`, `WithTooltip`), `Buttons & actions/IconButton/Shapes` (`InsideAGroup`) · **Vue:** `IconButton` (no `shape`)
234
+ **Forced colours.** An `outlined` button keeps a `ButtonText` outline and a pressed toggle, `Button` or `IconButton`, fills with `SelectedItem` (its text and icon in `SelectedItemText`); the focus outline follows the system colours.
235
+
236
+ **Storybook:** `Buttons & actions/IconButton` (`Shapes`, `ShapeToggle`), `Buttons & actions/IconButton/Sizes and widths` (`SizesLadder`, `Widths`, `Colours`), `Buttons & actions/IconButton/States` (`FocusOutline`, `Disabled`, `GhostToggleNeutral`, `SelectedForcedColours`), `Buttons & actions/IconButton/Toggle and motion` (`SizedToggle`, `ReducedMotion`, `PressMorph`), `Buttons & actions/IconButton/Morph` (`Frozen`, `WidensOrStaysRound`, `Contrast`), `Buttons & actions/IconButton/Morph unaffected` (`Unaffected`, `WithTooltip`), `Buttons & actions/IconButton/Shapes` (`InsideAGroup`) · **Vue:** `IconButton` (no `shape`)
218
237
 
219
238
  ---
220
239
 
@@ -222,7 +241,7 @@ The classes are exported as `iconButtonVariants`.
222
241
 
223
242
  ![ButtonGroup](../previews/buttons-actions-buttongroup.webp)
224
243
 
225
- Joins adjacent buttons into one control.
244
+ Joins adjacent buttons into one control; `look="standard"` is the Material 3 Expressive action group.
226
245
 
227
246
  ```tsx
228
247
  import { Button, ButtonGroup } from '@nxgt/material/components';
@@ -236,12 +255,42 @@ import { Button, ButtonGroup } from '@nxgt/material/components';
236
255
  </ButtonGroup>;
237
256
  ```
238
257
 
239
- A `<div>` taking its props. It stretches to its container (`w-full`); mark the
258
+ | Prop | Type | Default | Effect |
259
+ | --- | --- | --- | --- |
260
+ | `look` | `'standard'` | — | the Material 3 Expressive group (see below). Unset: the original group, which becomes the `standard` one by default in the next major, with `look="legacy"` to keep it |
261
+ | `size` | `'xs' \| 'sm' \| 'default' \| 'lg' \| 'xl'` | — | the `size` every `Button` and `IconButton` in the group takes through a context; a child's own `size` wins. Works with either look |
262
+ | `...div` | `ComponentProps<'div'>` | — | the host's props; `data-slot="button-group"`, `data-look` and `data-size` are set on it |
263
+
264
+ A `<div>` taking its props. Without `look` it stretches to its container (`w-full`); mark the
240
265
  current button with `data-state="active"`: it fills with the primary colour and
241
266
  its label and icons switch to the primary foreground, so every button can stay
242
267
  `tonal` and only the attribute moves.
243
268
 
244
- **Storybook:** `Buttons & actions/ButtonGroup` · **Vue:** `ButtonGroup`
269
+ `look="standard"` is the Material 3 Expressive group: the buttons keep their own
270
+ look, 8px apart, and a pressed one widens by 14px on each side while its
271
+ neighbours give the width back, so the group's width never changes (7, 10 and
272
+ 11px at `xs`, `sm` and `default`, which have less padding to give; none with
273
+ reduced motion; an icon-only neighbour at the end of the group keeps its icon and may give back less). `size` is shared by every `Button` and `IconButton` in the group
274
+ (a child's own `size` wins) and works with either look. It sets
275
+ `data-look` and `data-size` on the host; mark the group with `role="group"` and a
276
+ name when it is one control.
277
+
278
+ ```tsx
279
+ <ButtonGroup look="standard" size="lg" role="group" aria-label="Actions">
280
+ <Button variant="tonal">Save</Button>
281
+ <Button variant="tonal">Share</Button>
282
+ <IconButton variant="tonal" aria-label="More">
283
+ <MoreHorizontal />
284
+ </IconButton>
285
+ </ButtonGroup>
286
+ ```
287
+
288
+ On a coarse pointer the standard group's gap is `max(8px, 44px - the height of its size)`
289
+ (20px at `xs`, 12px at `sm`, 8px from `default`), so every button, in a row or
290
+ across wrapped rows, keeps a 44px hit area that touches no neighbour's. A child with its own smaller `size` in a standard group keeps the group's gap, so its coarse 44px hit area can overlap a neighbour's: give the group the smallest `size` instead. Without
291
+ `look` the group is unchanged.
292
+
293
+ **Storybook:** `Buttons & actions/ButtonGroup`, `Buttons & actions/ButtonGroup/Standard`, `Buttons & actions/ButtonGroup/Standard toggles`, `Buttons & actions/ButtonGroup/Standard phone and motion`, `Guides/Buttons expressive` · **Vue:** `ButtonGroup` (no `look` or `size`)
245
294
 
246
295
  ---
247
296
 
@@ -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 |
@@ -295,7 +296,7 @@ import { BottomAppBar, IconButton, TopAppBar, Typography } from '@nxgt/material/
295
296
 
296
297
  <TopAppBar>
297
298
  <IconButton variant="ghost" aria-label="Open menu"><Menu /></IconButton>
298
- <Typography variant="title-medium" className="flex-1">Inbox</Typography>
299
+ <Typography variant="title-md" emphasized className="flex-1">Inbox</Typography>
299
300
  <IconButton variant="ghost" aria-label="Search"><Search /></IconButton>
300
301
  </TopAppBar>
301
302
 
@@ -10,7 +10,7 @@ other pages are built from these.
10
10
  | <img src="../previews/primitives-accordion.webp" width="96" alt=""> | [Accordion](#accordion) | Collapsible sections with a rotating chevron and animated content |
11
11
  | <img src="../previews/primitives-collapsible.webp" width="96" alt=""> | [Collapsible](#collapsible) | A single show / hide region and its trigger |
12
12
  | <img src="../previews/primitives-card.webp" width="96" alt=""> | [Card](#card) | Card surface with header, action slot, content and footer |
13
- | <img src="../previews/primitives-typography.webp" width="96" alt=""> | [Typography](#typography) | Text in the Material type scale |
13
+ | <img src="../previews/primitives-typography.webp" width="96" alt=""> | [Typography](#typography) | Text in the Material 3 type scale, with an emphasized weight |
14
14
  | <img src="../previews/primitives-anchorheading.webp" width="96" alt=""> | [AnchorHeading](#anchorheading) | Heading with a hover copy-link |
15
15
  | <img src="../previews/primitives-label.webp" width="96" alt=""> | [Label](#label) | Form label with an optional required asterisk |
16
16
  | <img src="../previews/primitives-helpertext.webp" width="96" alt=""> | [HelperText](#helpertext) | Hint or error message under a field |
@@ -171,36 +171,78 @@ with `as` (`<CardTitle as="h2">`), so `getByRole('heading', { name })` finds it.
171
171
 
172
172
  ![Typography](../previews/primitives-typography.webp)
173
173
 
174
- Text in the Material type scale (headline, title, body, caption…).
174
+ Text in the Material 3 type scale: five roles, three sizes each, and an emphasized weight.
175
175
 
176
176
  ```tsx
177
177
  import { Typography } from '@nxgt/material/components';
178
178
 
179
179
  <>
180
- <Typography variant="headline-small">Orders</Typography>
181
- <Typography variant="body-medium">12 orders are waiting to ship.</Typography>
182
- <Typography variant="caption">Updated a minute ago</Typography>
180
+ <Typography variant="headline-sm">Orders</Typography>
181
+ <Typography>12 orders are waiting to ship.</Typography>
182
+ <Typography variant="label-sm" tone="muted">Updated a minute ago</Typography>
183
+ <Typography variant="title-md" emphasized>Next delivery</Typography>
183
184
  </>;
184
185
  ```
185
186
 
186
187
  | Prop | Type | Default | Effect |
187
188
  | --- | --- | --- | --- |
188
- | `variant` | `'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 |
189
+ | `variant` | `` `${'display' \| 'headline' \| 'title' \| 'body' \| 'label'}-${'lg' \| 'md' \| 'sm'}` `` | `'body-md'` | the step of the scale: size, line height, tracking and weight together. The fifteen former names still work, deprecated (see below) |
190
+ | `emphasized` | `boolean` | `false` | the heavier weight of the same step (`text-<step>-em`); same size, line height and tracking. No effect on a deprecated alias that is emphasized already |
189
191
  | `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 `className` colour still wins) |
190
192
  | `as` | an intrinsic tag name (`keyof JSX.IntrinsicElements`) | `'div'` | the element to render, keeping the variant, e.g. `as="h2"` or `as="span"` |
191
193
  | `asChild` | `boolean` | `false` | merge the classes onto the single child instead of rendering an element |
192
194
 
195
+ **The scale.** Sizes are in px, then line height; the weight is 400 except `title-md`, `title-sm` and the three `label` steps (500).
196
+
197
+ | Role | `lg` | `md` | `sm` |
198
+ | --- | --- | --- | --- |
199
+ | `display` | 57 / 64 | 45 / 52 | 36 / 44 |
200
+ | `headline` | 32 / 40 | 28 / 36 | 24 / 32 |
201
+ | `title` | 22 / 28 | 16 / 24 | 14 / 20 |
202
+ | `body` | 16 / 24 | 14 / 20 | 12 / 16 |
203
+ | `label` | 14 / 20 | 12 / 16 | 11 / 16 |
204
+
205
+ `emphasized` weights: `display` 540, `headline` 560, `title-lg` 580, `title-md` and `title-sm` 680, `body` 520, `label` 700. A variable font such as Inter reads them on its `wght` axis; any other font rounds to the nearest weight it has.
206
+
207
+ **The tokens.** The scale is Tailwind 4 tokens in `styles.css` (`--text-<role>-<size>` with `--line-height`, `--letter-spacing`, `--font-weight` and `--font-weight-em`), so `text-title-md` and `text-title-md-em` are utilities for your own markup, and an app or a zone retunes a step with CSS variables alone (no `defineTheme` key). The [theming guide](../guides/theming.md#type-scale) has the details.
208
+
209
+ ```tsx
210
+ <Typography variant="title-md" emphasized className="[--text-title-md:1.5rem] [--text-title-md--line-height:2rem]">
211
+ Larger inside this element
212
+ </Typography>
213
+ ```
214
+
193
215
  It renders a `<div>` unless `as` or `asChild` says otherwise. Use `as="span"` inside a `<button>` or a link, where a `<div>` is invalid HTML. It sets no `overflow`, so it is never a scroll region: text with break points wraps, while an unbreakable string can overflow its box (add `wrap-anywhere` or `min-w-0`; put your own wrapper with `overflow-auto` and `tabIndex={0}` around it to scroll):
194
216
 
195
217
  ```tsx
196
- <Typography as="span" variant="title-small">Inline</Typography>
197
- <Typography asChild variant="body-medium"><a href="/docs">Docs</a></Typography>
198
- <Typography tone="muted" variant="body-medium">Jan 3 to Jan 9</Typography>
218
+ <Typography as="span" variant="title-sm" emphasized>Inline</Typography>
219
+ <Typography asChild><a href="/docs">Docs</a></Typography>
220
+ <Typography tone="muted">Jan 3 to Jan 9</Typography>
199
221
  ```
200
222
 
223
+ **Deprecated names.** The fifteen former variants are aliases of the scale by name, removed in 4.0. Their **sizes changed** in 3.11 (the weights and line heights follow the scale too):
224
+
225
+ | Former variant | Use | Size before → after |
226
+ | --- | --- | --- |
227
+ | `normal`, `body-medium` | `body-md` | 14 → 14 px (weight 400, which `Typography` now sets itself instead of inheriting its parent's) |
228
+ | `body-small` | `body-sm` | 12 → 12 px |
229
+ | `caption` | `label-sm` with `tone="muted"` | 10 → 11 px |
230
+ | `title-small` | `title-sm` emphasized | 16 → 14 px |
231
+ | `title-medium` | `title-md` emphasized | 18 → 16 px |
232
+ | `title-large` | `title-lg` emphasized | 20 → 22 px |
233
+ | `headline-small` | `headline-sm` emphasized | 24 → 24 px |
234
+ | `headline-medium` | `headline-md` emphasized | 30 → 28 px |
235
+ | `headline-large` | `headline-lg` emphasized | 36 → 32 px |
236
+ | `headline-larger` | `display-sm` emphasized | 48 → 36 px |
237
+ | `huge-small` | `display-sm` emphasized | 60 → 36 px |
238
+ | `huge-medium` | `display-md` emphasized | 72 → 45 px |
239
+ | `huge-large`, `huge-larger` | `display-lg` emphasized | 96 → 57 px |
240
+
241
+ The bare `h1` to `h6` elements follow the scale too: `headline-md`, `headline-sm`, `title-lg`, `title-md`, `title-sm` and `label-md`, at weight 500.
242
+
201
243
  The variants are also exported as `typographyVariants` for use on your own elements.
202
244
 
203
- **Storybook:** `Primitives/Typography` · **Vue:** `Typography`
245
+ **Storybook:** `Primitives/Typography` (`Scale`), `Primitives/Typography/Type scale` (`Emphasized`, `Aliases`, `ZoneOverride`) · **Vue:** `Typography` (no scale)
204
246
 
205
247
  ---
206
248
 
@@ -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,