@nxgt/material 3.10.1 → 3.11.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 (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +4 -4
  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 +5 -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-fqz7csN3.js → components-1GK7YIhY.js} +14685 -14688
  22. package/dist/components.js +23 -23
  23. package/dist/{dialog-CkIYpJXk.js → dialog-BQp5IdFl.js} +147 -147
  24. package/dist/dnd.js +1 -1
  25. package/dist/{dropdown-menu-BC5Tbhb9.js → dropdown-menu-BsH9W_3C.js} +3 -3
  26. package/dist/editor-viewer.js +1 -1
  27. package/dist/editor.js +13 -13
  28. package/dist/empty-state-WaU4-T2c.js +117 -0
  29. package/dist/escape-owner-C4W_sOAU.js +116 -0
  30. package/dist/{field-blur-DPLsMufs.js → field-blur-CJHzVSh9.js} +1 -1
  31. package/dist/{field-blur-context-1ZwJgsHE.js → field-blur-context-Cft9s7Hg.js} +2 -2
  32. package/dist/{hover-card-Cox-tPDz.js → hover-card-C0fw2kLK.js} +72 -71
  33. package/dist/i18n.js +12 -12
  34. package/dist/{icon-badge-q3Ve8gra.js → icon-badge-DUAq9f7H.js} +4 -4
  35. package/dist/icon-button-ifTpXt0A.js +355 -0
  36. package/dist/{illustration-Hj-Q9se9.js → illustration-BmQmvW_4.js} +36 -30
  37. package/dist/illustrations.js +2 -2
  38. package/dist/lib/components/ui/actions/button/button-tones.d.ts +16 -0
  39. package/dist/lib/components/ui/actions/button/button.d.ts +13 -0
  40. package/dist/lib/components/ui/actions/button/button.variants.d.ts +4 -3
  41. package/dist/lib/components/ui/actions/button-group/button-group-context.d.ts +16 -0
  42. package/dist/lib/components/ui/actions/button-group/button-group-reset.d.ts +13 -0
  43. package/dist/lib/components/ui/actions/button-group/button-group.d.ts +19 -2
  44. package/dist/lib/components/ui/actions/icon-button/icon-button-shape.d.ts +3 -2
  45. package/dist/lib/components/ui/actions/icon-button/icon-button.d.ts +18 -4
  46. package/dist/lib/components/ui/actions/icon-button/icon-button.variants.d.ts +34 -4
  47. package/dist/lib/components/ui/feedback/state-frame/state-scene-illustration.d.ts +4 -2
  48. package/dist/lib/components/ui/feedback/typing-indicator/typing-indicator.d.ts +1 -1
  49. package/dist/lib/components/ui/fields/slider/split-slider-props.d.ts +1 -1
  50. package/dist/lib/components/ui/fields/slider/use-slider-control.d.ts +1 -1
  51. package/dist/lib/components/ui/overlays/dialog/dialog-variants.d.ts +1 -1
  52. package/dist/lib/components/ui/primitives/toggle/toggle.d.ts +1 -1
  53. package/dist/lib/components/ui/primitives/typography/typography.d.ts +17 -7
  54. package/dist/lib/components/ui/primitives/typography/typography.variants.d.ts +55 -0
  55. package/dist/lib/components/ui/selection/choice-group/use-choice-group-field.d.ts +2 -2
  56. package/dist/lib/i18n/translator.d.ts +1 -1
  57. package/dist/lib/illustrations/lazy-chunk.d.ts +9 -0
  58. package/dist/lib.js +2 -2
  59. package/dist/{line-path-BQeLQZ-P.js → line-path-C5pZGC49.js} +1 -1
  60. package/dist/main.js +29 -29
  61. package/dist/map.js +9 -9
  62. package/dist/material.css +1 -1
  63. package/dist/pdf-reader.js +1 -1
  64. package/dist/{plated-illustration-B7fcgOED.js → plated-illustration-iNO6KYTL.js} +9 -2
  65. package/dist/print.js +1 -1
  66. package/dist/{public-Cl4dLDHQ.js → public-B0Nq_yYV.js} +524 -524
  67. package/dist/{render-json-DyFYFIkZ.js → render-json-BOhvPnSv.js} +1 -1
  68. package/dist/{rich-code-block-C2Fp_FDU.js → rich-code-block-v7WmeZp9.js} +5 -5
  69. package/dist/{rich-text-viewer-LO1IZCWs.js → rich-text-viewer-W15XIwah.js} +2 -2
  70. package/dist/{skeleton-Bmp4gWAH.js → skeleton-DSdtr2qc.js} +1 -1
  71. package/dist/{sortable-provider-pVaEZzty.js → sortable-provider-CgDTolQR.js} +3 -3
  72. package/dist/{spinner-CsHcmcpF.js → spinner-BtSIYVrC.js} +3 -3
  73. package/dist/{state-scenes-Be9w_3an.js → state-scenes-BX03WmSu.js} +54 -41
  74. package/dist/styles.css +2 -0
  75. package/dist/theme-picker.js +3 -3
  76. package/dist/{theme-toggle-oU-fh6WE.js → theme-toggle-B2Pehdhn.js} +2 -2
  77. package/dist/{toggle-group-j8zaxEg1.js → toggle-group-BMJNcFAC.js} +1 -1
  78. package/dist/{toolbar-items-CqHvlTU8.js → toolbar-items-rUHdVqAq.js} +399 -398
  79. package/dist/{tooltip-rhbqNmr3.js → tooltip-CkttDIab.js} +1 -1
  80. package/dist/{translate-material-CGZljjp1.js → translate-material-Bd_KgfB8.js} +1 -1
  81. package/dist/{translator-DCYO4Ij3.js → translator-Dje8HrNz.js} +5 -5
  82. package/dist/{use-chat-composer-UHlGFSm0.js → use-chat-composer-Bt2ULsv6.js} +4172 -4172
  83. package/dist/{use-material-translation-DkxcLbLM.js → use-material-translation-B3V_bz-5.js} +1 -1
  84. package/dist/{utils-Bcd2Z3ea.js → utils-BLfntITJ.js} +14 -1
  85. package/docs/components/buttons-and-actions.md +64 -15
  86. package/docs/components/navigation-and-layout.md +1 -1
  87. package/docs/components/primitives.md +52 -10
  88. package/docs/guides/illustrations.md +12 -0
  89. package/docs/guides/theming.md +51 -0
  90. package/docs/previews/buttons-actions-button.webp +0 -0
  91. package/docs/previews/buttons-actions-iconbutton.webp +0 -0
  92. package/docs/previews/feedback-status-circularprogress.webp +0 -0
  93. package/docs/previews/feedback-status-hydrateloading.webp +0 -0
  94. package/docs/previews/feedback-status-notifications.webp +0 -0
  95. package/docs/previews/feedback-status-progress.webp +0 -0
  96. package/docs/previews/feedback-status-skeleton.webp +0 -0
  97. package/docs/previews/feedback-status-spinner.webp +0 -0
  98. package/docs/previews/feedback-status-typingindicator.webp +0 -0
  99. package/docs/previews/primitives-typography.webp +0 -0
  100. package/docs/roadmap.md +35 -4
  101. package/docs/troubleshooting.md +71 -2
  102. package/lib/components/base.css +21 -6
  103. package/lib/components/components.css +5 -2
  104. package/lib/components/type-scale.css +194 -0
  105. package/lib/components/ui/actions/button/button.css +156 -0
  106. package/lib/components/ui/actions/button-group/button-group-standard.css +131 -0
  107. package/lib/components/ui/actions/button-group/button-group.css +4 -1
  108. package/lib/components/ui/actions/icon-button/icon-button.css +115 -0
  109. package/lib/components/ui/actions/morph-fill.css +62 -0
  110. package/lib/styles.css +2 -0
  111. package/package.json +1 -1
  112. package/dist/button-DbfdITBB.js +0 -322
  113. package/dist/components/ui/actions/icon-button/icon-button-morph-media.css +0 -65
  114. package/dist/components/ui/actions/icon-button/icon-button-morph.css +0 -152
  115. package/dist/empty-state-BOZWBC5q.js +0 -98
  116. package/dist/escape-owner-CripBce_.js +0 -57
  117. package/dist/icon-button-DJcWpIhu.js +0 -319
  118. package/lib/components/ui/actions/icon-button/icon-button-morph-media.css +0 -65
  119. 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
 
@@ -295,7 +295,7 @@ import { BottomAppBar, IconButton, TopAppBar, Typography } from '@nxgt/material/
295
295
 
296
296
  <TopAppBar>
297
297
  <IconButton variant="ghost" aria-label="Open menu"><Menu /></IconButton>
298
- <Typography variant="title-medium" className="flex-1">Inbox</Typography>
298
+ <Typography variant="title-md" emphasized className="flex-1">Inbox</Typography>
299
299
  <IconButton variant="ghost" aria-label="Search"><Search /></IconButton>
300
300
  </TopAppBar>
301
301
 
@@ -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
 
@@ -41,6 +41,13 @@ with `data-scene`, `data-size` and `data-animated`). A bad icon name or style
41
41
  never crashes the page: outside production it warns once in the console, and
42
42
  the scene's built-in art is drawn instead.
43
43
 
44
+ Two data attributes say what was drawn, for a test or a style:
45
+ `data-plated` is on the `<svg>` once the plate has drawn (the disc that stands
46
+ in while the chunk loads has none), and `data-fallback` is on the built-in art
47
+ that stands in when the scene cannot be drawn (a bad icon, or a chunk that
48
+ failed to load). On an `EmptyState` or `ErrorState` it is on the box
49
+ (`data-slot="illustration"`) or the `<svg>` inside it.
50
+
44
51
  The sprites are read from `/assets/icons/sprites/`, or from the base of the
45
52
  nearest `IconSpritesProvider`. An icon whose name the sprite does not hold is
46
53
  blank: keep to names every style you use has (the suggested ones are the
@@ -51,6 +58,11 @@ arrives, in a box of the same size, so the page does not move. A streaming
51
58
  server render includes the plate; `renderToString` sends the disc and the
52
59
  browser draws the plate once the chunk has loaded.
53
60
 
61
+ If a chunk fails to load (a flaky network, or a tab kept open across a deploy
62
+ whose old hashed files are gone), the illustration draws the built-in art of the
63
+ scene's kind instead of throwing: the page, and the `EmptyState` around it,
64
+ stay. There is no retry; outside production the console warns once.
65
+
54
66
  ## Use them in the states
55
67
 
56
68
  `EmptyState`, `ErrorState` and the `DataTable` empty row draw today's built-in
@@ -35,6 +35,8 @@ at the top are needed is in [getting started](../getting-started.md#styles).
35
35
  `AttributeForm` and `AttributeValuesForm`, and the required-field mark drawn
36
36
  after a `Label` (the first three are dependencies, the rest ship in
37
37
  `lib/components/`);
38
+ - the **type scale**: `--text-<role>-<size>` tokens and the `text-title-md` and
39
+ `text-title-md-em` utilities ([Type scale](#type-scale));
38
40
  - the **tokens**: CSS variables on `:root`, and their dark values on `.dark`
39
41
  and `[data-theme="dark"]`;
40
42
  - an `@theme inline` block that maps each token to a Tailwind colour, so
@@ -664,6 +666,55 @@ does not set it: without it the title uses the body face. Set it on `:root`
664
666
  }
665
667
  ```
666
668
 
669
+ ## Type scale
670
+
671
+ The text sizes are the Material 3 type scale: five roles (`display`, `headline`,
672
+ `title`, `body`, `label`) in three sizes (`lg`, `md`, `sm`), as Tailwind 4
673
+ tokens in `styles.css`. Each step has a size, a line height, a tracking and a
674
+ weight, and an emphasized (heavier) weight:
675
+
676
+ ```css
677
+ --text-title-md: 1rem;
678
+ --text-title-md--line-height: 1.5rem;
679
+ --text-title-md--letter-spacing: 0.0094em;
680
+ --text-title-md--font-weight: 500;
681
+ --text-title-md--font-weight-em: 680;
682
+ ```
683
+
684
+ They are real Tailwind utilities, so your own markup uses the same steps as
685
+ `Typography`, `Button` and the bare headings:
686
+
687
+ ```tsx
688
+ <h2 className="text-headline-sm">Orders</h2>
689
+ <p className="text-body-md">12 orders are waiting.</p>
690
+ <span className="text-title-md-em">Next delivery</span>
691
+ ```
692
+
693
+ `text-title-md` sets size, line height, tracking and weight; `text-title-md-em`
694
+ keeps the first three and takes the emphasized weight. `font-bold`,
695
+ `leading-*` and `tracking-*` still win over them. Retune a step with CSS
696
+ variables, on `:root` for the app or on a zone for one part of the page; there
697
+ is no `defineTheme` key:
698
+
699
+ ```css
700
+ :root {
701
+ --text-body-md: 0.9375rem;
702
+ --text-body-md--line-height: 1.4rem;
703
+ }
704
+
705
+ .dense-table {
706
+ --text-title-md--font-weight-em: 600;
707
+ }
708
+ ```
709
+
710
+ Weights are numeric and read on a variable font's `wght` axis (Inter Variable
711
+ covers 100 to 900); the library sets no `font-variation-settings`, and
712
+ `font-optical-sizing` stays at the browser's `auto`. The bare `h1` to `h6` are
713
+ `headline-md`, `headline-sm`, `title-lg`, `title-md`, `title-sm` and
714
+ `label-md` at weight 500, so a retuned step retunes them too. The
715
+ [Typography component](../components/primitives.md#typography) has the table of
716
+ values and the deprecated names.
717
+
667
718
  ## Overriding tokens
668
719
 
669
720
  Redefine the variables **after** the import, for light and dark separately:
package/docs/roadmap.md CHANGED
@@ -123,6 +123,18 @@ version they shipped in, and planned entries may name the major they target.
123
123
 
124
124
  ## Next
125
125
 
126
+ - **`ButtonGroup` standard look by default, and the old `Typography` names
127
+ removed (4.0)** — `ButtonGroup`'s Material 3 Expressive look (`look="standard"`)
128
+ becomes the default, with `look="legacy"` to keep the original group. The
129
+ fifteen deprecated `Typography` variants (`huge-*`, `headline-larger`,
130
+ `headline-large`... `caption`) are removed in favour of the type scale's
131
+ names, once the other component families read the scale
132
+ ([Typography](components/primitives.md#typography)).
133
+
134
+ - **IconButton: `round` or `square` like Button** — a `shape` of `round` or
135
+ `square` on `IconButton`, with the same press morph, beside the expressive
136
+ shape names it takes today.
137
+
126
138
  - **DataGrid: row selection on phones** — the phone list gets the selection
127
139
  control the grid has with `enableRowSelection`: a checkbox per item and a
128
140
  select-all, with the same `rowSelection` state.
@@ -220,10 +232,11 @@ version they shipped in, and planned entries may name the major they target.
220
232
  - **`CircularProgress` at Material 3's size** — the default `size` becomes
221
233
  Material 3's 40 px (48 px when `wavy`) instead of 100 px. Pass `size` today
222
234
  to keep a size across the change.
223
- - **`Button` on the expressive size scale** — `xs` becomes 28 px (was
224
- 24) and `lg` 44 px (was 40), the scale `SplitButton` and `ToggleGroup`
225
- already follow, so a `Button` and a `SplitButton` of the same size line
226
- up.
235
+ - **`Button` `xs` and `lg` on the `SplitButton` scale** — 3.11 keeps `Button`
236
+ at 24, 32, 36 and 40 px (and adds `xl` 56 and `2xl` 96); `xs` becoming 28 px
237
+ and `lg` 44 px, the scale `SplitButton` and `ToggleGroup` follow, so a
238
+ `Button` and a `SplitButton` of the same size line up, stays a possible
239
+ change for a major.
227
240
  - **A CSP without `style-src-attr 'unsafe-inline'`** — the four form controls'
228
241
  hidden inputs and the `Checkbox` indicator no longer need it;
229
242
  `SelectValue`, the `RadioGroup` root, `Slider` and popper positioning still
@@ -295,6 +308,24 @@ version they shipped in, and planned entries may name the major they target.
295
308
 
296
309
  ## Shipped
297
310
 
311
+ - **Buttons and the type scale in Material 3 Expressive** — `Typography` takes
312
+ the Material 3 scale (`display`, `headline`, `title`, `body`, `label`, each
313
+ `lg`, `md` and `sm`, plus `emphasized`) as Tailwind tokens: `text-title-md`
314
+ and `text-title-md-em` are utilities, and an app or a zone retunes a step
315
+ with CSS variables. The former variants are deprecated aliases (their sizes
316
+ changed) and the bare `h1` to `h6` read the scale. `Button` gains the `xl`
317
+ (56 px) and `2xl` (96 px) sizes, `shape` `round` or `square` with a spring
318
+ press morph, and `selected` (a toggle with an inverted shape and a solid
319
+ fill); every button has a 2 px focus outline, a neutral disabled look and a
320
+ 44 px hit area on a coarse pointer. `IconButton` gets `size` `xs` to `xl`
321
+ and `width` `narrow`, `default` or `wide`, on Button's look and the shared
322
+ toggle fill. `ButtonGroup` gets `look="standard"` (the pressed button widens,
323
+ its neighbours give the width back) and a `size` its buttons share. Nothing
324
+ changes in core. React only; the Vue port is a `vue` issue. 3.11.0.
325
+ ([Buttons & actions](components/buttons-and-actions.md),
326
+ [Typography](components/primitives.md#typography),
327
+ [Type scale](guides/theming.md#type-scale))
328
+
298
329
  - **Icon backgrounds, celestial presets and illustrations** — `Background
299
330
  preset="icons"` draws a pattern of icons from the packs, `iconStyle` taking
300
331
  one style or a list and `iconMotif` choosing the layout.
@@ -86,6 +86,10 @@ words in your error.
86
86
  - [A test of the editor's More panel finds nothing (`[data-editor-more]`)](#a-test-of-the-editors-more-panel-finds-nothing-data-editor-more)
87
87
  - [An editor, a composer or `PdfReader` looks different after upgrading, and fewer tools fit before More](#an-editor-a-composer-or-pdfreader-looks-different-after-upgrading-and-fewer-tools-fit-before-more)
88
88
  - [An `IconButton` with `aria-pressed` now widens and fills when pressed](#an-iconbutton-with-aria-pressed-now-widens-and-fills-when-pressed)
89
+ - [Text got bigger or smaller after upgrading to 3.11 (`Typography` `huge-*`, `headline-*`, `title-*`, `caption`)](#text-got-bigger-or-smaller-after-upgrading-to-311-typography-huge--headline--title--caption)
90
+ - [`Type '"icon"' is not assignable to type '"xs" | "sm" | "default" | "lg" | "xl"'` when spreading Button props into an `IconButton`](#type-icon-is-not-assignable-to-type-xs--sm--default--lg--xl-when-spreading-button-props-into-an-iconbutton)
91
+ - [A pill `IconButton` toggle is the wrong width with a `size-*` class](#a-pill-iconbutton-toggle-is-the-wrong-width-with-a-size--class)
92
+ - [A `Button` with a height class of your own has the wrong corner radius](#a-button-with-a-height-class-of-your-own-has-the-wrong-corner-radius)
89
93
 
90
94
  **Feedback**
91
95
 
@@ -425,6 +429,7 @@ words in your error.
425
429
  **Illustrations**
426
430
 
427
431
  - [`Illustration: … Drawing the built-in art instead.`](#illustration--drawing-the-built-in-art-instead)
432
+ - [`Failed to fetch dynamically imported module` from an empty state or an illustration](#failed-to-fetch-dynamically-imported-module-from-an-empty-state-or-an-illustration)
428
433
 
429
434
  - [Reporting a bug](#reporting-a-bug)
430
435
 
@@ -1241,7 +1246,7 @@ inside the child element is clipped without one.
1241
1246
  **Fix:** update. Then pick, for each long label: leave the ellipsis (give a
1242
1247
  `title` so a sighted user can read it whole; the accessible name already
1243
1248
  is), let it break onto lines with `wrap`, or give the button a width of
1244
- your own through `className`. With `asChild`, put the label in a `<span>`:
1249
+ your own through `className`. With `asChild`, put the label in a `<span>` (on a touch screen in Safari an `asChild` host is not clipped, to keep its 44 px hit area, so only a span gets the ellipsis):
1245
1250
 
1246
1251
  ```tsx
1247
1252
  <Button title="Continue to the payment details">Continue to the payment details</Button>
@@ -1630,7 +1635,7 @@ screen.getByRole('button', { name: 'Bold', pressed: true });
1630
1635
  ### An `IconButton` with `aria-pressed` now widens and fills when pressed
1631
1636
 
1632
1637
  **When:** after upgrading, an icon toggle (an `IconButton` with `aria-pressed`) grows into a filled pill when pressed and pushes its neighbours by 14 px.
1633
- **Fix:** `morph="round"` keeps a filled circle that does not widen. A test that read the quiet pressed item's `::after` bar should read its background instead, or query the state: `getByRole('button', { name: 'Favourite', pressed: true })`.
1638
+ **Fix:** `morph="round"` keeps a filled circle that does not widen (a pill toggle's width follows its `size`: see [the wrong width with a `size-*` class](#a-pill-iconbutton-toggle-is-the-wrong-width-with-a-size--class)). A test that read the quiet pressed item's `::after` bar should read its background instead, or query the state: `getByRole('button', { name: 'Favourite', pressed: true })`.
1634
1639
 
1635
1640
  ```tsx
1636
1641
  <IconButton morph="round" aria-label="Favourite" aria-pressed={on} onClick={toggle}>
@@ -1657,6 +1662,58 @@ A dialog opened from a bar button that is showing returns the focus to that butt
1657
1662
 
1658
1663
  A tool that toggles is a `menuitemcheckbox`, a plain action or a dialog a `menuitem` (a dialog's has `aria-haspopup="dialog"`), the text colour, the highlight and the emoji a `menuitem` that opens their popover at More (a `dialog` named as the tool). The block type, the font, the callout types and the image's upload are submenus; the block type and the font list `menuitemradio` rows, and the submenu's row shows the current one after its name. A folded group of two or more rows is a `group` named by its label; a group of one row lists that row alone, with no group around it; a single-choice group stays a named radio group, even with one row. The alignments are a radio group named "Alignment" of `menuitemradio` rows, the current one checked (`getByRole('menuitemradio', { name: 'Align left' })`), while the bar keeps them as `aria-pressed` buttons. The text and highlight colour pickers are now a `dialog` (the library `Popover`, named as the tool), not a `menu`, in the bar too; a test of `[data-group-id="…"]` waits for its `hidden` attribute, not for it to leave. In the rail the groups are `data-slot="toolbar-group"` (they were `editor-toolbar-group`, which only `layout="wrap"` keeps) and the separators `data-slot="toolbar-separator"`: select a group by `data-group` or `data-group-id`.
1659
1664
 
1665
+ ### Text got bigger or smaller after upgrading to 3.11 (`Typography` `huge-*`, `headline-*`, `title-*`, `caption`)
1666
+
1667
+ **When:** after the upgrade, a heading, a stat or a caption set with a `Typography` `variant` such as `title-medium` (18 to 16 px), `title-large` (20 to 22 px), `headline-large` (36 to 32 px), `huge-large` (96 to 57 px) or `caption` (10 to 11 px) has another size, line height and weight, and so do the bare `h1` to `h6` (`h1` 24 to 28 px, `h2` 20 to 24, `h3` 18 to 22).
1668
+ **Why:** `Typography` moved to the Material 3 type scale. The fifteen former variants are kept, deprecated, as aliases of the scale by name, so the sizes follow the scale; the headings read it too.
1669
+ **Fix:** pick the step you want by its scale name, which also silences the deprecation. The table of former names and sizes is in [Typography](components/primitives.md#typography); to keep an old size in one place, retune the step with CSS variables:
1670
+
1671
+ ```tsx
1672
+ // before
1673
+ <Typography variant="title-medium">Next delivery</Typography>
1674
+ // after: the same step, written in the scale
1675
+ <Typography variant="title-md" emphasized>Next delivery</Typography>
1676
+ ```
1677
+
1678
+ ```css
1679
+ /* or keep 18 px for one zone */
1680
+ .invoice { --text-title-md: 1.125rem; --text-title-md--line-height: 1.75rem; }
1681
+ ```
1682
+
1683
+ ### `Type '"icon"' is not assignable to type '"xs" | "sm" | "default" | "lg" | "xl"'` when spreading Button props into an `IconButton`
1684
+
1685
+ **When:** a wrapper spreads a `Button`'s props (`ComponentProps<typeof Button>`, `ButtonProps`) into an `IconButton` or `IconLinkButton`, and the compiler rejects `size` after the upgrade, or `shape`.
1686
+ **Why:** `IconButton` has its own typed `size` (`xs` to `xl`), which the `Button`'s `icon`, `icon-xs`, `icon-sm`, `icon-lg` and `2xl` are not; and `Button`'s `shape` is `round` or `square` where `IconButton`'s is an expressive shape name.
1687
+ **Fix:** drop `size` and `shape` before spreading, or pass the ones `IconButton` takes:
1688
+
1689
+ ```tsx
1690
+ const { size, shape, ...rest } = props;
1691
+ return <IconButton size="lg" {...rest} />;
1692
+ ```
1693
+
1694
+ ### A pill `IconButton` toggle is the wrong width with a `size-*` class
1695
+
1696
+ **When:** an `IconButton` toggle (`aria-pressed`, `morph="pill"`) given a `size-*` class of your own (`size-12`) is as wide as its `size` says, not as your class does, when pressed.
1697
+ **Why:** the pill's pressed width follows the button's `size` (24 to 56 px, 36 px unset), so it is the same at every size without the old `::before`.
1698
+ **Fix:** give the button a `size` instead of a `size-*` class; use `width="wide"` for a longer resting button.
1699
+
1700
+ ```tsx
1701
+ <IconButton size="xl" aria-pressed={on} aria-label="Favourite">
1702
+ <Star />
1703
+ </IconButton>
1704
+ ```
1705
+
1706
+ ### A `Button` with a height class of your own has the wrong corner radius
1707
+
1708
+ **When:** a `Button` given a height of its own (`className="h-11"`) is a pill whose corners are those of its `size`: a 44 px button with the 18 px radius of a 36 px one, or a squarer pill than expected, and the corners spring toward the wrong size when pressed.
1709
+ **Why:** a round button's radius is half of `--btn-h`, which its `size` sets (36 px without one); a height class changes the box and not that variable.
1710
+ **Fix:** set `--btn-h` with your height class, or use `size`:
1711
+
1712
+ ```tsx
1713
+ <Button className="[--btn-h:44px] h-(--btn-h)">Save</Button>
1714
+ <Button size="xl">Save</Button>
1715
+ ```
1716
+
1660
1717
  ## Feedback
1661
1718
 
1662
1719
  ### An `EmptyState` with no `kind` shows "No Data Found" where it showed "Something Went Wrong"
@@ -7233,6 +7290,18 @@ import { HEALTH_SCENES } from '@nxgt/material/illustrations/health';
7233
7290
  <Illustration scene="patient-record" scenes={HEALTH_SCENES} iconStyle="duotone-light" />;
7234
7291
  ```
7235
7292
 
7293
+ ### `Failed to fetch dynamically imported module` from an empty state or an illustration
7294
+
7295
+ **When:** a page that shows an `EmptyState`, an `ErrorState`, a `DataTable` empty state or an `Illustration` with a scene loses its content to the error boundary, with `Failed to fetch dynamically imported module` (or `error loading dynamically imported module`) naming an illustration chunk. Seen on a flaky network, and in a tab kept open across a deploy whose old hashed chunk is gone. Versions before the release that carries this fix (3.10.0 and earlier).
7296
+ **Why:** the scene and the plate load on demand, and a rejected import was not caught inside the component, so it reached the route's error boundary.
7297
+ **Fix:** upgrade to the release that carries the fix. A failed illustration chunk then draws the built-in art of the state's kind (no retry), and outside production the console warns once with `Illustration: the "…" chunk failed to load`. In a test, `data-fallback` marks the built-in art and `data-plated` marks a drawn plate:
7298
+
7299
+ ```tsx
7300
+ expect(container.querySelector('[data-fallback]')).not.toBeNull();
7301
+ ```
7302
+
7303
+ Before upgrading, precaching the illustration chunks in the service worker narrows the window.
7304
+
7236
7305
  ---
7237
7306
 
7238
7307
  ## Reporting a bug