@mortenbrudvik/waveui 0.15.0 → 0.17.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 (188) hide show
  1. package/README.md +339 -40
  2. package/dist/components/data-display/List.cjs +1 -1
  3. package/dist/components/data-display/List.cjs.map +1 -1
  4. package/dist/components/data-display/List.mjs +405 -219
  5. package/dist/components/data-display/List.mjs.map +1 -1
  6. package/dist/components/data-display/List.registry.cjs +1 -1
  7. package/dist/components/data-display/List.registry.cjs.map +1 -1
  8. package/dist/components/data-display/List.registry.mjs +12 -6
  9. package/dist/components/data-display/List.registry.mjs.map +1 -1
  10. package/dist/components/input/Combobox.cjs +1 -1
  11. package/dist/components/input/Combobox.cjs.map +1 -1
  12. package/dist/components/input/Combobox.mjs +163 -148
  13. package/dist/components/input/Combobox.mjs.map +1 -1
  14. package/dist/components/input/Dropdown.cjs +1 -1
  15. package/dist/components/input/Dropdown.cjs.map +1 -1
  16. package/dist/components/input/Dropdown.mjs +132 -117
  17. package/dist/components/input/Dropdown.mjs.map +1 -1
  18. package/dist/components/input/Label.cjs +1 -1
  19. package/dist/components/input/Label.cjs.map +1 -1
  20. package/dist/components/input/Label.mjs +1 -0
  21. package/dist/components/input/Label.mjs.map +1 -1
  22. package/dist/components/input/Listbox.cjs +1 -1
  23. package/dist/components/input/Listbox.cjs.map +1 -1
  24. package/dist/components/input/Listbox.mjs +90 -78
  25. package/dist/components/input/Listbox.mjs.map +1 -1
  26. package/dist/components/input/Option.cjs +1 -1
  27. package/dist/components/input/Option.cjs.map +1 -1
  28. package/dist/components/input/Option.data.cjs +2 -0
  29. package/dist/components/input/Option.data.cjs.map +1 -0
  30. package/dist/components/input/Option.data.mjs +114 -0
  31. package/dist/components/input/Option.data.mjs.map +1 -0
  32. package/dist/components/input/Option.mjs +21 -17
  33. package/dist/components/input/Option.mjs.map +1 -1
  34. package/dist/components/layout/FlatTree.cjs +2 -0
  35. package/dist/components/layout/FlatTree.cjs.map +1 -0
  36. package/dist/components/layout/FlatTree.context.cjs +2 -0
  37. package/dist/components/layout/FlatTree.context.cjs.map +1 -0
  38. package/dist/components/layout/FlatTree.context.mjs +20 -0
  39. package/dist/components/layout/FlatTree.context.mjs.map +1 -0
  40. package/dist/components/layout/FlatTree.item.cjs +2 -0
  41. package/dist/components/layout/FlatTree.item.cjs.map +1 -0
  42. package/dist/components/layout/FlatTree.item.mjs +90 -0
  43. package/dist/components/layout/FlatTree.item.mjs.map +1 -0
  44. package/dist/components/layout/FlatTree.mjs +276 -0
  45. package/dist/components/layout/FlatTree.mjs.map +1 -0
  46. package/dist/components/layout/Tree.core.cjs +1 -1
  47. package/dist/components/layout/Tree.core.cjs.map +1 -1
  48. package/dist/components/layout/Tree.core.mjs +166 -165
  49. package/dist/components/layout/Tree.core.mjs.map +1 -1
  50. package/dist/components/layout/Tree.selection.cjs +1 -1
  51. package/dist/components/layout/Tree.selection.cjs.map +1 -1
  52. package/dist/components/layout/Tree.selection.mjs +11 -57
  53. package/dist/components/layout/Tree.selection.mjs.map +1 -1
  54. package/dist/components/layout/useFlatTree.cjs +2 -0
  55. package/dist/components/layout/useFlatTree.cjs.map +1 -0
  56. package/dist/components/layout/useFlatTree.mjs +83 -0
  57. package/dist/components/layout/useFlatTree.mjs.map +1 -0
  58. package/dist/components/portal/Portal.cjs +1 -1
  59. package/dist/components/portal/Portal.cjs.map +1 -1
  60. package/dist/components/portal/Portal.mjs +12 -11
  61. package/dist/components/portal/Portal.mjs.map +1 -1
  62. package/dist/components/provider/WaveProvider.cjs +1 -1
  63. package/dist/components/provider/WaveProvider.cjs.map +1 -1
  64. package/dist/components/provider/WaveProvider.mjs +59 -35
  65. package/dist/components/provider/WaveProvider.mjs.map +1 -1
  66. package/dist/components/table/DataGrid.cjs +1 -1
  67. package/dist/components/table/DataGrid.cjs.map +1 -1
  68. package/dist/components/table/DataGrid.context.cjs +2 -0
  69. package/dist/components/table/DataGrid.context.cjs.map +1 -0
  70. package/dist/components/table/DataGrid.context.mjs +74 -0
  71. package/dist/components/table/DataGrid.context.mjs.map +1 -0
  72. package/dist/components/table/DataGrid.header.cjs +2 -0
  73. package/dist/components/table/DataGrid.header.cjs.map +1 -0
  74. package/dist/components/table/DataGrid.header.mjs +168 -0
  75. package/dist/components/table/DataGrid.header.mjs.map +1 -0
  76. package/dist/components/table/DataGrid.mjs +488 -591
  77. package/dist/components/table/DataGrid.mjs.map +1 -1
  78. package/dist/components/table/DataGrid.sizing.cjs +2 -0
  79. package/dist/components/table/DataGrid.sizing.cjs.map +1 -0
  80. package/dist/components/table/DataGrid.sizing.mjs +187 -0
  81. package/dist/components/table/DataGrid.sizing.mjs.map +1 -0
  82. package/dist/components/table/DataGrid.store.cjs +2 -0
  83. package/dist/components/table/DataGrid.store.cjs.map +1 -0
  84. package/dist/components/table/DataGrid.store.mjs +108 -0
  85. package/dist/components/table/DataGrid.store.mjs.map +1 -0
  86. package/dist/components/table/DataGrid.window.cjs +2 -0
  87. package/dist/components/table/DataGrid.window.cjs.map +1 -0
  88. package/dist/components/table/DataGrid.window.mjs +165 -0
  89. package/dist/components/table/DataGrid.window.mjs.map +1 -0
  90. package/dist/components/table/Table.cjs +1 -1
  91. package/dist/components/table/Table.cjs.map +1 -1
  92. package/dist/components/table/Table.mjs +74 -72
  93. package/dist/components/table/Table.mjs.map +1 -1
  94. package/dist/components/table/Table.resize.cjs +2 -0
  95. package/dist/components/table/Table.resize.cjs.map +1 -0
  96. package/dist/components/table/Table.resize.mjs +98 -0
  97. package/dist/components/table/Table.resize.mjs.map +1 -0
  98. package/dist/components/table/useGridNavigation.cjs +1 -1
  99. package/dist/components/table/useGridNavigation.cjs.map +1 -1
  100. package/dist/components/table/useGridNavigation.mjs +343 -140
  101. package/dist/components/table/useGridNavigation.mjs.map +1 -1
  102. package/dist/components/table/useTableColumnSizing.cjs +2 -0
  103. package/dist/components/table/useTableColumnSizing.cjs.map +1 -0
  104. package/dist/components/table/useTableColumnSizing.mjs +76 -0
  105. package/dist/components/table/useTableColumnSizing.mjs.map +1 -0
  106. package/dist/components/table/useTableFeatures.cjs +2 -0
  107. package/dist/components/table/useTableFeatures.cjs.map +1 -0
  108. package/dist/components/table/useTableFeatures.mjs +110 -0
  109. package/dist/components/table/useTableFeatures.mjs.map +1 -0
  110. package/dist/components/table/useTableSelection.cjs +2 -0
  111. package/dist/components/table/useTableSelection.cjs.map +1 -0
  112. package/dist/components/table/useTableSelection.mjs +39 -0
  113. package/dist/components/table/useTableSelection.mjs.map +1 -0
  114. package/dist/components/table/useTableSort.cjs +2 -0
  115. package/dist/components/table/useTableSort.cjs.map +1 -0
  116. package/dist/components/table/useTableSort.mjs +58 -0
  117. package/dist/components/table/useTableSort.mjs.map +1 -0
  118. package/dist/components/typography/Text.cjs +1 -1
  119. package/dist/components/typography/Text.cjs.map +1 -1
  120. package/dist/components/typography/Text.mjs +11 -7
  121. package/dist/components/typography/Text.mjs.map +1 -1
  122. package/dist/hooks/useActiveDescendant.cjs +1 -1
  123. package/dist/hooks/useActiveDescendant.cjs.map +1 -1
  124. package/dist/hooks/useActiveDescendant.mjs +33 -25
  125. package/dist/hooks/useActiveDescendant.mjs.map +1 -1
  126. package/dist/hooks/useListbox.cjs +1 -1
  127. package/dist/hooks/useListbox.cjs.map +1 -1
  128. package/dist/hooks/useListbox.mjs +140 -130
  129. package/dist/hooks/useListbox.mjs.map +1 -1
  130. package/dist/hooks/useRovingTabIndex.cjs +1 -1
  131. package/dist/hooks/useRovingTabIndex.cjs.map +1 -1
  132. package/dist/hooks/useRovingTabIndex.mjs +1 -1
  133. package/dist/hooks/useRovingTabIndex.mjs.map +1 -1
  134. package/dist/hooks/useSelection.cjs +2 -0
  135. package/dist/hooks/useSelection.cjs.map +1 -0
  136. package/dist/hooks/useSelection.mjs +57 -0
  137. package/dist/hooks/useSelection.mjs.map +1 -0
  138. package/dist/hooks/useVirtualWindow.cjs +2 -0
  139. package/dist/hooks/useVirtualWindow.cjs.map +1 -0
  140. package/dist/hooks/useVirtualWindow.mjs +102 -0
  141. package/dist/hooks/useVirtualWindow.mjs.map +1 -0
  142. package/dist/index.cjs +1 -1
  143. package/dist/index.d.cts +1799 -63
  144. package/dist/index.d.ts +1799 -63
  145. package/dist/index.mjs +150 -136
  146. package/dist/lib/brand.cjs +2 -0
  147. package/dist/lib/brand.cjs.map +1 -0
  148. package/dist/lib/brand.mjs +358 -0
  149. package/dist/lib/brand.mjs.map +1 -0
  150. package/dist/lib/classNames.cjs +1 -1
  151. package/dist/lib/classNames.cjs.map +1 -1
  152. package/dist/lib/classNames.mjs +68 -49
  153. package/dist/lib/classNames.mjs.map +1 -1
  154. package/dist/lib/cn.cjs +1 -1
  155. package/dist/lib/cn.cjs.map +1 -1
  156. package/dist/lib/cn.mjs +5 -1
  157. package/dist/lib/cn.mjs.map +1 -1
  158. package/dist/lib/columnSizing.cjs +2 -0
  159. package/dist/lib/columnSizing.cjs.map +1 -0
  160. package/dist/lib/columnSizing.mjs +27 -0
  161. package/dist/lib/columnSizing.mjs.map +1 -0
  162. package/dist/lib/flatTree.cjs +2 -0
  163. package/dist/lib/flatTree.cjs.map +1 -0
  164. package/dist/lib/flatTree.mjs +118 -0
  165. package/dist/lib/flatTree.mjs.map +1 -0
  166. package/dist/lib/selection.cjs +2 -0
  167. package/dist/lib/selection.cjs.map +1 -0
  168. package/dist/lib/selection.mjs +28 -0
  169. package/dist/lib/selection.mjs.map +1 -0
  170. package/dist/lib/tokens.cjs +2 -0
  171. package/dist/lib/tokens.cjs.map +1 -0
  172. package/dist/lib/tokens.mjs +43 -0
  173. package/dist/lib/tokens.mjs.map +1 -0
  174. package/dist/lib/treeCascade.cjs +2 -0
  175. package/dist/lib/treeCascade.cjs.map +1 -0
  176. package/dist/lib/treeCascade.mjs +52 -0
  177. package/dist/lib/treeCascade.mjs.map +1 -0
  178. package/dist/lib/virtual.cjs +2 -0
  179. package/dist/lib/virtual.cjs.map +1 -0
  180. package/dist/lib/virtual.mjs +54 -0
  181. package/dist/lib/virtual.mjs.map +1 -0
  182. package/dist/lib/waveTokens.cjs +2 -0
  183. package/dist/lib/waveTokens.cjs.map +1 -0
  184. package/dist/lib/waveTokens.mjs +24 -0
  185. package/dist/lib/waveTokens.mjs.map +1 -0
  186. package/dist/styles.css +1 -1
  187. package/package.json +3 -1
  188. package/src/styles/tokens.css +210 -10
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @mortenbrudvik/waveui
2
2
 
3
- A React component library inspired by the Fluent UI 2 design language: 79 accessible, composable components built with TypeScript, Tailwind CSS 4 and CSS custom properties, with light, dark and high-contrast themes and right-to-left support.
3
+ A React component library inspired by the Fluent UI 2 design language: 80 accessible, composable components built with TypeScript, Tailwind CSS 4 and CSS custom properties, with light, dark and high-contrast themes and right-to-left support.
4
4
 
5
5
  > This is **not** Microsoft's official `@fluentui/react-components` package.
6
6
 
7
7
  - [Requirements](#requirements) · [Installation](#installation) · [Quick start](#quick-start)
8
8
  - [Styles](#styles) · [Theming](#theming) · [Global effects](#global-effects) · [Styling hooks](#styling-hooks)
9
9
  - [React Server Components](#react-server-components) · [Components](#components) · [Usage notes](#usage-notes)
10
- - [Keyboard support](#keyboard-support) · [Hooks and utilities](#hooks-and-utilities) · [Upgrading from 0.14](#upgrading-from-014) · [Upgrading from 0.13](#upgrading-from-013) · [Upgrading from 0.12](#upgrading-from-012) · [Upgrading from 0.11](#upgrading-from-011) · [Upgrading from 0.10](#upgrading-from-010) · [Upgrading from 0.9](#upgrading-from-09) · [Upgrading from 0.8](#upgrading-from-08) · [Upgrading from 0.7](#upgrading-from-07) · [Upgrading from 0.6](#upgrading-from-06) · [Upgrading from 0.5](#upgrading-from-05) · [Upgrading from 0.4](#upgrading-from-04) · [Development](#development)
10
+ - [Large data](#large-data) · [Keyboard support](#keyboard-support) · [Hooks and utilities](#hooks-and-utilities) · [Upgrading from 0.16](#upgrading-from-016) · [Upgrading from 0.15](#upgrading-from-015) · [Upgrading from 0.14](#upgrading-from-014) · [Upgrading from 0.13](#upgrading-from-013) · [Upgrading from 0.12](#upgrading-from-012) · [Upgrading from 0.11](#upgrading-from-011) · [Upgrading from 0.10](#upgrading-from-010) · [Upgrading from 0.9](#upgrading-from-09) · [Upgrading from 0.8](#upgrading-from-08) · [Upgrading from 0.7](#upgrading-from-07) · [Upgrading from 0.6](#upgrading-from-06) · [Upgrading from 0.5](#upgrading-from-05) · [Upgrading from 0.4](#upgrading-from-04) · [Development](#development)
11
11
 
12
12
  ## Requirements
13
13
 
@@ -117,11 +117,11 @@ export function Shell({ children }: { children: React.ReactNode }) {
117
117
  ```
118
118
 
119
119
  - `theme`: `'light'` (default), `'dark'` or `'high-contrast'`. `dir`: `'ltr'` (default) or `'rtl'`. `portalContainer`: where overlays render (default `document.body`).
120
- - A nested provider inherits every prop it omits (`theme`, `dir`, `portalContainer`) from the enclosing provider, so the light panel above stays right-to-left. `portalContainer={null}` sends a nested subtree's overlays to `document.body`.
120
+ - A nested provider inherits every prop it omits (`theme`, `dir`, `portalContainer`, `motion`, `applyStylesToPortals`; `inputDefaults` and `tokens` merge key by key) from the enclosing provider, so the light panel above stays right-to-left. `portalContainer={null}` sends a nested subtree's overlays to `document.body`.
121
121
  - The provider renders `<div class="wave-root wave-<theme>" dir data-wave-theme>` with `bg-background text-foreground font-wave text-body-1`; a `className` you pass wins.
122
- - Portaled overlays (Dialog, Drawer, Popover, Menu, listboxes, Tooltip, Toaster) render in a `wave-portal` wrapper that carries the provider's theme classes, direction and font.
122
+ - Portaled overlays (Dialog, Drawer, Popover, Menu, listboxes, Tooltip, Toaster) render in a `wave-portal` wrapper that carries the provider's theme classes, direction and font, and its `tokens` overrides unless `applyStylesToPortals={false}` (see [Brand colors and token overrides](#brand-colors-and-token-overrides)).
123
123
  - `inputDefaults`: `{ size?, appearance? }`, the default `size` and `appearance` of the text inputs and pickers below it (see [Sizes and appearances](#sizes-and-appearances)). A nested provider merges its own defined keys over the enclosing provider's, so `<WaveProvider inputDefaults={{ size: 'small' }}>` inside an app with `appearance: 'filled-darker'` gives small filled-darker fields; omitting the prop inherits the enclosing value.
124
- - `useWaveTheme()` returns `{ theme, dir, themeClassName, portalContainer, inputDefaults, hasProvider }` (`inputDefaults` is `{}` outside a provider).
124
+ - `useWaveTheme()` returns `{ theme, dir, themeClassName, portalContainer, inputDefaults, motion, tokens, tokenStyle, applyStylesToPortals, hasProvider }` (outside a provider `inputDefaults` and `tokens` are `{}`, `tokenStyle` is `undefined` and `applyStylesToPortals` is `true`).
125
125
 
126
126
  **Theme classes.** The tokens are selected by `wave-light` (also `:root`), `wave-dark` and `wave-high-contrast`; `getThemeClassName(theme)` returns them. For dark and high contrast the provider also emits the deprecated 0.4 classes `dark` and `high-contrast`, so 0.4 `.dark` selectors in your CSS and class-based `dark:` variants (`@custom-variant dark (&:where(.dark, .dark *));`) keep working. Tailwind 4's default `dark:` variant follows `prefers-color-scheme`, not a class, and Wave does not define a class-based one. Wave's CSS still honours the two legacy classes, but they set only `--wave-*` variables. `color-scheme` is set on the three `wave-*` theme classes only, never on `:root`, so a page without a provider keeps its own scrollbar and form-control scheme.
127
127
 
@@ -164,11 +164,40 @@ export function Shell({ children }: { children: React.ReactNode }) {
164
164
 
165
165
  A server cannot know the setting, so a server-rendered page paints light first and switches to dark after hydration on a dark system. `<meta name="color-scheme" content="light dark">` in the document head at least draws the scrollbar and canvas in the system's scheme from the start (the theme class on `<html>` takes over once it is set).
166
166
 
167
+ ### Brand colors and token overrides
168
+
169
+ A brand color in three lines: `createBrandTokens` builds the 16 brand-ramp tokens from one color, and `WaveProvider tokens` applies them.
170
+
171
+ ```tsx
172
+ import { WaveProvider, createBrandTokens } from '@mortenbrudvik/waveui';
173
+
174
+ const brand = createBrandTokens('#c43e00'); // once, at module scope
175
+ export const App = ({ children }: { children: React.ReactNode }) => <WaveProvider tokens={brand}>{children}</WaveProvider>;
176
+ ```
177
+
178
+ - **Contrast holds.** Each generated stop keeps the luminance of the default ramp's stop and takes its hue and colorfulness from the seed, so every text, fill, stroke and focus-ring pair of the light and dark themes stays above the floors the themes are tested against (4.5:1 for text, 3:1 for strokes and focus rings), whatever the seed. The cost: stop 80 (the light primary) is the seed's hue at the default's lightness, so a light seed gives a darker button. For exact brand colors pass a full ramp (`createBrandTokens({ 10: '#…', …, 160: '#…' })`, a `BrandRamp`), which is returned unchanged and checked in development: each pair the themes read from it that fails its contrast minimum logs a warning. The high-contrast theme ignores the brand.
179
+ - **Any token.** `tokens` takes any token by its camelCase name, the CSS variable without `--wave-` (`primaryForeground`, `brand80`, `paletteRedTint`, `shadow8`, `fontFamilyMonospace`, `zOverlay`); TypeScript refuses a misspelled one. A value is a finite number or a string without `;`, `{` or `}` (anything else is skipped with a development warning, so a value cannot add a declaration of its own to the server-rendered `style`). The provider writes them as inline custom properties on its root (also in the server HTML) and on every portal of its subtree, so its dialogs, menus, popovers, tooltips and toasts match it; `applyStylesToPortals={false}` keeps them off the portals, an enclosing provider's overrides (a brand ramp) included; theme and direction stay.
180
+ - **Nesting.** A nested provider merges its `tokens` over the enclosing ones key by key, also when it sets another `theme`: set a semantic override in the provider that sets the theme it was chosen for. `tokens={null}` clears the inherited overrides for the theme's own tokens and for the overlays, while the theme-independent ones (the ramps, fonts, stacking, motion) still reach the subtree's own elements through the CSS cascade. A nested accent:
181
+
182
+ ```tsx
183
+ <WaveProvider tokens={brand}>
184
+ <AppHeader />
185
+ {/* This panel, and the dialogs and menus it opens, use a crimson primary. */}
186
+ <WaveProvider theme="light" tokens={{ primary: '#c4314b', primaryHover: '#a9283f', primaryPressed: '#7a1c2d' }}>
187
+ <CampaignPanel />
188
+ </WaveProvider>
189
+ </WaveProvider>
190
+ ```
191
+
192
+ - **Tokens in JavaScript.** `waveTokens` holds every token as the `var()` that reads it, typed (`waveTokens.primary` is `'var(--wave-primary)'`), for inline styles and charts; `chartPalette` lists the 12 palette fills in the order a chart gives them to its series. An SVG chart takes the values as they are: `<Bar dataKey="north" fill={waveTokens[chartPalette[0]]} />` is `var(--wave-palette-blue)` and follows the theme. A canvas chart resolves them with `getComputedStyle`; colors never tell series apart on their own, so give every chart a second encoding (a pattern, a marker, a label). The guide's "Charts with WaveUI" chapter has the recipes, and Storybook's `Guides/Charts` runs them with Recharts and Chart.js.
193
+ - **Server Components.** `createBrandTokens`, `getTokenStyle`, `waveTokens` and `chartPalette` are server-safe. `getTokenStyle(overrides)` returns the inline style the provider applies, for an element no provider renders: in a server-rendered layout, `<html style={getTokenStyle(createBrandTokens('#c43e00'))}>` brands the whole page, portals included, since the ramp is theme-independent (a semantic token such as `primary` belongs in `tokens`, because every theme class below `<html>` declares it again).
194
+ - **Strict CSP.** Under a Content Security Policy whose `style-src` blocks inline styles, the server-rendered overrides do not apply and hydration does not apply them again: ship them in a stylesheet there (below), and pass no `tokens` (nor `getTokenStyle`), since the overlays and the subtrees rendered in the client would still take them and leave the page half branded.
195
+
167
196
  ### Customizing tokens
168
197
 
169
198
  Every color is a runtime CSS variable prefixed `--wave-`. The Tailwind color utilities use the same name without the prefix: `--wave-muted-foreground` is `text-muted-foreground`, `--wave-primary` is `bg-primary`, `border-primary`, `ring-primary`, and so on.
170
199
 
171
- Override a token for one theme by targeting its theme selector. With `./styles`, load your CSS after Wave's (the selectors have the same specificity); on the Tailwind path any unlayered rule wins over Wave's `theme` layer.
200
+ Override a token for one theme by targeting its theme selector. With `./styles`, load your CSS after Wave's (the selectors have the same specificity); on the Tailwind path any unlayered rule wins over Wave's `theme` layer. A provider's `tokens` win over these rules on the elements that carry them (an inline style beats a stylesheet rule on the same element).
172
201
 
173
202
  ```css
174
203
  /* All light surfaces */
@@ -183,7 +212,7 @@ Override a token for one theme by targeting its theme selector. With `./styles`,
183
212
  }
184
213
  ```
185
214
 
186
- **Re-branding.** The primary, hover, pressed, focus-ring and selected colors of the light and dark themes are built from the brand ramp (`--wave-brand-10` … `--wave-brand-160`, declared on `:root`), so overriding the ramp re-themes them: light uses brand-80/70/40 for primary/hover/pressed, brand-160 and brand-60 for selected surfaces and text; dark uses brand-110/120/90 and brand-20/110. High contrast uses fixed colors. `--wave-info` is a fixed color and does not follow the ramp. Check the contrast of your brand against `--wave-primary-foreground` (white in light, black in dark).
215
+ **Re-branding.** The primary, hover, pressed, focus-ring and selected colors of the light and dark themes are built from the brand ramp (`--wave-brand-10` … `--wave-brand-160`, declared on `:root`), so overriding the ramp re-themes them: light uses brand-80/70/40 for primary/hover/pressed, brand-80 for the focus ring, brand-160/150/140 for the selected surface and its hover and pressed states and brand-60 for selected text; dark uses brand-110/120/90, brand-100 for the focus ring, brand-20/40/10 and brand-110. High contrast uses fixed colors. `--wave-info` is a fixed color and does not follow the ramp. A ramp written in CSS is not checked: check its contrast against `--wave-primary-foreground` (white in light, black in dark) and the selection pairs, or generate it with `createBrandTokens` (above).
187
216
 
188
217
  ```css
189
218
  :root {
@@ -284,13 +313,18 @@ Generated from `src/styles/tokens.css`. A value followed by a ramp or token name
284
313
 
285
314
  </details>
286
315
 
287
- Theme-independent variables on `:root`: `--wave-brand-10` … `--wave-brand-160` (also utilities such as `bg-brand-80`), `--wave-grey-2` … `--wave-grey-98`, `--wave-font-family` (utility `font-wave`), the stacking layers `--wave-z-overlay` (1000), `--wave-z-toast` (1100) and `--wave-z-tooltip` (1200), and the motion durations `--wave-duration-*` and curves `--wave-curve-*` (see [Enter and exit motion](#enter-and-exit-motion)).
316
+ Each theme also declares, beside the table's tokens:
317
+
318
+ - **The persona palette**: 12 hues (`red`, `pumpkin`, `gold`, `forest`, `teal`, `blue`, `navy`, `lavender`, `purple`, `magenta`, `brown`, `steel`) with five tokens each: `--wave-palette-<hue>` (the strong fill, 3:1 on the page), `-foreground` (its text), `-tint` (a surface: light in light, deep in dark), `-tint-foreground` (text on the tint and on the page) and `-border` (the hue's stroke), the last three Fluent's `Background2`, `Foreground2` and `BorderActive` persona roles. Utilities follow the names on the Tailwind path (`bg-palette-blue-tint`; the precompiled `./styles` holds only the fill and foreground utilities Avatar uses); high contrast paints every fill, tint foreground and border white, and every foreground and tint black. The guide's "Persona Palette" section lists the values.
319
+ - **The shadows** `--wave-shadow-2` … `--wave-shadow-64`, which the `shadow-N` utilities read: Fluent's values, ambient and key opacity 0.12 and 0.14 in light and the stronger 0.24 and 0.28 in dark and high contrast; on a dark page the border still carries most of a raised surface's separation.
320
+
321
+ Theme-independent variables on `:root`: `--wave-brand-10` … `--wave-brand-160` (also utilities such as `bg-brand-80`), `--wave-grey-2` … `--wave-grey-98`, the font families `--wave-font-family`, `--wave-font-family-monospace` and `--wave-font-family-numeric` (utilities `font-wave`, `font-wave-mono` and `font-wave-numeric`; `Text font="monospace"` and `font="numeric"` use the last two), the stacking layers `--wave-z-overlay` (1000), `--wave-z-toast` (1100) and `--wave-z-tooltip` (1200), and the motion durations `--wave-duration-*` and curves `--wave-curve-*` (see [Enter and exit motion](#enter-and-exit-motion)).
288
322
 
289
323
  The three `input-filled-*` tokens (utilities `bg-input-filled-darker`, `bg-input-filled-lighter`, `border-input-filled-stroke`) draw the `filled-darker` and `filled-lighter` appearances of the text inputs and pickers (see [Sizes and appearances](#sizes-and-appearances)); `input-filled-lighter` follows `background`, and the stroke is transparent except in the high-contrast theme.
290
324
 
291
325
  Contrast of every text and non-text pair is checked per theme by `src/styles/__tests__/tokens.test.ts`. One known limit: the `warning` fill (`#fde300` in light) is a background and accent color only; use `warning-tint-foreground` for warning text and icons.
292
326
 
293
- Other Wave theme values: the type ramp `text-caption-2`, `text-caption-1`, `text-body-1`, `text-body-2`, `text-subtitle-2`, `text-subtitle-1`, `text-title-3`, `text-title-2`, `text-title-1`, `text-large-title`, `text-display`; shadows `shadow-2`, `shadow-4`, `shadow-8`, `shadow-16`, `shadow-28`, `shadow-64`; animations `animate-wave-spin`, `animate-wave-spin-slow`, `animate-wave-pulse`, `animate-wave-indeterminate`, `animate-wave-indeterminate-rtl`; on the Tailwind path, the motion utilities `duration-wave-ultra-fast` … `duration-wave-ultra-slow` and `ease-wave-accelerate-max` … `ease-wave-linear`. Border radius uses Tailwind's default scale (Wave no longer overrides it).
327
+ Other Wave theme values: the type ramp `text-caption-2`, `text-caption-1`, `text-body-1`, `text-body-2`, `text-subtitle-2`, `text-subtitle-1`, `text-title-3`, `text-title-2`, `text-title-1`, `text-large-title`, `text-display`; shadows `shadow-2`, `shadow-4`, `shadow-8`, `shadow-16`, `shadow-28`, `shadow-64` (per theme, above); animations `animate-wave-spin`, `animate-wave-spin-slow`, `animate-wave-pulse`, `animate-wave-indeterminate`, `animate-wave-indeterminate-rtl`; on the Tailwind path, the motion utilities `duration-wave-ultra-fast` … `duration-wave-ultra-slow` and `ease-wave-accelerate-max` … `ease-wave-linear`. Spacing, border radius and stroke widths use Tailwind's default scales (Wave defines no tokens for them and no longer overrides the radius); the guide's "Spacing, Radius and Stroke Scales" maps Fluent's `spacing*`, `borderRadius*` and `strokeWidth*` tokens to the classes.
294
328
 
295
329
  ## Global effects
296
330
 
@@ -307,14 +341,14 @@ What adding Wave changes outside its own components:
307
341
  There are no `html` or `body` rules, no global reduced-motion override (each component handles reduced motion itself), and no Preflight unless you import it. The only global rule is Tailwind's `@layer properties` fallback in `./styles`: in browsers without `@property` support (detected by an `@supports` query) it sets the initial values of Tailwind's `--tw-*` custom properties on `*`, `::before`, `::after` and `::backdrop`, and nothing else. It is also the only layered block in that otherwise unlayered file.
308
342
 
309
343
  - **Utility classes (`./styles`).** The precompiled stylesheet contains the Tailwind utilities the components use (`.flex`, `.p-4`, `.bg-primary`, …) as ordinary class selectors, unlayered, plus Tailwind's `@property --tw-*` registrations. An element of yours with the same class name gets the same style. It contains no other utility: generic names that Tailwind would generate from words in Wave's comments and strings (`.container`, `.collapse`, `.table`, `.shadow`, `.ring`, …) are excluded, so they never restyle your own markup.
310
- - **Theme and variant (`./tailwind`).** Wave adds its color names, `font-wave`, the type ramp, the shadow scale, the `animate-wave-*` animations and the `duration-wave-*` and `ease-wave-*` motion tokens to your Tailwind theme, and defines the `wave-rtl:` variant the components use to mirror glyphs (`./variants` defines it alone, for custom setups on `./tokens`). It matches by the element's own direction (`:dir(rtl)`, with a `[dir=rtl]` fallback for browsers without `:dir()`), so unlike Tailwind's `rtl:` it does not apply inside an LTR subtree of an RTL page; your build can use it too. Tailwind's `rtl:` is unchanged.
344
+ - **Theme and variant (`./tailwind`).** Wave adds its color names, the font families `font-wave`, `font-wave-mono` and `font-wave-numeric`, the type ramp, the shadow scale, the `animate-wave-*` animations and the `duration-wave-*` and `ease-wave-*` motion tokens to your Tailwind theme, and defines the `wave-rtl:` variant the components use to mirror glyphs (`./variants` defines it alone, for custom setups on `./tokens`). It matches by the element's own direction (`:dir(rtl)`, with a `[dir=rtl]` fallback for browsers without `:dir()`), so unlike Tailwind's `rtl:` it does not apply inside an LTR subtree of an RTL page; your build can use it too. Tailwind's `rtl:` is unchanged.
311
345
  - The variant writes the direction as `:nth-child(n of :dir(rtl))`, which matches the same elements, so your app's CSS minifier keeps it. Vite's default minifier, Lightning CSS, rewrites a bare `:dir()` to a `:lang()` list for targets below Chrome 120, which never matches a page that sets `dir` without a right-to-left `lang`. Your own `:dir()` rules still go through that rewrite in a Vite build (Tailwind's `rtl:` keeps working through its `[dir=rtl]` alternatives). To keep them, set `build.cssTarget` to browsers with `:dir()` (for example `['chrome120', 'edge120', 'firefox128', 'safari16.4']`), or exclude the feature: `css: { lightningcss: { exclude: Features.DirSelector } }` with `import { Features } from 'lightningcss'`.
312
346
  - postcss-preset-env at its default stage 2 enables `dir-pseudo-class`, which rewrites `:dir()` in a way that breaks Wave's mirroring (the 0.5 form of the variant too). Turn it off with `features: { 'dir-pseudo-class': false }`. Next.js's built-in PostCSS defaults (stage 3) are not affected.
313
347
 
314
348
  **Colliding utility names.** Wave's Tailwind color names are unprefixed, because they are the utility vocabulary 0.4 documented and that consumer code uses in `className` overrides. In a shared Tailwind build, or next to another stylesheet that defines the same class names, each of the following names resolves to a single definition in every utility that takes a color (`bg-`, `text-`, `border-`, `ring-`, `outline-`, `fill-`, `stroke-`, `accent-`, `caret-`, `decoration-`, `divide-`, `placeholder-`, `shadow-`, `from-`/`via-`/`to-`, …):
315
349
 
316
350
  - Shared with shadcn/ui: `background`, `foreground`, `card`, `card-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `muted`, `muted-foreground`, `accent`, `accent-foreground`, `destructive`, `destructive-foreground`, `border`, `input`, `ring`.
317
- - Wave's other names: `primary-hover`, `primary-pressed`, `error`, `error-foreground`, `subtle`, `subtle-hover`, `subtle-pressed`, `subtle-selected`, `selected`, `selected-foreground`, `selected-hover`, `selected-pressed`, `muted-hover`, `muted-pressed`, `stroke`, `stroke-hover`, `stroke-accessible`, `success`, `success-foreground`, `success-tint`, `success-tint-foreground`, `warning`, `warning-foreground`, `warning-tint`, `warning-tint-foreground`, `error-tint`, `error-tint-foreground`, `severe`, `severe-foreground`, `severe-tint`, `severe-tint-foreground`, `info`, `info-foreground`, `info-tint`, `info-tint-foreground`, `inverted`, `inverted-foreground`, `inverted-border`, `input-filled-darker`, `input-filled-lighter`, `input-filled-stroke`, `track`, `skeleton`, `rating`, `presence-available`, `presence-busy`, `presence-away`, `presence-offline`, `presence-oof`, `presence-glyph`, `backdrop`, `brand-10` … `brand-160`.
351
+ - Wave's other names: `primary-hover`, `primary-pressed`, `error`, `error-foreground`, `subtle`, `subtle-hover`, `subtle-pressed`, `subtle-selected`, `selected`, `selected-foreground`, `selected-hover`, `selected-pressed`, `muted-hover`, `muted-pressed`, `stroke`, `stroke-hover`, `stroke-accessible`, `success`, `success-foreground`, `success-tint`, `success-tint-foreground`, `warning`, `warning-foreground`, `warning-tint`, `warning-tint-foreground`, `error-tint`, `error-tint-foreground`, `severe`, `severe-foreground`, `severe-tint`, `severe-tint-foreground`, `info`, `info-foreground`, `info-tint`, `info-tint-foreground`, `inverted`, `inverted-foreground`, `inverted-border`, `input-filled-darker`, `input-filled-lighter`, `input-filled-stroke`, `track`, `skeleton`, `rating`, `presence-available`, `presence-busy`, `presence-away`, `presence-offline`, `presence-oof`, `presence-glyph`, `backdrop`, `brand-10` … `brand-160`, and for each palette hue (`red`, `pumpkin`, … `steel`) `palette-<hue>`, `palette-<hue>-foreground`, `palette-<hue>-tint`, `palette-<hue>-tint-foreground` and `palette-<hue>-border`.
318
352
 
319
353
  Workarounds when your app has its own design system:
320
354
 
@@ -370,10 +404,10 @@ export function checkedRows(tree: HTMLElement) {
370
404
 
371
405
  ## React Server Components
372
406
 
373
- - Every component and hook module of the package starts with `"use client"`. The entry point, `cn`, the slot helpers, the class-name constants (`buttonClassNames`, …), `hsvToHex`, `hexToHsv`, `createToaster`, the date helpers (`addDays`, `addMonths`, `isSameDay`, `startOfWeek`, `getWeekNumber`), `getInitials`, `partitionAvatarGroupItems`, the breadcrumb helpers (`partitionBreadcrumbItems`, `truncateBreadcrumbLongName`, `truncateBreadcrumbLongTooltip`, `isTruncatableBreadcrumbContent`), `createTableColumn` and the types are server-safe, so a Server Component can import from `@mortenbrudvik/waveui`, read the constants, create a toaster handle, compute dates and render Wave components with serializable props. A DataGrid column with functions (`compare`, `renderCell`) is not serializable: define such columns in a client component.
407
+ - Every component and hook module of the package starts with `"use client"`. The entry point, `cn`, the slot helpers, the class-name constants (`buttonClassNames`, …), `hsvToHex`, `hexToHsv`, `createToaster`, the date helpers (`addDays`, `addMonths`, `isSameDay`, `startOfWeek`, `getWeekNumber`), `getInitials`, `partitionAvatarGroupItems`, the breadcrumb helpers (`partitionBreadcrumbItems`, `truncateBreadcrumbLongName`, `truncateBreadcrumbLongTooltip`, `isTruncatableBreadcrumbContent`), `createTableColumn`, the theming helpers (`createBrandTokens`, `getTokenStyle`, `waveTokens`, `chartPalette`, `getThemeClassName`) and the types are server-safe, so a Server Component can import from `@mortenbrudvik/waveui`, read the constants, create a toaster handle, compute dates, build a brand ramp and render Wave components with serializable props (a `tokens` object is one). A DataGrid column with functions (`compare`, `renderCell`) is not serializable: define such columns in a client component.
374
408
  - A Server Component **cannot dot into a client component**: `Card.Header` throws "Cannot access Header on the server". Every sub-component is therefore also exported under a flat name. Use the flat names in Server Components and either form in client components.
375
409
  - Every compound can be composed in a Server Component. Parts written there reach the client as lazy references, and the compounds recognise them, so the server HTML and the client behaviour are the same as in a client file: Accordion items, Carousel slides, the options of Combobox, Dropdown and Listbox, the parts and options of TagPicker, DataGrid parts (`DataGrid.SelectionCell` included), the Table parts, Overflow items and dividers, Breadcrumb items (collapsed into the overflow menu too), the TeachingPopover parts, Drawer and Menu triggers, the Drawer header, body and footer parts, the tags of a TagGroup and the halves of an InteractionTag, the header of a `Menu.Group`, the halves of a `Menu.SplitGroup`, List, TabList and Tree items, Nav items, categories, section headers, dividers and app items, the header, body and footer of a NavDrawer, the stars of a Rating or RatingDisplay, the parts of Toast and MessageBar, the bars of a MessageBarGroup, the ColorPicker parts, `SwatchPicker.Row` and the swatches (`ColorSwatch`, `ImageSwatch`, `EmptySwatch`, in rows of the grid layout or the implicit ones), `Tree.ItemPersonaLayout`, a `Label` or `InfoLabel` passed as the `label` of a Field, and a `Button` passed as a MessageBar, SearchBox or Tag dismiss slot.
376
- - Event handlers and other functions cannot be passed from a Server Component; put interactive parts in a `'use client'` component.
410
+ - Event handlers and other functions cannot be passed from a Server Component; put interactive parts in a `'use client'` component. That includes every windowed or data-driven component: a `virtualWindow` (from the `useVirtualWindow` hook), `renderOption`, a FlatTree's model and `renderItem`, a windowed List's function child and `isSelectionDisabled` are client values. The server HTML of a windowed component holds its first window (the top of the list in the window's `initialViewportSize`), and that of a closed data-mode Combobox or Dropdown no options.
377
411
  - A `Popover`, `Dialog`, overlay `Drawer` or `NavDrawer`, popup `Menu`, `Combobox`, `Dropdown`, `DatePicker`, `TimePicker` or `TagPicker` rendered on the server with `defaultOpen` (or `open`) is closed in the server HTML and opens once hydrated, so no ARIA reference points at a popup that is not there. `onOpenChange` is not called for it.
378
412
  - An inline `Drawer` or `NavDrawer` (`type="inline"`) renders in the server HTML: open, or `hidden` when it is closed and kept mounted (`unmountOnClose={false}`).
379
413
  - A TeachingPopover with a `TeachingPopover.Trigger` written directly inside it starts closed. One that points at its Trigger or a `target` renders its surface once hydrated (the surface sits in a portal). Write `TeachingPopover.Surface` directly inside TeachingPopover too: a Surface rendered by a component of yours is found only once it has mounted, so the server HTML holds the popover's own surface as well (two dialogs with the same title id) until then. A Surface with children of yours is named by its `TeachingPopover.Title` once mounted (as a Dialog is by `Dialog.Title`), so its server HTML has no `aria-labelledby`.
@@ -457,6 +491,7 @@ export function SaveArea() {
457
491
  | `Dialog` | `DialogTrigger`, `DialogContent`, `DialogTitle`, `DialogFooter`, `DialogClose` |
458
492
  | `Drawer` | `DrawerTrigger`, `DrawerTitle`, `DrawerClose`, `DrawerHeader`, `DrawerHeaderTitle`, `DrawerHeaderNavigation`, `DrawerBody`, `DrawerFooter` |
459
493
  | `Dropdown` | `DropdownOption`, `DropdownOptionGroup` |
494
+ | `FlatTree` | `FlatTreeItem` (the row the tree renders; it is not written by hand) |
460
495
  | `InteractionTag` | `InteractionTagPrimary`, `InteractionTagSecondary` |
461
496
  | `List` | `ListItem` |
462
497
  | `Listbox` | `ListboxOption`, `ListboxOptionGroup` (also `Option`, `OptionGroup`) |
@@ -472,7 +507,7 @@ export function SaveArea() {
472
507
  | `Skeleton` | `SkeletonGroup` |
473
508
  | `Stepper` | `StepperStep` |
474
509
  | `SwatchPicker` | `SwatchPickerRow` |
475
- | `Table` | `TableHeader`, `TableHeaderCell`, `TableBody`, `TableRow`, `TableCell`, `TableSelectionCell`, `TableCellLayout`, `TableCellActions` |
510
+ | `Table` | `TableHeader`, `TableHeaderCell`, `TableBody`, `TableRow`, `TableCell`, `TableSelectionCell`, `TableCellLayout`, `TableCellActions`, `TableResizeHandle` |
476
511
  | `TabList` | `TabListTab`, `TabListPanel`, `TabListPanels` |
477
512
  | `TagPicker` | `TagPickerControl`, `TagPickerGroup`, `TagPickerInput`, `TagPickerButton`, `TagPickerList`, `TagPickerOption`, `TagPickerOptionGroup` (also `OptionGroup`) |
478
513
  | `TeachingPopover` | `TeachingPopoverTrigger`, `TeachingPopoverSurface`, `TeachingPopoverHeader`, `TeachingPopoverTitle`, `TeachingPopoverBody`, `TeachingPopoverFooter`, `TeachingPopoverCarousel`, `TeachingPopoverCarouselNav`, `TeachingPopoverCarouselPageCount`, `TeachingPopoverCarouselFooter` |
@@ -482,7 +517,7 @@ export function SaveArea() {
482
517
 
483
518
  ## Components
484
519
 
485
- 79 components, plus the `Portal` and `Presence` utilities. Every component accepts `ref` as a prop (React 19), a `className`, and the native attributes of its element. Storybook (`npm run dev`) shows every component with its props, states and themes.
520
+ 80 components, plus the `Portal` and `Presence` utilities. Every component accepts `ref` as a prop (React 19), a `className`, and the native attributes of its element. Storybook (`npm run dev`) shows every component with its props, states and themes.
486
521
 
487
522
  Your `className` is merged last with `cn()`, so it replaces a conflicting class **of the same variant**: `bg-error` replaces a resting `bg-primary`. A class behind a variant is replaced only by a class with the same variant. That matters for the gated hover and pressed classes (`not-disabled:not-aria-disabled:hover:…` / `…:active:…`) and the state classes (`data-[selected]:…`, `aria-disabled:…`): they stay next to yours and win while their state applies, because they are more specific. A bare `hover:bg-error` therefore does not override a Button's built-in hover color while the Button is enabled. It still paints on hover while the Button is disabled or `aria-disabled`, and on `appearance="transparent"`, which has no hover background. To override the gated and state classes, use the same prefix (`not-disabled:not-aria-disabled:hover:bg-error`, `data-[selected]:bg-error`) or the important modifier (`hover:bg-error!`); see [Buttons](#buttons).
488
523
 
@@ -513,9 +548,9 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
513
548
  | `SearchBox` | Search input with a clear button; `size`, `appearance`, `onValueChange`; input attributes go to the `<input>`. The root draws the field, so `className` and `style` style the field box, and `contentBefore`/`contentAfter` sit beside the text. |
514
549
  | `Slider` | Styled native range input, filled up to the thumb; `size` (`small`, `medium`); `onValueChange(number)`. |
515
550
  | `SpinButton` | Numeric input with step buttons; `size`, `appearance`; typed text is a draft committed on blur or Enter; `min`, `max`, `step`, `largeStep`, `precision`; `displayValue` shows formatted text; `allowEmpty` allows a `null` value; holding a step button repeats it (see [SpinButton](#spinbutton)). |
516
- | `Combobox` | Editable combobox: typing filters the options (`filter` replaces the match, `query` controls the typed text); `multiselect` selects several values; `freeform` makes the typed text the value; a chevron (`expandIcon`) and `clearable`. `Combobox.Option`, `Combobox.OptionGroup` (see [Multi-select pickers](#multi-select-pickers) and [Filtering and async search](#filtering-and-async-search)). |
517
- | `Dropdown` | Select-only combobox (a button) with typeahead; `multiselect` selects several values; `clearable`, `expandIcon` and `renderValue`. `Dropdown.Option`, `Dropdown.OptionGroup`. |
518
- | `Listbox` | A list of options in the page that holds focus itself (`aria-activedescendant`), single-select or `multiselect`, with typeahead, groups and native form support; `Listbox.Option`, `Listbox.OptionGroup`. The listbox primitives under the pickers are public, for pickers of your own (see [Listbox and custom pickers](#listbox-and-custom-pickers)). |
551
+ | `Combobox` | Editable combobox: typing filters the options (`filter` replaces the match, `query` controls the typed text); `multiselect` selects several values; `freeform` makes the typed text the value; a chevron (`expandIcon`) and `clearable`. `Combobox.Option`, `Combobox.OptionGroup`, or the options as data (`items`, `renderOption`) with an optional `virtualWindow` for long lists (see [Multi-select pickers](#multi-select-pickers), [Filtering and async search](#filtering-and-async-search) and [Large data](#large-data)). |
552
+ | `Dropdown` | Select-only combobox (a button) with typeahead; `multiselect` selects several values; `clearable`, `expandIcon` and `renderValue`. `Dropdown.Option`, `Dropdown.OptionGroup`, or the options as data (`items`, `renderOption`) with an optional `virtualWindow` (see [Large data](#large-data)). |
553
+ | `Listbox` | A list of options in the page that holds focus itself (`aria-activedescendant`), single-select or `multiselect`, with typeahead, groups and native form support; `Listbox.Option`, `Listbox.OptionGroup`, or the options as data (`items`, `renderOption`) with an optional `virtualWindow`. The listbox primitives under the pickers are public, for pickers of your own (see [Listbox and custom pickers](#listbox-and-custom-pickers)). |
519
554
  | `TagPicker` | Multi-select combobox that shows the selection as removable tags (Tags in a TagGroup list named "Selected", one Tag size below the picker); `size` (`medium`, `large`, `extra-large`), `appearance`; the input is described by a summary of the selection ("Selected: Apple, Banana"), and additions and removals are announced; `filter` and `query` as in Combobox. A chevron opens the list (`expandIcon`), `clearable` removes every tag, `overflow="count"` keeps the tags on one line with a "+N" count, and `allowCreate` with `onOptionCreate` adds a tag of the user's own text. Composable: `TagPicker.Control`, `.Group`, `.Input` or `.Button` (a trigger without text entry), `.List`, `.Option` (`media`, `secondaryContent`) and `.OptionGroup`; a part without children renders its defaults (see [TagPicker parts and free tagging](#tagpicker-parts-and-free-tagging)). |
520
555
  | `DatePicker` | Date input with a calendar dialog (month and year views, week numbers, marked days, optional "Go to today" and Close buttons); `size`, `appearance`; locale-aware format and parse, `minDate`, `maxDate`, `disabledDates`, `onInvalidInput`. |
521
556
  | `Calendar` | The DatePicker's calendar in the page (`role="group"`): month and year views, week numbers, marked days and custom day content, week, work-week or month ranges, "Go to today", native form support (see [Calendar](#calendar)). |
@@ -547,13 +582,13 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
547
582
  | `Persona` | Avatar with up to four lines of text (`primaryText` to `quaternaryText`), `textPosition` (`after`, `before`, `below`), `presenceOnly` and presence; sizes up to `huge`. |
548
583
  | `Divider` | Horizontal or vertical separator, optionally labelled. |
549
584
  | `Image` | `<img>` with `fit`, `shape`, `shadow`, `bordered`; warns in development when `alt` is missing. Never wider than its parent; with the default `fit` its height follows its width. `fit="none"` keeps the top-left corner in view, `"center"` the middle. |
550
- | `List` | Plain list, selectable listbox (single or multiple) or, with item `action`s, a grid; `navigationMode` (`none`, `items`, `composite`) gives a list without selection arrow keys, or pins the grid; `List.Item onAction`, `disabledSelection` and check marks (`checkmark`); in a selectable or navigable List item values are unique (development warning; see [Lists](#lists)). |
585
+ | `List` | Plain list, selectable listbox (single or multiple) or, with item `action`s, a grid; `navigationMode` (`none`, `items`, `composite`) gives a list without selection arrow keys, or pins the grid; `List.Item onAction`, `disabledSelection` and check marks (`checkmark`); `virtualWindow` with `count` and a function child renders only the items in view of a long list; in a selectable or navigable List item values are unique (development warning; see [Lists](#lists)). |
551
586
 
552
587
  ### Typography
553
588
 
554
589
  | Component | Description |
555
590
  | --------- | --------------------------------------------------------------------------- |
556
- | `Text` | Type-ramp text (`variant`, `weight`); polymorphic `as`; inherits its color. |
591
+ | `Text` | Type-ramp text (`variant`; `weight`: `regular`, `medium`, `semibold`, `bold`; `font`: `base`, `monospace`, `numeric`); polymorphic `as`; inherits its color. |
557
592
 
558
593
  ### Layout
559
594
 
@@ -563,6 +598,7 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
563
598
  | `Accordion` | Disclosure sections (`Accordion.Item`, `.Trigger`, `.Panel`); single (default) or `type="multiple"`; `headingLevel`; panels collapse as they open and close (`Accordion.Panel motion`); item values are unique (development warning). |
564
599
  | `TabList` | Tabs (`TabList.Tab`, `.Panel`, `.Panels`) with automatic activation, or manual with `selectTabOnFocus={false}`; `orientation`, `appearance` (`transparent`, `subtle`, `subtle-circular`, `filled-circular`), `size`, a `Tab` `icon` (alone: icon-only); the selection line slides to the new tab (`motion`); tab values are unique (development warning). |
565
600
  | `Tree` | Hierarchical tree (`Tree.Item`) with expand/collapse and typeahead; single selection, or checkboxes with a cascading mixed state (`selectionMode="multiple"`); row `actions`, `aside` and `Tree.ItemPersonaLayout`; `navigationMode="treegrid"`; groups collapse as they open and close (`motion`) (see [Trees](#trees)). Nested items are `Tree.Item` elements in their parent's children (directly, in Fragments or from a render function), not rendered by a recursive component; values are unique. |
601
+ | `FlatTree` | A tree from flat data (`useFlatTree(items)`: items with a `parentValue`, in any order) for trees of thousands of items: the visible items as sibling `treeitem`s with their level and position, the nested Tree's keys, single selection and cascading checkboxes, and a `virtualWindow` that renders only the items in view (see [Trees](#trees) and [Large data](#large-data)). |
566
602
  | `Carousel` | Slides (`Carousel.Item` children, directly or in Fragments; a component that renders `Carousel.Item` is not a slide) with previous/next, a slide picker and optional auto-rotation with a pause control. `groupSize` (a number or `auto`) shows several at once, `draggable` drags them, `Carousel.Item` `thumbnail` shows thumbnails, `controlsLayout` places the buttons, `Carousel.Button` replaces one, and `motion` (`slide`, `fade` or `null`) with `motionDuration` sets how the page changes. |
567
603
  | `Overflow` | Hides items that do not fit in one row (or column, `orientation="vertical"`) and renders an overflow button (`useOverflowMenu` lists the hidden items); items hide by `priority` unless `pinned`, from the end or the start (`overflowDirection`), in groups with `Overflow.Divider`; `buttonPosition`, `minimumVisible`, `asChild` items, `onOverflowChange`; item ids are unique (development warning). The row has a 4px padding, so its items' focus rings are not clipped (see [Overflow](#overflow)). |
568
604
  | `Grid` | CSS grid with a column count and token gaps. |
@@ -606,16 +642,16 @@ Your `className` is merged last with `cn()`, so it replaces a conflicting class
606
642
 
607
643
  | Component | Description |
608
644
  | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
609
- | `Table` | Static table: `Table.Header`, `Table.HeaderCell` (`sortable`, with your sort state), `Table.Body`, `Table.Row`, `Table.Cell`, and the cell parts `Table.SelectionCell`, `Table.CellLayout` and `Table.CellActions`; `striped`; scrolls horizontally (see [Static tables](#static-tables)). |
610
- | `DataGrid` | Interactive grid (APG grid keyboard model) with row selection and sortable headers; header from children or `columns`, rows from `items` with typed columns (`createTableColumn`) that sort themselves; `selectOnRowClick`, `selectionAppearance`, `subtleSelection`, `DataGrid.SelectionCell`; row ids are unique (development warning; see [Sorting and selecting in a DataGrid](#sorting-and-selecting-in-a-datagrid)). |
645
+ | `Table` | Static table: `Table.Header`, `Table.HeaderCell` (`sortable`, with your sort state), `Table.Body`, `Table.Row`, `Table.Cell`, and the cell parts `Table.SelectionCell`, `Table.CellLayout`, `Table.CellActions` and `Table.ResizeHandle` (a column's resize separator); `striped`; scrolls horizontally; its state can come from the headless `useTableFeatures` with sort, selection and column-sizing plugins (see [Static tables](#static-tables)). |
646
+ | `DataGrid` | Interactive grid (APG grid keyboard model) with row selection and sortable headers; header from children or `columns`, rows from `items` with typed columns (`createTableColumn`) that sort themselves; with `items`, selection and select-all follow the data (`isSelectionDisabled`); `selectOnRowClick`, `selectionAppearance`, `subtleSelection`, `DataGrid.SelectionCell`; `resizableColumns` and column widths (`columnSizingOptions`, `columnWidths`); `virtualWindow` renders only the rows in view; row ids are unique (development warning; see [Sorting and selecting in a DataGrid](#sorting-and-selecting-in-a-datagrid) and [Large data](#large-data)). |
611
647
 
612
648
  ### Provider
613
649
 
614
650
  | Component | Description |
615
651
  | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
616
- | `WaveProvider` | Theme (`light`, `dark`, `high-contrast`), direction, portal container and `inputDefaults` (the default `size` and `appearance` of the text inputs and pickers) and `motion` (`'none'` turns the default motion of every animated component below it off). |
652
+ | `WaveProvider` | Theme (`light`, `dark`, `high-contrast`), direction, portal container and `inputDefaults` (the default `size` and `appearance` of the text inputs and pickers) and `motion` (`'none'` turns the default motion of every animated component below it off); `tokens` overrides design tokens in the subtree and its overlays, and `applyStylesToPortals={false}` keeps them off the overlays (see [Brand colors and token overrides](#brand-colors-and-token-overrides)). |
617
653
 
618
- `Portal` renders its children into `document.body` (or the provider's `portalContainer`) inside a themed `wave-portal` wrapper; use it for your own overlays.
654
+ `Portal` renders its children into `document.body` (or the provider's `portalContainer`) inside a themed `wave-portal` wrapper, which also carries the provider's token overrides; use it for your own overlays.
619
655
 
620
656
  ### Motion
621
657
 
@@ -1087,10 +1123,11 @@ export function FruitList() {
1087
1123
  - **Focus and selection.** One Tab stop. The active option exists only while the list has focus: the selected option (with `multiselect`, the first selected one in list order), else the first; while no option can be active, the list draws its own focus ring. Space and Enter select, so the selection does not follow the active option; a single-select Listbox never deselects. A press on an option focuses the list and makes that option active without scrolling, so the click selects it even while the selected option is scrolled out of view. Tab, Escape and the Alt+arrow keys are left to the page: an enclosing Dialog closes on Escape.
1088
1124
  - **Props.** `multiselect` (an array value, announced toggles; see [Multi-select pickers](#multi-select-pickers)), `onActiveOptionChange` (`null` when focus leaves the list), `disabled` (out of the Tab order, selects and submits nothing), `disabledOptionsFocusable`, `autoFocus`, `name`, `form` and `required`, and Field wiring. It has no built-in height: give it one through `className` and it scrolls.
1089
1125
  - **Option groups.** `OptionGroup`'s `label` is optional and takes any text content (an icon next to the text is fine). Without one no heading renders, and `aria-label` or `aria-labelledby` names the group; these two always land on the `role="group"` list. A group without a name warns in development.
1126
+ - **Data mode** (Listbox, Combobox, Dropdown): `items` (`ListboxItem`: `value`, `label`, and optionally `textValue`, `disabled` and `hidden`; values unique) replace the option children, which are then neither read nor rendered (passing both warns once). The keys, typeahead, the selected labels and the Combobox filter come from `items`, also for a selected item whose option is not rendered, and every option carries `aria-posinset` and `aria-setsize` among the shown options. `renderOption(item, state)` draws an option (`state`: `selected`, `active`, and `index` among the shown options; `ListboxOptionRenderState`): return an `Option`, or an option of yours on `useListboxOption`, with the item's `value`; it is called again only when its item, state or position, or the function, change, so pass a stable function. A closed Combobox or Dropdown in data mode renders no options (its labels come from `items`). `virtualWindow` windows a long data-mode list (see [Large data](#large-data)).
1090
1127
 
1091
1128
  **Pickers of your own.** The pieces under Dropdown, Combobox and TagPicker are public:
1092
1129
 
1093
- - `useListbox(options)` holds the listbox: its keys, typeahead, filtering, the active option and the registration of the options. `mode` is `'select-only'` (a button combobox, as Dropdown), `'editable'` (an `<input>` combobox, as Combobox) or `'standalone'` (a list that holds focus, as Listbox). You own the open state (`open`, `onOpenChange(open, details)`) and the value (`selectedValues`, `onSelect(value, details)`, with `details.item` and `details.event`); spread `getComboboxProps()` on your combobox and attach `onKeyDown` and `onKeyUp`.
1130
+ - `useListbox(options)` holds the listbox: its keys, typeahead, filtering, the active option and the registration of the options. `mode` is `'select-only'` (a button combobox, as Dropdown), `'editable'` (an `<input>` combobox, as Combobox) or `'standalone'` (a list that holds focus, as Listbox). You own the open state (`open`, `onOpenChange(open, details)`) and the value (`selectedValues`, `onSelect(value, details)`, with `details.item` and `details.event`); spread `getComboboxProps()` on your combobox and attach `onKeyDown` and `onKeyUp`. With `items` (data mode) it reads no children, so you may render only the shown options (`listbox.items`), or a window of them: `onScrollToItem(index)` is then called first for every move that scrolls the active option into view, with its index among the shown options, so pass `(index) => virtualWindow.scrollToIndex(index, { align: 'auto' })` and keep the active option rendered.
1094
1131
  - `useListboxPopup(options)` dismisses the popup (Escape, a press outside, focus leaving: `DismissReason`) and positions it, and `ListboxSurface` renders the list: in a portaled surface while it is shown, `hidden` in place otherwise, so the options register from the first render. `listClassName` and `surfaceClassName` are merged last (`max-h-*` replaces the list's height). Two duties remain yours: set your combobox's `aria-expanded` from `expanded` after spreading `getComboboxProps()`, and while the listbox is open with nothing to show, close it on Escape yourself (outside an IME composition) without calling `listbox.onKeyDown`, so the Escape reaches an enclosing layer.
1095
1132
  - `ListboxProvider` provides the listbox's context to its options when you render the list yourself instead of through `ListboxSurface`. `ListboxSurface`'s `listProps` (`ListboxSurfaceListProps`) pass attributes and a `ref` to the list element; the listbox's own `aria-multiselectable` and `aria-activedescendant` always win.
1096
1133
  - Options are `Option` and `OptionGroup` (or `TagPicker.Option`, an Option with `media` and `secondaryContent`), or components of your own on `useListboxOption`, marked with `markListboxElement` so that `collectOptionLabels(children)` knows their labels before they register (in the server HTML and the first render).
@@ -2048,7 +2085,7 @@ export function Notices({ notices, onDismiss }: { notices: Notice[]; onDismiss:
2048
2085
 
2049
2086
  - **Sizes and shape.** `size` takes the named sizes or a box in pixels (16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 96, 120, 128); `shape="square"` rounds the corners. An Avatar in a `Tag` with `shape="rounded"` is square.
2050
2087
  - **Initials** come from `getInitials(name, { dir, firstInitialOnly })`: two letters for a full name, one for a single name, brackets and punctuation dropped, a hyphen or apostrophe joins words. A name with digits only, emoji or a script without letter initials (Han, Kana, Hangul, Thai, Lao, Khmer, Myanmar, Tibetan) shows none, so an icon or the `initials` slot is the better content. `initials` replaces the text. On a right-to-left page the two letters of a left-to-right name swap; Hebrew and Arabic names keep their order.
2051
- - **Color.** `color` is `neutral` (the default), `brand`, `colorful` or one of `red`, `pumpkin`, `gold`, `forest`, `teal`, `blue`, `navy`, `lavender`, `purple`, `magenta`, `brown`, `steel`. `colorful` picks a named color from `idForColor`, else from `name`: the same input is always the same color, so a person keeps their color across pages. The palette is the `--wave-palette-<name>` and `--wave-palette-<name>-foreground` tokens; high contrast paints a white fill with black text.
2088
+ - **Color.** `color` is `neutral` (the default), `brand`, `colorful` or one of `red`, `pumpkin`, `gold`, `forest`, `teal`, `blue`, `navy`, `lavender`, `purple`, `magenta`, `brown`, `steel`. `colorful` picks a named color from `idForColor`, else from `name`: the same input is always the same color, so a person keeps their color across pages. The palette is the `--wave-palette-<name>` and `--wave-palette-<name>-foreground` tokens; high contrast paints a white fill with black text. Each hue also has a tint, a tint foreground and a border (`--wave-palette-<name>-tint`, `-tint-foreground`, `-border`) for tags, badges and charts of your own; Avatar does not use them.
2052
2089
  - **`active`** is `active` (the `activeAppearance` around the avatar: `ring`, `ring-shadow`, `shadow` or `glow`), `inactive` (dimmed) or `unset`.
2053
2090
  - **Presence.** `PresenceBadge` `status` is `available`, `busy`, `away`, `offline`, `do-not-disturb` or `out-of-office` (Fluent's names); `dnd` and `oof` still work and log a deprecation warning. Inside an Avatar the badge takes the size that fits the avatar.
2054
2091
  - **`Persona`** shows `primaryText` (default: `name`), then `secondaryText`, `tertiaryText` and `quaternaryText`, after, before or below the avatar (`textPosition`; below, the text is centered unless `textAlignment="start"`). `presenceOnly` swaps the avatar for the presence badge. A Persona without a `name` or text needs an `aria-label`.
@@ -2190,10 +2227,41 @@ const rows = React.useMemo(() => {
2190
2227
  // sort={sort} onSortChange={setSort}> <DataGrid.Body>{rows.map(…)}</DataGrid.Body> </DataGrid>
2191
2228
  ```
2192
2229
 
2193
- - **Row selection.** `selectionMode` (`single`, `multiple`) with `selectedItems`, `defaultSelectedItems` and `onSelectedItemsChange(selectedItems, details)`. With `selectionMode="single"`, pass at most one id (with several, the first one whose row is rendered is selected, with a development warning). Each row's selection control is named after the row's first `DataGrid.Cell`, `<td>` or `<th>` child (Fragments are looked into); a cell rendered by another component is not seen, so pass `selectionLabel` to `DataGrid.Row` then.
2230
+ - **Row selection.** `selectionMode` (`single`, `multiple`) with `selectedItems`, `defaultSelectedItems` and `onSelectedItemsChange(selectedItems, details)`. With `selectionMode="single"`, pass at most one id (with several, the first one is selected, or in a grid without `items` the first one whose row is rendered, with a development warning). Each row's selection control is named after the row's first `DataGrid.Cell`, `<td>` or `<th>` child (Fragments are looked into); a cell rendered by another component is not seen, so pass `selectionLabel` to `DataGrid.Row` then.
2194
2231
  - **Selecting on a row click.** `selectOnRowClick` toggles a row when it is clicked (in single mode it selects the row alone, or clears it). The row's `onClick` runs first, and its `preventDefault()` keeps the selection. A click on a link, a button, an input or another interactive element in the row, a click from a popup opened from the row (a Menu in a cell), and a click that ends a text selection never toggle; Space on a cell toggles either way.
2195
2232
  - **Looks.** `selectionAppearance` fills a selected row with the selected token (`brand`, the default), the subtle one (`neutral`) or not at all (`none`; the control, `aria-selected` and the forced-colors outline still show it). `subtleSelection` draws an unchecked row control only while its row is hovered or holds focus, and the select-all while the grid is (always in forced colors and on touch screens); the controls stay focusable and named.
2196
- - **Per row.** `DataGrid.Row selectionCellProps` takes `disabled` (a natively disabled control: row clicks, Space and select-all skip the row, and select-all counts the enabled rows only), `invisible` (no visible control; the row still selects), `className` and `aria-label`. `DataGrid.SelectionCell` places the selection cell where you write it in a row (the select-all in a header row), instead of the automatic first cell.
2233
+ - **Selection follows the data** in a grid with `items` (0.16): select-all selects every item whose selection is not disabled, rendered or not, and its checkbox shows their state; when all of them are selected it removes only their ids. A row click, Space and a row's control change one id, and the ids of rows that are not rendered (filtered out, on another page, outside a `virtualWindow`, restored by id) stay selected and are reported by `onSelectedItemsChange`, until your code removes them. Select-all reports the ids outside `items` first, then the selected ids in `items` order. Both use the ids from `getRowId`: a row that sets its own, different `rowId` is outside them (a development warning says so). A grid of `DataGrid.Row` children keeps the 0.15 rule: ids of rows that are not rendered drop out at the next change.
2234
+ - **Per row.** `DataGrid.Row selectionCellProps` takes `disabled` (a natively disabled control: row clicks, Space and select-all skip the row, and select-all counts the enabled rows only), `invisible` (no visible control; the row still selects), `className` and `aria-label`. `DataGrid.SelectionCell` places the selection cell where you write it in a row (the select-all in a header row), instead of the automatic first cell. In a grid with `items`, `isSelectionDisabled(item, index)` disables items whether their rows are rendered or not (select-all skips them and keeps their state) and is the default `disabled` of each rendered row's selection cell; a cell's own `disabled` wins for its control, and a value that disagrees warns in development. Define it outside the component or memoise it: it is called for every item when `items`, `getRowId` or the function change.
2235
+ - **Resizable columns.** With `columns` and `items`, `resizableColumns` puts a resize handle (a focusable `role="separator"`) at the end edge of each column's header cell: drag it with a mouse, a finger or a pen (mirrored in right-to-left grids), or press F2 on the header cell (or its sort button) to move focus to it, then the arrow keys (20 pixels, 5 with Shift), Home (the minimum width), and Enter, Space or Escape to return; Enter on a sortable header still sorts. A double click sets a resized column back to its initial width. `columnSizingOptions` gives each column a `minWidth` (80 by default) and its starting width (`defaultWidth`, else `idealWidth`, else 150), and `columnWidths` or `defaultColumnWidths` with `onColumnWidthsChange(widths, details)` hold the widths (every column's, by `columnId`), for instance to persist them. Any of these props lays the columns out from widths (a `<colgroup>`, a fixed table layout, cells truncated with an ellipsis), with or without handles. An unresized grid fills its container (at least as wide as its columns); after the first change, or with `columnWidths` or `defaultColumnWidths`, it is exactly as wide as its columns, so the dragged edge follows the pointer and slack shows after the last column. There is no auto-fit and no maximum width. `labels.resizeColumn` and `labels.columnWidth(width)` name the handles and their value ("Resize column", "150 pixels").
2236
+
2237
+ ```tsx
2238
+ import * as React from 'react';
2239
+ import { DataGrid } from '@mortenbrudvik/waveui';
2240
+
2241
+ // Person, columns and personId as in the first example.
2242
+ const SIZING = { name: { defaultWidth: 240, minWidth: 120 }, role: { idealWidth: 160 } };
2243
+
2244
+ export function ResizablePeople({ people }: { people: Person[] }) {
2245
+ const [widths, setWidths] = React.useState<Record<string, number>>(() =>
2246
+ JSON.parse(localStorage.getItem('people-widths') ?? '{}'),
2247
+ );
2248
+ return (
2249
+ <DataGrid
2250
+ aria-label="People"
2251
+ items={people}
2252
+ columns={columns}
2253
+ getRowId={personId}
2254
+ resizableColumns
2255
+ columnSizingOptions={SIZING}
2256
+ columnWidths={widths}
2257
+ onColumnWidthsChange={(next) => {
2258
+ setWidths(next);
2259
+ localStorage.setItem('people-widths', JSON.stringify(next));
2260
+ }}
2261
+ />
2262
+ );
2263
+ }
2264
+ ```
2197
2265
  - **Typing a wrapper.** A generic wrapper `function AppGrid<T>(props: DataGridProps<T>)` compiles and forwards every form, but an inline `onSortChange` passed to it next to `items` gets no contextual type: annotate its parameters, or type the wrapper's props as `DataGridBaseProps<T> & DataGridItemsSortProps<T>`.
2198
2266
 
2199
2267
  ### Static tables
@@ -2268,6 +2336,88 @@ export function Members({ members }: { members: Member[] }) {
2268
2336
  - **Cell layout.** `Table.CellLayout` puts `media` (an icon, or an Avatar, which gets a 32px box) before the main text and a `description` under it; `truncate` keeps each line on one line where the column's width is bounded, and `appearance="primary"` marks the cell that names the row.
2269
2337
  - **Cell actions.** `Table.CellActions` covers the end of its cell (the cell is its positioning box) and shows its buttons while the row is hovered or holds focus, always in forced colors and on touch screens; `visible` keeps them (while a menu opened from one is open). Its background follows the library's own row fills: none, the row hover, a `striped` table's stripe and a DataGrid's selected rows. A row background of your own, or a table on another surface (a Card), needs the same background class on the actions, and the actions must be direct children of the cell for the stripe and selection fills to match.
2270
2338
 
2339
+ **State from hooks.** `useTableFeatures` holds a table's rows and its plugins hold the sort, the selection and the column widths, by row id and over every item, so a table that renders only a page or a window of its rows keeps select-all and the sort over all of them (Fluent's call shape; the `Components/Table/Features` story is the full version):
2340
+
2341
+ ```tsx
2342
+ import {
2343
+ Table,
2344
+ createTableColumn,
2345
+ useTableColumnSizing,
2346
+ useTableFeatures,
2347
+ useTableSelection,
2348
+ useTableSort,
2349
+ } from '@mortenbrudvik/waveui';
2350
+
2351
+ type Member = { id: string; name: string; role: string };
2352
+
2353
+ // Outside the component, as the grid's columns: the rows keep their array while these stay.
2354
+ const columns = [
2355
+ createTableColumn<Member>({ columnId: 'name', label: 'Name', compare: (a, b) => a.name.localeCompare(b.name) }),
2356
+ createTableColumn<Member>({ columnId: 'role', label: 'Role', compare: (a, b) => a.role.localeCompare(b.role) }),
2357
+ ];
2358
+ const memberId = (member: Member) => member.id;
2359
+ const sizing = { name: { defaultWidth: 240, minWidth: 120 } };
2360
+
2361
+ export function Members({ members }: { members: Member[] }) {
2362
+ const { getRows, sort, selection, columnSizing } = useTableFeatures(
2363
+ { items: members, columns, getRowId: memberId },
2364
+ [
2365
+ useTableSort({ defaultSort: { columnId: 'name', direction: 'ascending' } }),
2366
+ useTableSelection({ selectionMode: 'multiple' }),
2367
+ useTableColumnSizing({ columnSizingOptions: sizing }),
2368
+ ],
2369
+ );
2370
+ return (
2371
+ <Table aria-label="Members" {...columnSizing.getTableProps()}>
2372
+ <colgroup>
2373
+ <col style={{ width: 40 }} />
2374
+ <col {...columnSizing.getColumnProps('name')} />
2375
+ <col {...columnSizing.getColumnProps('role')} />
2376
+ </colgroup>
2377
+ <Table.Header>
2378
+ <Table.Row>
2379
+ <Table.SelectionCell
2380
+ aria-label="Select all members"
2381
+ checked={selection.allRowsSelected ? true : selection.someRowsSelected ? 'mixed' : false}
2382
+ onCheckedChange={(_checked, details) => selection.toggleAllRows(details?.event)}
2383
+ />
2384
+ {(['name', 'role'] as const).map((columnId) => (
2385
+ <Table.HeaderCell
2386
+ key={columnId}
2387
+ className="relative"
2388
+ sortable
2389
+ sortDirection={sort.getSortDirection(columnId)}
2390
+ onSortClick={(event) => sort.toggleColumnSort(columnId, event.nativeEvent)}
2391
+ aside={<Table.ResizeHandle {...columnSizing.getResizeHandleProps(columnId)} />}
2392
+ >
2393
+ {columnId === 'name' ? 'Name' : 'Role'}
2394
+ </Table.HeaderCell>
2395
+ ))}
2396
+ </Table.Row>
2397
+ </Table.Header>
2398
+ <Table.Body>
2399
+ {getRows().map(({ item, rowId }) => (
2400
+ <Table.Row key={rowId}>
2401
+ <Table.SelectionCell
2402
+ aria-label={`Select ${item.name}`}
2403
+ checked={selection.isRowSelected(rowId)}
2404
+ onCheckedChange={(_checked, details) => selection.toggleRow(rowId, details?.event)}
2405
+ />
2406
+ <Table.Cell>{item.name}</Table.Cell>
2407
+ <Table.Cell>{item.role}</Table.Cell>
2408
+ </Table.Row>
2409
+ ))}
2410
+ </Table.Body>
2411
+ </Table>
2412
+ );
2413
+ }
2414
+ ```
2415
+
2416
+ - **Plugins.** Call each plugin hook unconditionally in the array (or before it) and pass the same plugins on every render; a slot without its plugin holds inert values, and the order of the array does not matter (a feature given twice uses its last plugin, with a development warning). Options may be written inline: a plugin whose options change identity only makes the state be computed again, while `getRows()` keeps its array until `items`, `getRowId`, `columns` or the sort change.
2417
+ - **Sort.** `useTableSort` takes DataGrid's sort shape (`sort`, `defaultSort`, `onSortChange(sort, details)`); `getRows()` is a stable sort of a copy of `items` by the sorted column's `compare` (descending negates it), and `sort` gives `value`, `setColumnSort`, `toggleColumnSort` and `getSortDirection`.
2418
+ - **Selection.** `useTableSelection` takes DataGrid's selection props (`selectionMode`, `selectedItems`, `defaultSelectedItems`, `onSelectedItemsChange(selectedItems, details)`, row ids) and `isDisabled(item, index)`. `allRowsSelected` and `someRowsSelected` count every selectable item of `items`, rendered or not; `toggleAllRows` selects them all, or removes only their ids (a selected disabled item and ids outside `items` keep their state), and `clearRows` empties the selection. For a selection without a table, `useSelection` holds the same id set (`toggle`, `select`, `deselect`, `toggleAll(universe)`, `clear`).
2419
+ - **Column widths.** `useTableColumnSizing` takes DataGrid's sizing props (`columnSizingOptions`, `columnWidths`, `defaultColumnWidths`, `onColumnWidthsChange(widths, details)`); spread `getTableProps()` on the table (a fixed layout and its width), `getColumnProps(columnId)` on each `<col>` and `getResizeHandleProps(columnId)` on a `Table.ResizeHandle`, which the `aside` of a positioned header cell (`relative`) holds. The handle is a Tab stop in a static table: Tab to it, then the arrow keys (20 pixels, 5 with Shift), Home and End (the minimum and, with `maxWidth`, the maximum); a double click sets a resized column back to its initial width. It keeps no width of its own: without the hook, pass `width`, `minWidth` and `onResize(width, details)` from your state. `label` ("Resize column") and `valueText(width)` ("N pixels") name it and its value.
2420
+
2271
2421
  ### Lists
2272
2422
 
2273
2423
  A `List` without selection is a plain list. `navigationMode="items"` makes its items one Tab stop with the arrow keys, Home, End and typeahead, and `onAction` gives an item an action of its own:
@@ -2293,6 +2443,7 @@ export function RecentFiles({ open }: { open: (file: string) => void }) {
2293
2443
  - **`onAction`** runs on a click and on Enter, and on Space where the item cannot be selected (a list without selection, an item without `value`); in a selectable item Space only toggles. It runs after the item's `onClick` and before the selection toggles: `event.preventDefault()` keeps the selection. Clicks and keys in the item's actions never run it. In a list that does not navigate its items it runs on clicks only, with a development warning.
2294
2444
  - **`disabledSelection`** keeps an item's selection as it is (click, Enter and Space) while the item stays focusable and in the arrow order; without `onAction` it is also `aria-disabled`.
2295
2445
  - **Check marks.** `checkmark` on a selectable List draws a decorative check at the start of each selected item; `List.Item checkmark` replaces it with your glyph or hides it (`false`).
2446
+ - **Long lists.** `virtualWindow` with `count` and a function child `(index) => <List.Item …/>` renders only the items in view (see [Large data](#large-data)); the keys then move by index over the whole list, and selection keeps the values of items that are not rendered.
2296
2447
 
2297
2448
  ### Trees
2298
2449
 
@@ -2341,6 +2492,37 @@ export function FileTree() {
2341
2492
  - **Persona rows.** `Tree.ItemPersonaLayout` (flat `TreeItemPersonaLayout`, Fluent's name) lays out an item's label as a person: `media` (an Avatar of size 32 fits a row), the name as children and a `description` under it; the item is named by the name and described by the description.
2342
2493
  - **Motion.** A child group opens and closes the space it takes while it fades, over 200ms both ways, in an element of its own around the `role="group"` element (`wave-tree-item__collapse`, `display: contents` while open); a nested group goes with its parent. The items of a closing group leave the arrow order and typeahead at once, and focus inside it moves to the item that collapsed. `Tree motion` replaces the motion of every group, or turns it off with `null`; the chevron's turn is not part of it. A pointer click that falls through an exiting group onto its item (an `inert` element lets the pointer through) does nothing; Enter and a screen reader's click on the item still activate it.
2343
2494
 
2495
+ **Flat trees.** `FlatTree` builds a tree from flat data, for trees of thousands of items: `useFlatTree(items, options)` takes items with a `parentValue` (in any order) and the nested Tree's state props, and the tree renders the visible items (the roots and the children of every expanded branch) as sibling `role="treeitem"` elements with `aria-level`, `aria-setsize`, `aria-posinset` and, on branches, `aria-expanded`:
2496
+
2497
+ ```tsx
2498
+ import * as React from 'react';
2499
+ import { FlatTree, useFlatTree, useVirtualWindow, type FlatTreeItemData } from '@mortenbrudvik/waveui';
2500
+
2501
+ export function Files({ items }: { items: readonly FlatTreeItemData[] }) {
2502
+ const [checked, setChecked] = React.useState<string[]>([]);
2503
+ const model = useFlatTree(items, {
2504
+ defaultExpandedItems: ['docs'],
2505
+ selectionMode: 'multiple',
2506
+ checkedItems: checked,
2507
+ onCheckedItemsChange: setChecked,
2508
+ });
2509
+ const virtualWindow = useVirtualWindow({ itemSize: 28 });
2510
+ return (
2511
+ <FlatTree
2512
+ aria-label="Files"
2513
+ model={model}
2514
+ {...model.getTreeProps()}
2515
+ virtualWindow={virtualWindow}
2516
+ style={{ height: 400 }}
2517
+ />
2518
+ );
2519
+ }
2520
+ // items: [{ value: 'docs', label: 'Documents' }, { value: 'cv', parentValue: 'docs', label: 'CV.pdf' }, …]
2521
+ ```
2522
+
2523
+ - **The model.** `useFlatTree` returns `visibleItems` (each with `item`, `level`, `position`, `setSize`, `parentValue`, `hasChildren`, `isBranch`, `expanded`, `index` and, in multiple mode, `checked`), `getItem`, `expand` (a value or a list, in one change), `collapse`, `toggle`, `isExpanded`, `setChecked` and `getTreeProps()`. Expanded and checked state take the nested Tree's names (`expandedItems`, `defaultExpandedItems`, `onExpandedItemsChange`, `checkedItems`, `defaultCheckedItems`, `onCheckedItemsChange(checkedItems, details)`), and checks cascade the same way. `itemType: 'branch'` makes an item without children expandable (children loaded on demand). Pass `items`, and controlled arrays, as stable references: the model is rebuilt when they change. An unknown `parentValue` (the item becomes a root), a cycle of parents (cut) and a duplicate value (the first wins) warn in development.
2524
+ - **The tree.** `FlatTree` takes `model`, `selectionMode` (`model.getTreeProps()` passes the model's), `selected`, `current`, `onItemSelect(value)` and `renderItem(row)` (the item's content; its `label` by default), with the nested Tree's keys, chevrons, checkboxes and radios. An item whose data is `disabled` is skipped by the keys and typeahead and cannot be activated, expanded or checked by the user. When a branch collapses around the focused item (from code), focus moves to the branch. It has no row actions, `aside` or motion (items expand and collapse at once). Name it with `aria-label` or `aria-labelledby` (a development warning asks for one).
2525
+
2344
2526
  ### Breadcrumbs
2345
2527
 
2346
2528
  ```tsx
@@ -2542,7 +2724,8 @@ The names and hidden texts that components render themselves are English by defa
2542
2724
  | `Rating` | `labels` (`star(value, max)`; it also names the half values of a `step={0.5}` Rating, "2.5 stars") |
2543
2725
  | `RatingDisplay` | `labels` (`rating(value, max, formattedValue)`, `count(count, formattedCount)`); `locale` formats the value and the count (pass it when rendering on the server) |
2544
2726
  | `SpinButton` | `labels` (`increment`, `decrement`) |
2545
- | `DataGrid` | `labels` (`selectAll`, `selectionHeader`); each row's fallback name "Select row": `DataGrid.Row` `selectionLabel` |
2727
+ | `DataGrid` | `labels` (`selectAll`, `selectionHeader`, and with `resizableColumns` `resizeColumn` ("Resize column", the handles' name) and `columnWidth(width)` ("150 pixels", their value text)); each row's fallback name "Select row": `DataGrid.Row` `selectionLabel` |
2728
+ | `Table.ResizeHandle` | `label` ("Resize column"), `valueText(width)` ("150 pixels") |
2546
2729
  | `AvatarGroup` | `overflowLabel`, `unnamedMemberLabel` |
2547
2730
  | `InfoButton` | `aria-label` (the button and its note; default "Information") |
2548
2731
  | `Hamburger` | `aria-label` ("Navigation", only while it has no other name) |
@@ -2552,11 +2735,95 @@ The names and hidden texts that components render themselves are English by defa
2552
2735
 
2553
2736
  Without a text prop, these defaults are replaced through `aria-label`: the PresenceBadge status names ("Available", …: `aria-label` on the badge), the MessageBar "Dismiss" and SearchBox "Clear search" button names (`dismiss={{ 'aria-label': 'Lukk' }}`, which keeps the default icon), and the names of the Breadcrumb, Pagination, Nav, Stepper, Carousel and Toaster regions ("Breadcrumb", "Pagination", "Navigation", "Progress", "Carousel", "Notifications"). The placeholders of Dropdown, DatePicker, TimePicker and SearchBox are `placeholder` props, and ColorPicker's default presets are replaced through `presets`.
2554
2737
 
2555
- 0.7 and 0.8 add no other built-in text: the submenu half of a `Menu.SplitGroup`, a context menu (`Menu.Popover`) and a `Toolbar.RadioGroup` are named by the `aria-label` you pass, a toast's container by its severity and title, and a tree row's actions toolbar by its item's label. Nor do 0.10 and 0.11, beyond the rows above: a `TagGroup` is named by the `aria-label` you pass, an inline Drawer by its title; a `Listbox`, a `Calendar` and a `ColorSwatch` or `ImageSwatch` are named by the `aria-label` you pass (or a `Field`'s label, or a `Tooltip` around the swatch), an `OptionGroup` by its label, and the month and year buttons by their text in `locale`. 0.14 adds none either beyond the rows above: a `Table.SelectionCell` control is named by the `aria-label` or `aria-labelledby` you pass (a development warning asks for one), and a `CarouselFooter`'s `initialStepText` and `finalStepText` are your text.
2738
+ 0.7 and 0.8 add no other built-in text: the submenu half of a `Menu.SplitGroup`, a context menu (`Menu.Popover`) and a `Toolbar.RadioGroup` are named by the `aria-label` you pass, a toast's container by its severity and title, and a tree row's actions toolbar by its item's label. Nor do 0.10 and 0.11, beyond the rows above: a `TagGroup` is named by the `aria-label` you pass, an inline Drawer by its title; a `Listbox`, a `Calendar` and a `ColorSwatch` or `ImageSwatch` are named by the `aria-label` you pass (or a `Field`'s label, or a `Tooltip` around the swatch), an `OptionGroup` by its label, and the month and year buttons by their text in `locale`. 0.14 adds none either beyond the rows above: a `Table.SelectionCell` control is named by the `aria-label` or `aria-labelledby` you pass (a development warning asks for one), and a `CarouselFooter`'s `initialStepText` and `finalStepText` are your text. 0.16 adds none beyond the resize handles' rows above: a `FlatTree` is named by the `aria-label` or `aria-labelledby` you pass (a development warning asks for one).
2739
+
2740
+ ## Large data
2741
+
2742
+ DataGrid, List, Listbox, Combobox, Dropdown and FlatTree can render only the items in view of a long list, plus an overscan, with spacers (`aria-hidden`, `data-virtual-spacer`) as tall as the items they stand for. Create a window with `useVirtualWindow({ itemSize })` and pass it as `virtualWindow`; the component calls its `layout`, attaches its scroller and keeps the focused item rendered. Selection, select-all, sorting, the keyboard and the positions a screen reader announces follow the data, not the rendered items. No virtualization library is needed (an adapter of one can implement the same `VirtualWindow` interface: the guide's "Data at Scale" chapter has one for `@tanstack/react-virtual`).
2743
+
2744
+ ```tsx
2745
+ import {
2746
+ Combobox,
2747
+ DataGrid,
2748
+ Field,
2749
+ List,
2750
+ createTableColumn,
2751
+ useVirtualWindow,
2752
+ type ListboxItem,
2753
+ } from '@mortenbrudvik/waveui';
2754
+
2755
+ interface Member {
2756
+ id: string;
2757
+ name: string;
2758
+ }
2759
+
2760
+ // Outside the component: a new array would be computed again over every item at every row the
2761
+ // window scrolls past.
2762
+ const columns = [
2763
+ createTableColumn<Member>({
2764
+ columnId: 'name',
2765
+ label: 'Name',
2766
+ compare: (a, b) => a.name.localeCompare(b.name),
2767
+ renderCell: (member) => member.name,
2768
+ }),
2769
+ ];
2770
+ const memberId = (member: Member) => member.id;
2771
+
2772
+ export function Members({ members }: { members: Member[] }) {
2773
+ // 44-pixel rows under a header of 45 pixels (with the select-all checkbox), which sticks over them.
2774
+ const virtualWindow = useVirtualWindow({ itemSize: 44, scrollPaddingStart: 45 });
2775
+ return (
2776
+ <DataGrid
2777
+ aria-label="Members"
2778
+ items={members}
2779
+ columns={columns}
2780
+ getRowId={memberId}
2781
+ selectionMode="multiple"
2782
+ virtualWindow={virtualWindow}
2783
+ containerProps={{ style: { height: 480 } }}
2784
+ />
2785
+ );
2786
+ }
2787
+
2788
+ export function Files({ files }: { files: readonly string[] }) {
2789
+ const virtualWindow = useVirtualWindow({ itemSize: 40 });
2790
+ return (
2791
+ <List
2792
+ aria-label="Files"
2793
+ selectable
2794
+ selectionMode="multiple"
2795
+ virtualWindow={virtualWindow}
2796
+ count={files.length}
2797
+ style={{ height: 320 }}
2798
+ >
2799
+ {(index) => <List.Item value={files[index]}>{files[index]}</List.Item>}
2800
+ </List>
2801
+ );
2802
+ }
2803
+
2804
+ export function PersonPicker({ people }: { people: readonly ListboxItem[] }) {
2805
+ const virtualWindow = useVirtualWindow({ itemSize: 32 });
2806
+ return (
2807
+ <Field label="Person">
2808
+ <Combobox items={people} virtualWindow={virtualWindow} />
2809
+ </Field>
2810
+ );
2811
+ }
2812
+ ```
2813
+
2814
+ - **A fixed item height.** Every item is exactly the window's `itemSize` tall: the component sets that height on each row, item or option, after its own `style`, so the content must fit. A windowed DataGrid's body cells drop their block padding and keep their text on one line (truncated, as the columns are laid out from widths); a List item with one line of text fits in 40 pixels and the default `Option` in 32 (the stories use 44-pixel DataGrid rows and 28-pixel FlatTree items). Variable heights are not supported.
2815
+ - **A bounded height.** The scroller needs a height or a maximum height: the DataGrid's scroll wrapper (`containerProps`), the List, Listbox or FlatTree element. Without one the window measures the whole list and renders every item (DataGrid warns in development). The popup list of a Combobox or Dropdown scrolls within its own maximum height.
2816
+ - **Data, not elements.** The DataGrid needs `items` and `columns` (rows rendered from them, or by a `DataGrid.Body` render function), the pickers their data mode (`items`, `renderOption`), and the List `count` with a function child `(index) => <List.Item …/>`, called for the rendered indexes only; FlatTree renders its model. Keep `items`, `columns`, `getRowId`, `isSelectionDisabled` and `renderOption` stable (outside the component, or memoised): the component that calls `useVirtualWindow` renders again at every row boundary, and a new array or function is computed again over every item.
2817
+ - **The keyboard** moves by the data: a target that is not rendered is scrolled into the window and focused (or, in a picker, made active) in the commit that renders it, and the focused item stays rendered when it is scrolled away. The DataGrid carries `aria-rowcount` and `aria-rowindex`, List items and options `aria-setsize` and `aria-posinset` (a windowed List that is a grid, `aria-rowcount` and `aria-rowindex`), and FlatTree items `aria-level`, `aria-setsize` and `aria-posinset`.
2818
+ - **The scroller** turns the browser's scroll anchoring off, and is no Tab stop of its own while one of its items is the Tab stop; a plain windowed List (`navigationMode` `none`) keeps the browser's default, under which WebKit's keyboard cannot reach it: pass `tabIndex={0}` or `navigationMode="items"`.
2819
+ - **Overscan.** By default the window renders enough items for 240 pixels beyond each edge, and at least 6 (`overscan` sets a number of items). A scroll faster than the overscan in one frame (a fling of the scroll bar) shows the spacers until the next frame renders the new items; raise `overscan` if that matters.
2820
+ - **On the server** the window lays out the top of the list in `initialViewportSize` (480 pixels), so the server HTML holds the first window.
2821
+ - **Your own scroller.** `useVirtualWindow` works for markup of your own too: render `layout(count)`, items for the item entries and empty blocks of `size` pixels for the spacers, and attach `scrollRef` by merging it (`useMergedRefs`) or after destructuring it (`const { scrollRef } = virtualWindow`): `ref={virtualWindow.scrollRef}` fails the React Compiler's lint. `scrollToIndex(index, { align })` scrolls an item into view and moves the window in the same update.
2822
+ - **Limits.** Typeahead in a windowed List matches the rendered items only (in the pickers and FlatTree it searches the data); option groups cannot be windowed (`OptionGroup` children beside a `virtualWindow` are a development warning); TagPicker, TimePicker and the static `Table` have no `virtualWindow` (the table hooks keep a static table's state over every item); `<tfoot>` rows are not supported in a windowed DataGrid (they get no position, and the arrow keys skip them). The guide lists the rest.
2556
2823
 
2557
2824
  ## Keyboard support
2558
2825
 
2559
- As implemented through 0.14. Buttons, links, checkboxes, switches and the trigger buttons of Accordion, Carousel, Nav, Breadcrumb and Pagination are native elements: Tab to reach them, Enter or Space to activate. A Link or `Button as="a"` without `href`, and a Button or Link rendered as a `div` or `span`, are buttons: Tab reaches them, Enter (key down) and Space (key up) activate them. A `disabledFocusable` control stays in the Tab order but Enter, Space and clicks do nothing. The sort buttons of a sortable `Table.HeaderCell` and the step buttons of `TeachingPopover.CarouselNav` are native buttons too.
2826
+ As implemented through 0.16. Buttons, links, checkboxes, switches and the trigger buttons of Accordion, Carousel, Nav, Breadcrumb and Pagination are native elements: Tab to reach them, Enter or Space to activate. A Link or `Button as="a"` without `href`, and a Button or Link rendered as a `div` or `span`, are buttons: Tab reaches them, Enter (key down) and Space (key up) activate them. A `disabledFocusable` control stays in the Tab order but Enter, Space and clicks do nothing. The sort buttons of a sortable `Table.HeaderCell` and the step buttons of `TeachingPopover.CarouselNav` are native buttons too.
2560
2827
 
2561
2828
  | Component | Keys |
2562
2829
  | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -2567,12 +2834,14 @@ As implemented through 0.14. Buttons, links, checkboxes, switches and the trigge
2567
2834
  | RadioGroup, SwatchPicker (row layout) | One Tab stop. All four arrows move and select (wrapping), Home/End jump; Left/Right mirrored in RTL. Keys pressed on a link inside a radio's label stay the link's. An `EmptySwatch` in the row layout is a Tab stop of its own, and the arrow keys pressed on it do nothing. |
2568
2835
  | SwatchPicker (`layout="grid"`) | One Tab stop: the last focused swatch, at first the selected one, else the first. The arrows move focus only: Left/Right through the swatches in reading order across the rows (mirrored in RTL), stopping at the grid's first and last swatch; Up/Down to the enabled swatch of the previous/next row nearest the same column, stopping at the first and last row; Home/End to the first/last swatch of the row, Ctrl+Home/Ctrl+End of the grid. Space, Enter and a click select. Disabled swatches are skipped; an `EmptySwatch` is a cell like the others. |
2569
2836
  | SwatchPicker (`focusMode="tab"`) | Every enabled swatch is a Tab stop, and the arrow keys do nothing; Space, Enter and a click select (the selected swatch stays selected). |
2570
- | Listbox | One Tab stop: the list. The active option shows only while the list has focus (the selected option, else the first). Down/Up move without wrapping, Home/End, PageUp/PageDown by 10, typing moves to the matching option (a Space within 500 ms of a letter continues the search); Space and Enter select, or with `multiselect` toggle. Tab, Escape and Alt with an arrow are left to the page (an enclosing Dialog closes on Escape). With `disabledOptionsFocusable` the keys reach disabled options, but Space and Enter do nothing on them. |
2837
+ | Listbox | One Tab stop: the list. The active option shows only while the list has focus (the selected option, else the first). Down/Up move without wrapping, Home/End, PageUp/PageDown by 10, typing moves to the matching option (a Space within 500 ms of a letter continues the search); Space and Enter select, or with `multiselect` toggle. Tab, Escape and Alt with an arrow are left to the page (an enclosing Dialog closes on Escape). With `disabledOptionsFocusable` the keys reach disabled options, but Space and Enter do nothing on them. Windowed (`virtualWindow`): the same keys over every option, rendered or not; the active option is scrolled into view with the window. |
2571
2838
  | Rating | Right/Up one step more (a star, or half a star with `step={0.5}`), Left/Down one fewer (never below `step`), Home/End `step`/`max`; mirrored in RTL. A pointer click on a half-star Rating chooses the half or the whole star by position; Space and Enter on a focused radio choose that radio's own value. |
2572
2839
  | TabList | One Tab stop (the selected tab). Left/Right (Up/Down when vertical) move and select, Home/End; with `selectTabOnFocus={false}` they only move focus, and Enter or Space selects the focused tab; disabled tabs skipped; mirrored in RTL. |
2573
- | Tree | One Tab stop. Up/Down, Home/End; Right expands or moves to the first child, Left collapses or moves to the parent (mirrored in RTL); Enter activates; Space activates, or in a checkbox tree (`selectionMode="multiple"`) toggles the focused item's check; `*` expands siblings; type to jump (a Space within 500 ms of a letter continues the search). Row actions: Tab from the focused item enters them and Shift+Tab returns (`navigationMode="tree"`); with `navigationMode="treegrid"` the "next" arrow moves into them (on an expanded parent too, instead of to its first child), "previous" returns, and Up/Down move between items. |
2574
- | List (selectable, or `navigationMode="items"`) | One Tab stop (selectable: the first selected item, else the first; without selection: the item that last had focus). Up/Down (wrapping), Home/End, typeahead above 7 items (a Space within 500 ms of a letter continues the search). Enter toggles a selectable item and runs `onAction`; Space toggles a selectable item, or runs `onAction` on an item that cannot be selected. With item actions in a selectable list, or `navigationMode="composite"` (grid): Up/Down between rows, Left/Right into the actions (mirrored in RTL), Enter/F2 into a text field, Escape back. A plain List (`navigationMode` `none`) has no arrow keys: its items' controls are Tab stops. |
2575
- | DataGrid | One Tab stop. Arrows between cells (Left/Right to the previous/next cell of the row, mirrored in RTL; Up/Down and PageUp/PageDown keep the visual column across `colSpan`/`rowSpan` cells of grouped headers), Home/End row start/end, Ctrl+Home/Ctrl+End grid start/end, PageUp/PageDown 10 rows; Enter/F2 into a cell's widgets, Escape back; Space on a cell toggles its row's selection. |
2840
+ | Tree | One Tab stop. Up/Down, Home/End; Right expands or moves to the first child, Left collapses or moves to the parent (mirrored in RTL); Enter activates; Space activates, or in a checkbox tree (`selectionMode="multiple"`) toggles the focused item's check; `*` expands siblings; type to jump (a Space within 500 ms of a letter continues the search). Row actions: Tab from the focused item enters them and Shift+Tab returns (`navigationMode="tree"`); with `navigationMode="treegrid"` the "next" arrow moves into them (on an expanded parent too, instead of to its first child), "previous" returns, and Up/Down move between items. |
2841
+ | FlatTree | One Tab stop: the focused item while focus is inside, else the `selected` item while it is visible, else the first enabled item. Up/Down between the visible items, Home/End; Right expands a collapsed branch or moves to its first child, Left collapses an expanded branch or moves to its parent (mirrored in RTL); Enter activates (and toggles a branch); Space activates, or in a checkbox tree toggles the check; `*` expands the sibling branches; typing moves to the next visible item whose label starts with the text. Disabled items are skipped. Windowed, an item that is not rendered is scrolled into view before it takes focus. |
2842
+ | List (selectable, or `navigationMode="items"`) | One Tab stop (selectable: the first selected item, else the first; without selection: the item that last had focus). Up/Down (wrapping), Home/End, typeahead above 7 items (a Space within 500 ms of a letter continues the search). Enter toggles a selectable item and runs `onAction`; Space toggles a selectable item, or runs `onAction` on an item that cannot be selected. With item actions in a selectable list, or `navigationMode="composite"` (grid): Up/Down between rows, Left/Right into the actions (mirrored in RTL), Enter/F2 into a text field, Escape back. A plain List (`navigationMode` `none`) has no arrow keys: its items' controls are Tab stops. Windowed (`virtualWindow` with `count`): the keys move by index over the whole list, Up/Down wrapping, Home/End, PageUp/PageDown by ten (stopping at the ends); typeahead searches the rendered items only; the focused item is the Tab stop while focus is in the list. |
2843
+ | DataGrid | One Tab stop. Arrows between cells (Left/Right to the previous/next cell of the row, mirrored in RTL; Up/Down and PageUp/PageDown keep the visual column across `colSpan`/`rowSpan` cells of grouped headers), Home/End row start/end, Ctrl+Home/Ctrl+End grid start/end, PageUp/PageDown 10 rows; Enter/F2 into a cell's widgets, Escape back; Space on a cell toggles its row's selection. A column header with a resize handle (`resizableColumns`): F2 on the header cell or its sort button moves focus to the handle (Enter on a sortable header still sorts); there Left/Right move the column's edge by 20 pixels (5 with Shift; mirrored in RTL), Home sets the minimum width, and Enter, Space or Escape return to the header; Tab moves between the header's sort button and handle. Windowed (`virtualWindow`): Up/Down, PageUp/PageDown and Ctrl+Home/Ctrl+End move over every row of the data; a row that is not rendered is scrolled in, then focused. |
2844
+ | Table.ResizeHandle | A Tab stop in a static table. Left/Right move the column's edge by 20 pixels (5 with Shift; mirrored in RTL), Home and End set the minimum and the maximum (`maxWidth`); with `onLeave`, Escape and the release of Enter or Space call it (to move focus back to the header; a held Enter or Space stays on the handle until it is released). |
2576
2845
  | Menu | Trigger: Enter/Space/Down open and focus the first item, Up opens on the last; a trigger with `aria-disabled="true"` (a `disabledFocusable` MenuButton or SplitButton) ignores them and its click. Menu: Up/Down (wrapping), Home/End, typeahead, Enter/Space activate (a Space within 500 ms of a letter continues the search); on a checkbox, radio or switch item Space changes it and keeps the menu open, Enter changes it and closes a popup menu; on `Menu.ItemLink` Enter is the browser's link activation. A submenu's item: Right (Left in RTL), Enter and Space open the submenu and focus its first item; in a submenu Left (Right in RTL) and Escape close only it and return focus to its item. `Menu.SplitGroup`: Right (Left in RTL) moves from the action to the submenu button, Left back. Escape closes and returns focus, Tab closes every level. Context menu (`openOnContext`): Shift+F10 or the ContextMenu key on the region opens it at the focused element; Escape and item activation return focus there, and Tab moves on from it. |
2577
2846
  | Combobox, TagPicker, TimePicker | Down/Up open and move, Alt+Down opens, typing makes the first match active (not in a `freeform` Combobox), Enter commits the active option (with no active option, Enter submits the form; TimePicker instead never submits while its text is edited: a complete time within the bounds is committed, erased text clears the value, other text is kept and flagged invalid, and the list closes; leaving the input with edited text settles it the same way and closes the list), Alt+Up and Tab close, Escape closes and then discards typed text. The chevron button (Combobox, TimePicker) is not a Tab stop; the clear button (`clearable`) is a Tab stop after the input. Tab from erased TimePicker text clears the value first, so focus moves past the clear button that goes with it. TagPicker: Backspace in the empty input, and the previous arrow (Left, Right in RTL) with the caret at the start of the text and nothing selected, focus the last tag; on a tag's remove button Left/Right move between the tags (mirrored in RTL) and past the last one return to the trigger, and Delete, Backspace, Enter or Space remove it and focus the next tag's remove button, else the input (a click focuses the input). Typing makes the option whose label equals the text active, else the first match; with `allowCreate` Enter on the create option adds the text. `TagPicker.Button`: Down, Up and a typed letter open and move, Enter and Space add the active option. The chevron is not a Tab stop; the clear button is, after the trigger. Multi-select (`multiselect`, Combobox): Enter toggles the active option and keeps the list open; Tab, Escape and Alt+Up close without committing; focusing the input selects the shown labels, so typing replaces them with a query, and Backspace and Delete only edit the text. With `disabledOptionsFocusable` (Combobox) the arrow keys reach disabled options, but Enter does nothing on them. |
2578
2847
  | Dropdown | Down/Up/Home/End and typing open and move, PageUp/PageDown move by 10, Enter/Space commit (a Space within 500 ms of a letter continues the search), Alt+Up and Tab commit and close, Escape closes. The clear button (`clearable`) is a Tab stop after the combobox button. Multi-select (`multiselect`): Enter and Space toggle the active option and keep the list open; Tab, Escape and Alt+Up close without committing. With `disabledOptionsFocusable` the keys reach disabled options, but Enter and Space do nothing on them, and Tab or Alt+Up on one closes the list without selecting. |
@@ -2612,7 +2881,7 @@ Typeahead (Menu, a selectable or navigable List, Tree, Dropdown, Listbox) accept
2612
2881
  | `useModalOpen()`, `hasOpenModal()`, `modalAllowProps` | Whether a modal is open; props that keep an app-owned region usable over a modal. |
2613
2882
  | `useFocusableGroup(options)` | One tab stop that holds several focusable elements (`tabBehavior`: `unlimited`, `limited`, `limited-trap-focus`); Enter or F2 enters, Escape leaves. |
2614
2883
  | `useFocusVisible()` | `true` while the last input was the keyboard; `false` on the server. |
2615
- | `useGridNavigation(options)` | Keyboard navigation of a static `<table role="grid">` (the Table's own). |
2884
+ | `useGridNavigation(options)` | Keyboard navigation of a static `<table role=\"grid\">` (the Table's own); `model` (`GridNavigationModel`: `rowCount`, `headerRowCount`, `scrollToRow(index)`) moves the vertical keys by the data of a grid whose rows are rendered in part (`data-row-index` on each rendered row), and returns `activeRowIndex` and `pendingRowIndex`, the rows to keep rendered. A widget with `data-grid-auxiliary` (a resize handle) is never a cell's target; F2 enters it. |
2616
2885
  | `getTabbableElements(container)`, `getFirstTabbable`, `getLastTabbable` | The tabbable elements of a container in Tab order (hidden, disabled and `inert` ones left out). |
2617
2886
  | `focusRing`, `focusRingInset`, `inputFocus`, `inputFocusWithin` | The focus class strings of the library, to compose with `cn()` on your own controls. |
2618
2887
  | `useId(prefix?)` | SSR-safe ids (`prefix-<react id>`; never parse the React part). |
@@ -2622,12 +2891,16 @@ Typeahead (Menu, a selectable or navigable List, Tree, Dropdown, Listbox) accept
2622
2891
  | `usePresence(visible, options?)`, `<Presence visible>` | Mount and unmount an element with CSS enter and exit motion (`data-presence` phases; see [Enter and exit motion](#enter-and-exit-motion)). |
2623
2892
  | `useFieldControl(props, options?)` | Merge a custom control's labelling props with the surrounding `Field` (its label, validation message, hint and state). |
2624
2893
  | `useAnnounce()`, `announce(message, politeness?)` | Screen-reader announcements through a shared live region. |
2625
- | `useActiveDescendant(options)` | The `aria-activedescendant` model under the pickers, for composites that keep focus on one element: `items`, `getId`, `getElement` (the item's element for scrolling; by default the element with its id), `enabled`, `fallback`, `loop`, `activateFirstOnChange`, `onActiveValueChange`; returns `activeValue`, `activeDescendantId` and stable `setActiveValue`, `highlight` (pointer moves, not scrolled into view), `first`, `last`, `next`, `prev` and `move`. State-based: it reads no DOM during render, so an active fallback item's id is in the server HTML. |
2626
- | `useListbox(options)`, `useListboxOption(props)` | The listbox under Dropdown, Combobox, TagPicker and Listbox: keys, typeahead, filtering, the active option and option registration, in `'select-only'`, `'editable'` or `'standalone'` mode, with `multiselect` and `disabledOptionsFocusable`; `onOpenChange(open, details)` and `onSelect(value, details)` (see [Listbox and custom pickers](#listbox-and-custom-pickers)). `useListboxOption` builds an option component of your own. |
2894
+ | `useActiveDescendant(options)` | The `aria-activedescendant` model under the pickers, for composites that keep focus on one element: `items`, `getId`, `getElement` (the item's element for scrolling; by default the element with its id), `enabled`, `fallback`, `loop`, `activateFirstOnChange`, `onActiveValueChange`; returns `activeValue`, `activeDescendantId` and stable `setActiveValue`, `highlight` (pointer moves, not scrolled into view), `first`, `last`, `next`, `prev` and `move`. State-based: it reads no DOM during render, so an active fallback item's id is in the server HTML. `onScrollToItem(index)` is called first for every move that scrolls, for a windowed list that renders part of its items. |
2895
+ | `useListbox(options)`, `useListboxOption(props)` | The listbox under Dropdown, Combobox, TagPicker and Listbox: keys, typeahead, filtering, the active option and option registration, in `'select-only'`, `'editable'` or `'standalone'` mode, with `multiselect` and `disabledOptionsFocusable`; `onOpenChange(open, details)` and `onSelect(value, details)` (see [Listbox and custom pickers](#listbox-and-custom-pickers)). `useListboxOption` builds an option component of your own. In data mode (`items`) `onScrollToItem(index)` receives every active option that scrolls into view, by its index among the shown options. |
2896
+ | `useVirtualWindow(options)` | A window over a long list of fixed-height items (`itemSize`, `overscan`, `initialViewportSize`, `scrollPaddingStart`), for the `virtualWindow` prop of DataGrid, List, Listbox, Combobox, Dropdown and FlatTree: `VirtualWindow` (`itemSize`, `scrollRef`, `layout(count, { keep })`, `scrollToIndex(index, { align })`) and its `VirtualEntry` list (see [Large data](#large-data)). |
2897
+ | `useSelection(options)` | Selection by id held outside the DOM (`selectionMode`, `selectedItems`, `defaultSelectedItems`, `onSelectedItemsChange`): `isSelected`, `toggle`, `select`, `deselect`, `toggleAll(universe)` (keeps the ids outside the universe), `clear`. |
2898
+ | `useTableFeatures(options, plugins)`, `useTableSort`, `useTableSelection`, `useTableColumnSizing` | Headless state of a table built from the `Table` parts: the rows of `items` (`getRows()`), and the sort, selection and column widths of its plugins, over every item (see [Static tables](#static-tables)). |
2899
+ | `useFlatTree(items, options?)` | The model of a `FlatTree`: flat items with a `parentValue`, the visible rows with their level and position, the expanded and checked state (see [Trees](#trees)). |
2627
2900
  | `useListboxPopup(options)`, `ListboxSurface` | The popup of a custom picker: dismissal (`onDismiss(reason)`, a `DismissReason`: `'escape'`, `'outside-press'` or `'focus-outside'`) and positioning, and the surface that renders the list (`listClassName`, `surfaceClassName`). |
2628
2901
  | `ListboxProvider` | Provides a listbox's context to its options when you render the list yourself. |
2629
2902
  | `collectOptionLabels(children)`, `markListboxElement(component, kind)` | Read option labels from children before the options register (server HTML, first render); mark your own option and group components so they are read. |
2630
- | `useWaveTheme()` | Current theme, direction, portal container, `inputDefaults` and `motion` (`'default'` or `'none'`). |
2903
+ | `useWaveTheme()` | Current theme, direction, portal container, `inputDefaults`, `motion` (`'default'` or `'none'`), the merged token overrides (`tokens`), the inline style that applies them (`tokenStyle`) and `applyStylesToPortals`. |
2631
2904
  | `useToastController()` | `dispatchToast` (content or options) / `dismissToast` / `dismissAllToasts` inside `<Toaster>`. |
2632
2905
  | `createToaster()` | A toast controller for code outside components; bind it with `<Toaster toaster={handle}>` (see [Toasts](#toasts)). Server-safe. |
2633
2906
  | `useIsOverflowing(target)`, `useOverflowMenu()`, `useIsOverflowItemVisible(id)` | Overflow detection. |
@@ -2640,11 +2913,37 @@ Typeahead (Menu, a selectable or navigable List, Tree, Dropdown, Listbox) accept
2640
2913
  | `partitionAvatarGroupItems({ items, maxInlineItems, layout })` | Split a list into the inline and overflow items of an AvatarGroup (`{ inlineItems, overflowItems }`); imports nothing. |
2641
2914
  | `addDays(date, amount)`, `addMonths(date, amount)`, `isSameDay(a, b)`, `startOfWeek(date, firstDayOfWeek?)`, `getWeekNumber(date, options?)` | Date helpers on calendar days: every returned date is local midnight (Fluent's keep the time of day). `addMonths` clamps the day to the target month (January 31 plus a month is February 28 or 29); `getWeekNumber` takes `firstDayOfWeek` and `firstWeekOfYear` (`GetWeekNumberOptions`; ISO 8601 is `{ firstDayOfWeek: 1, firstWeekOfYear: 'first-four-day-week' }`). With the types `DayOfWeek` (`0`–`6`, Sunday first) and `FirstWeekOfYear`. Server-safe. |
2642
2915
  | `buttonClassNames`, `toastTitleClassNames`, … | The stable class names of every component and part (see [Styling hooks](#styling-hooks)). Server-safe. |
2643
- | `cn(...classes)` | clsx + tailwind-merge that knows Wave's type ramp and shadows; the last class wins over a conflicting class of the same variant. |
2916
+ | `cn(...classes)` | clsx + tailwind-merge that knows Wave's type ramp, shadows and font families; the last class wins over a conflicting class of the same variant. |
2644
2917
  | `composeEventHandlers(theirs, ours)` | Run a consumer handler, then yours unless it called `preventDefault()`. |
2645
2918
  | `mergeRefs(...refs)` | Merge refs outside components. |
2646
2919
  | `resolveSlot`, `renderSlot` | The slot helpers the components use. |
2647
2920
  | `getThemeClassName(theme)` | The theme classes of a theme. |
2921
+ | `createBrandTokens(seed)` | The 16 brand tokens (`brand10` … `brand160`, a `BrandTokens`) of a `#rgb` or `#rrggbb` seed, each stop at the default ramp's luminance so the light and dark themes keep their contrast; or of a full `BrandRamp`, returned unchanged and checked for contrast in development. For `WaveProvider tokens` or `getTokenStyle` (see [Brand colors and token overrides](#brand-colors-and-token-overrides)). Server-safe. |
2922
+ | `waveTokens` | Every design token as the `var()` that reads it, typed as that literal (`waveTokens.primary` is `'var(--wave-primary)'`), for inline styles and SVG charts; with the types `WaveTokens` and `WaveTokenName`. Frozen and server-safe; no component imports it. |
2923
+ | `chartPalette` | The 12 palette fills as token names, in the order that keeps neighbouring chart series as far apart as the palette allows (read each with `waveTokens[name]`); colors still need a second encoding in every chart. Frozen and server-safe. |
2924
+ | `getTokenStyle(overrides)` | The inline style (`TokenStyle`) that applies token overrides (`WaveTokenOverrides`), the one `WaveProvider` puts on its root and portals; `undefined` when there is nothing to set. Server-safe. |
2925
+
2926
+ ## Upgrading from 0.16
2927
+
2928
+ 0.17 removes nothing public, and everything it adds is opt-in (`tokens` and `applyStylesToPortals`, `createBrandTokens`, `waveTokens`, `getTokenStyle`, `chartPalette`, `Text font`, the palette tints and the font families), but the dark and high-contrast shadows change, and so do two types:
2929
+
2930
+ 1. **Stronger shadows in the dark and high-contrast themes.** The shadow levels are tokens of each theme (`--wave-shadow-2` … `--wave-shadow-64`): the light theme keeps the shadows of 0.16, and dark and high contrast take Fluent's, twice as dark (ambient opacity 0.24, key 0.28), so screenshots of those themes change. A shadow color utility or an opacity modifier (`shadow-black`, `shadow-4/50`) no longer changes Wave's levels: set the level's token for a theme instead (`.wave-dark { --wave-shadow-8: … }`, or `tokens={{ shadow8: … }}`). An `@theme` of yours that sets `--shadow-N` keeps its value.
2931
+ 2. **`WaveContextValue` has three new members**, `tokens`, `tokenStyle` and `applyStylesToPortals` (`{}`, `undefined` and `true` outside a provider): a hand-built value (a test mock of `useWaveTheme`) adds them.
2932
+ 3. **`TextWeight` has a fourth value, `'medium'`:** an exhaustive `switch` over it, or a `Record<TextWeight, …>` of your own, adds a case.
2933
+ 4. **With `tokens`, the provider root and the portal wrappers carry the overrides** in their `style` attribute, a nested provider's root too. Without `tokens` the markup is 0.16's; a test that compares a whole `style` attribute changes when you adopt them.
2934
+
2935
+ On the `./tailwind` path your Tailwind theme gains the palette's new color names and the `font-wave-mono` and `font-wave-numeric` families (see [Global effects](#global-effects)). [Brand colors and token overrides](#brand-colors-and-token-overrides) shows the new API; the CHANGELOG's [0.17.0 section](CHANGELOG.md) lists the additions and the full upgrade notes.
2936
+
2937
+ ## Upgrading from 0.15
2938
+
2939
+ 0.16 removes nothing public, and everything it adds is opt-in (windowing, `FlatTree`, the table hooks, the pickers' data mode, resizable columns), except one behaviour change for a DataGrid given `items`:
2940
+
2941
+ 1. **Selection follows the data.** Select-all selects every selectable item of `items`, rendered or not, and its checkbox reflects them; a row click, Space or a row's control no longer drops the selected ids whose rows are not rendered, and `onSelectedItemsChange` (same signature) reports them, so an id of a filtered-out item, of another page or restored by id stays selected until your code removes it. In single mode the selected id is the first one given. Select-all reports the ids outside `items` first, then the others in `items` order (0.15: the order the rows mounted in, the sorted order for a grid that mounts sorted). A grid of `DataGrid.Row` children is unchanged.
2942
+ 2. **Disabled rows:** select-all can skip only the rows it knows. Pass `isSelectionDisabled(item, index)` when items can be disabled and not all rows are rendered (a `virtualWindow`): it also becomes each rendered row's selection-cell `disabled`.
2943
+ 3. **New development warnings** for a `DataGrid.Row` whose own `rowId` differs from its item's id, or is no item's id, in a grid with `items` (`DataGrid.Row:row-id-mismatch`, `DataGrid.Row:row-id-outside-items`): pass a `getRowId` that returns the rows' ids. The `DataGrid:single-several-selected` text changes with `items`.
2944
+ 4. **Types:** `UseGridNavigationResult` has two new members (`activeRowIndex`, `pendingRowIndex`), which a hand-built value adds; `ListboxOptionElementProps` gains optional `aria-posinset`, `aria-setsize` and `style` (spread them in an option of yours; merge `style` after your own); `DataGridLabels` gains `resizeColumn` and `columnWidth`.
2945
+
2946
+ A windowed list needs a bounded height and a fixed item height (see [Large data](#large-data)). The CHANGELOG's [0.16.0 section](CHANGELOG.md) lists the additions and the full upgrade notes.
2648
2947
 
2649
2948
  ## Upgrading from 0.14
2650
2949