@keboola/design 11.4.0 → 11.5.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 (173) hide show
  1. package/AGENTS.md +191 -1
  2. package/CHANGELOG.md +358 -0
  3. package/COMPONENT-API.md +68 -47
  4. package/dist/{AIKit-DDg5DBCG.js → AIKit-CNX6aiKm.js} +208 -62
  5. package/dist/AIKit-CNX6aiKm.js.map +1 -0
  6. package/dist/{Alert-DHJFrWnS.js → Alert-Ba5b2YPq.js} +2 -2
  7. package/dist/{Alert-DHJFrWnS.js.map → Alert-Ba5b2YPq.js.map} +1 -1
  8. package/dist/{Badge-Ch26CXoP.js → Badge-D0pXdekt.js} +2 -2
  9. package/dist/Badge-D0pXdekt.js.map +1 -0
  10. package/dist/{BrandEditor-Go7w8Zj2.js → BrandEditor-CSANsxUz.js} +6 -6
  11. package/dist/{BrandEditor-Go7w8Zj2.js.map → BrandEditor-CSANsxUz.js.map} +1 -1
  12. package/dist/{BrandLogo-Dmi7jo21.js → BrandLogo-CoKBk4MI.js} +4 -4
  13. package/dist/BrandLogo-CoKBk4MI.js.map +1 -0
  14. package/dist/{Breadcrumb-uEkM6adI.js → Breadcrumb-g_xn_-Nf.js} +2 -2
  15. package/dist/{Breadcrumb-uEkM6adI.js.map → Breadcrumb-g_xn_-Nf.js.map} +1 -1
  16. package/dist/{Checkbox-fC0BuO2B.js → Checkbox-CUIn2jap.js} +2 -2
  17. package/dist/{Checkbox-fC0BuO2B.js.map → Checkbox-CUIn2jap.js.map} +1 -1
  18. package/dist/{Clipboard-ChPdiUy3.js → Clipboard-BTW6C-b_.js} +2 -2
  19. package/dist/{Clipboard-ChPdiUy3.js.map → Clipboard-BTW6C-b_.js.map} +1 -1
  20. package/dist/{ClipboardButton-D9pDjgkB.js → ClipboardButton-CwZJdRaz.js} +2 -2
  21. package/dist/{ClipboardButton-D9pDjgkB.js.map → ClipboardButton-CwZJdRaz.js.map} +1 -1
  22. package/dist/{CodeEditor-Cxy3HnKH.js → CodeEditor-C9zy7PxK.js} +6 -6
  23. package/dist/{CodeEditor-Cxy3HnKH.js.map → CodeEditor-C9zy7PxK.js.map} +1 -1
  24. package/dist/{ColorInput-Bxj-1eLT.js → ColorInput-BZV-u_94.js} +3 -3
  25. package/dist/{ColorInput-Bxj-1eLT.js.map → ColorInput-BZV-u_94.js.map} +1 -1
  26. package/dist/{Combobox-Cs_1pj2q.js → Combobox-BfIfaVMA.js} +273 -104
  27. package/dist/Combobox-BfIfaVMA.js.map +1 -0
  28. package/dist/{Command-B7ML9YMv.js → Command-Bfa1kpbt.js} +2 -2
  29. package/dist/{Command-B7ML9YMv.js.map → Command-Bfa1kpbt.js.map} +1 -1
  30. package/dist/{CopyField-BO_sh-HO.js → CopyField-DpT0Ngz1.js} +2 -2
  31. package/dist/{CopyField-BO_sh-HO.js.map → CopyField-DpT0Ngz1.js.map} +1 -1
  32. package/dist/{DateInput-ZdaMQo5i.js → DateInput-DiEd5uCL.js} +3 -3
  33. package/dist/{DateInput-ZdaMQo5i.js.map → DateInput-DiEd5uCL.js.map} +1 -1
  34. package/dist/{DateRangeInput-DkvNh_sT.js → DateRangeInput-DPfVYchl.js} +3 -3
  35. package/dist/{DateRangeInput-DkvNh_sT.js.map → DateRangeInput-DPfVYchl.js.map} +1 -1
  36. package/dist/{FieldItem-CCd9-xnp.js → FieldItem-B7XMmZSz.js} +3 -3
  37. package/dist/{FieldItem-CCd9-xnp.js.map → FieldItem-B7XMmZSz.js.map} +1 -1
  38. package/dist/{Form-k6PH-em3.js → Form-CsttTHn4.js} +7 -7
  39. package/dist/Form-CsttTHn4.js.map +1 -0
  40. package/dist/{FormGroup-DbcDtrgU.js → FormGroup-DHvTFdno.js} +3 -3
  41. package/dist/{FormGroup-DbcDtrgU.js.map → FormGroup-DHvTFdno.js.map} +1 -1
  42. package/dist/{InputGroup-BnLHY0eq.js → InputGroup-DcZRlVnN.js} +22 -14
  43. package/dist/InputGroup-DcZRlVnN.js.map +1 -0
  44. package/dist/{Kbd-CvSM3D3-.js → Kbd-Cvvt91Q8.js} +14 -4
  45. package/dist/Kbd-Cvvt91Q8.js.map +1 -0
  46. package/dist/{MermaidDiagram-WkonYErO.js → MermaidDiagram-CXG5tC6J.js} +19 -18
  47. package/dist/MermaidDiagram-CXG5tC6J.js.map +1 -0
  48. package/dist/{MultiActionButtonGroup-CxWH7yWK.js → MultiActionButtonGroup-DokHbPgM.js} +2 -2
  49. package/dist/{MultiActionButtonGroup-CxWH7yWK.js.map → MultiActionButtonGroup-DokHbPgM.js.map} +1 -1
  50. package/dist/{Navbar-B2UsolVU.js → Navbar-Liy1TPI7.js} +6 -6
  51. package/dist/{Navbar-B2UsolVU.js.map → Navbar-Liy1TPI7.js.map} +1 -1
  52. package/dist/PageHeader-Q3IpCnQd.js.map +1 -1
  53. package/dist/{PageToolbar-DnbFt14Q.js → PageToolbar-DExqEYGl.js} +2 -2
  54. package/dist/{PageToolbar-DnbFt14Q.js.map → PageToolbar-DExqEYGl.js.map} +1 -1
  55. package/dist/{Popconfirm-CILjmzKI.js → Popconfirm-5pBzT_HQ.js} +10 -11
  56. package/dist/{Popconfirm-CILjmzKI.js.map → Popconfirm-5pBzT_HQ.js.map} +1 -1
  57. package/dist/{RadioGroup-XFzFd1ti.js → RadioGroup-DXPbSj8P.js} +2 -2
  58. package/dist/{RadioGroup-XFzFd1ti.js.map → RadioGroup-DXPbSj8P.js.map} +1 -1
  59. package/dist/{Search-4Bxtt8hS.js → Search-5uQAiNIP.js} +2 -2
  60. package/dist/{Search-4Bxtt8hS.js.map → Search-5uQAiNIP.js.map} +1 -1
  61. package/dist/{SearchInput-f_oY9T4Z.js → SearchInput-D4UgAXJK.js} +3 -3
  62. package/dist/{SearchInput-f_oY9T4Z.js.map → SearchInput-D4UgAXJK.js.map} +1 -1
  63. package/dist/{SectionNav-BpJlisbT.js → SectionNav-OkKCjSA-.js} +3 -3
  64. package/dist/{SectionNav-BpJlisbT.js.map → SectionNav-OkKCjSA-.js.map} +1 -1
  65. package/dist/{Select-DpcgKqKB.js → Select-DV3Hak_l.js} +3 -4
  66. package/dist/Select-DV3Hak_l.js.map +1 -0
  67. package/dist/{Sidebar-Cm6I4dE1.js → Sidebar-D3cY1kha.js} +44 -20
  68. package/dist/Sidebar-D3cY1kha.js.map +1 -0
  69. package/dist/{Slider-pPC_3gz9.js → Slider-CRf-bB4y.js} +2 -2
  70. package/dist/{Slider-pPC_3gz9.js.map → Slider-CRf-bB4y.js.map} +1 -1
  71. package/dist/{Switch-ChfPKcTE.js → Switch-B0WdGSjz.js} +7 -7
  72. package/dist/{Switch-ChfPKcTE.js.map → Switch-B0WdGSjz.js.map} +1 -1
  73. package/dist/{TextInput-DHM6dHmn.js → TextInput-DH5SkbsE.js} +3 -3
  74. package/dist/{TextInput-DHM6dHmn.js.map → TextInput-DH5SkbsE.js.map} +1 -1
  75. package/dist/{ThemeProvider-DrQJtzpd.js → ThemeProvider-2RMdVGxO.js} +17 -12
  76. package/dist/{ThemeProvider-DrQJtzpd.js.map → ThemeProvider-2RMdVGxO.js.map} +1 -1
  77. package/dist/{Timeline-6btyJl4D.js → Timeline-BG5Xogxh.js} +11 -11
  78. package/dist/{Timeline-6btyJl4D.js.map → Timeline-BG5Xogxh.js.map} +1 -1
  79. package/dist/{Workbench-BzE-Vvd9.js → Workbench-UR2FBC7I.js} +3 -1
  80. package/dist/Workbench-UR2FBC7I.js.map +1 -0
  81. package/dist/{ZoomableSvg-BWnDkR61.js → ZoomableSvg-pooPb1Cu.js} +3 -3
  82. package/dist/{ZoomableSvg-BWnDkR61.js.map → ZoomableSvg-pooPb1Cu.js.map} +1 -1
  83. package/dist/editors.js +4 -4
  84. package/dist/flint.js +134 -8
  85. package/dist/flint.js.map +1 -1
  86. package/dist/index.d.ts +537 -135
  87. package/dist/index.js +40 -39
  88. package/dist/internal/blocks/AIKit.js +2 -2
  89. package/dist/internal/blocks/AuthLayout.js +118 -0
  90. package/dist/internal/blocks/AuthLayout.js.map +1 -0
  91. package/dist/internal/blocks/BrandEditor.js +1 -1
  92. package/dist/internal/blocks/ClipboardButton.js +1 -1
  93. package/dist/internal/blocks/ColorInput.js +1 -1
  94. package/dist/internal/blocks/Command.js +1 -1
  95. package/dist/internal/blocks/CopyField.js +1 -1
  96. package/dist/internal/blocks/DateInput.js +1 -1
  97. package/dist/internal/blocks/DateRangeInput.js +1 -1
  98. package/dist/internal/blocks/ExportMenu.js +1 -1
  99. package/dist/internal/blocks/Form/connectors/MultiComboboxField.js +1 -1
  100. package/dist/internal/blocks/Form.js +1 -1
  101. package/dist/internal/blocks/FormGroup.js +1 -1
  102. package/dist/internal/blocks/MultiActionButtonGroup.js +1 -1
  103. package/dist/internal/blocks/MultiCombobox.js +107 -48
  104. package/dist/internal/blocks/MultiCombobox.js.map +1 -1
  105. package/dist/internal/blocks/NumberInput.js +1 -1
  106. package/dist/internal/blocks/PageToolbar.js +1 -1
  107. package/dist/internal/blocks/PasswordInput.js +2 -2
  108. package/dist/internal/blocks/SearchInput.js +1 -1
  109. package/dist/internal/blocks/SearchInputWithNavigation.js +2 -2
  110. package/dist/internal/blocks/SectionNav.js +1 -1
  111. package/dist/internal/blocks/TextInput.js +1 -1
  112. package/dist/internal/blocks/Textarea.js +1 -1
  113. package/dist/internal/blocks/TimeInput.js +2 -2
  114. package/dist/internal/components/Alert.js +1 -1
  115. package/dist/internal/components/Avatar.js +1 -1
  116. package/dist/internal/components/Badge.js +1 -1
  117. package/dist/internal/components/BrandLogo.js +1 -1
  118. package/dist/internal/components/Breadcrumb.js +1 -1
  119. package/dist/internal/components/Checkbox.js +1 -1
  120. package/dist/internal/components/Clipboard.js +1 -1
  121. package/dist/internal/components/CollapsiblePanel.js +1 -1
  122. package/dist/internal/components/Combobox.js +2 -2
  123. package/dist/internal/components/InputGroup.js +1 -1
  124. package/dist/internal/components/Kbd.js +1 -1
  125. package/dist/internal/components/Navbar.js +1 -1
  126. package/dist/internal/components/Popconfirm.js +1 -1
  127. package/dist/internal/components/RadioGroup.js +1 -1
  128. package/dist/internal/components/Search.js +1 -1
  129. package/dist/internal/components/Select.js +1 -1
  130. package/dist/internal/components/Sidebar.js +1 -1
  131. package/dist/internal/components/Slider.js +1 -1
  132. package/dist/internal/components/Switch.js +1 -1
  133. package/dist/internal/components/ThemeProvider.js +1 -1
  134. package/dist/internal/components/Timeline.js +1 -1
  135. package/dist/internal/components/Tooltip.js +1 -1
  136. package/dist/internal/components/Truncated.js +1 -1
  137. package/dist/internal/components/Workbench.js +1 -1
  138. package/dist/internal/components/ZoomableSvg.js +1 -1
  139. package/dist/internal/utils.js +2 -2
  140. package/dist/markdown-editor.js +29 -23
  141. package/dist/markdown-editor.js.map +1 -1
  142. package/dist/mermaid.js +1 -1
  143. package/dist/rjsf.js +22 -23
  144. package/dist/rjsf.js.map +1 -1
  145. package/dist/styles.css +1 -1
  146. package/dist/testing.js +1 -1
  147. package/dist/{useColorSchemeState-klihG9-i.js → themeScopeContext-D7fSyb06.js} +16 -3
  148. package/dist/themeScopeContext-D7fSyb06.js.map +1 -0
  149. package/dist/{themeTokens-D70HeNVF.js → themeTokens-qP1-55O7.js} +37 -10
  150. package/dist/themeTokens-qP1-55O7.js.map +1 -0
  151. package/dist/{utils-B5Z52hYs.js → utils-DSvrhU8E.js} +6 -2
  152. package/dist/{utils-B5Z52hYs.js.map → utils-DSvrhU8E.js.map} +1 -1
  153. package/dist/vega.d.ts +1 -1
  154. package/dist/vega.js +3 -3
  155. package/dist/vega.js.map +1 -1
  156. package/dist/{vegaTheme-DfoEQhOy.js → vegaTheme-CSvFEMBl.js} +6 -6
  157. package/dist/vegaTheme-CSvFEMBl.js.map +1 -0
  158. package/package.json +12 -10
  159. package/src/styles.css +3 -0
  160. package/dist/AIKit-DDg5DBCG.js.map +0 -1
  161. package/dist/Badge-Ch26CXoP.js.map +0 -1
  162. package/dist/BrandLogo-Dmi7jo21.js.map +0 -1
  163. package/dist/Combobox-Cs_1pj2q.js.map +0 -1
  164. package/dist/Form-k6PH-em3.js.map +0 -1
  165. package/dist/InputGroup-BnLHY0eq.js.map +0 -1
  166. package/dist/Kbd-CvSM3D3-.js.map +0 -1
  167. package/dist/MermaidDiagram-WkonYErO.js.map +0 -1
  168. package/dist/Select-DpcgKqKB.js.map +0 -1
  169. package/dist/Sidebar-Cm6I4dE1.js.map +0 -1
  170. package/dist/Workbench-BzE-Vvd9.js.map +0 -1
  171. package/dist/themeTokens-D70HeNVF.js.map +0 -1
  172. package/dist/useColorSchemeState-klihG9-i.js.map +0 -1
  173. package/dist/vegaTheme-DfoEQhOy.js.map +0 -1
package/AGENTS.md CHANGED
@@ -93,8 +93,12 @@ This file answers **which** component to use; [`COMPONENT-API.md`](./COMPONENT-A
93
93
 
94
94
  **Never** build custom label + input combinations — compose `Label` + `FormGroup.TextInput` + `FormGroup.Help` inside `FormGroup`.
95
95
 
96
+ **`Select` and `Select.Trigger` will not compile without an accessible name** — one of `aria-label`, `aria-labelledby`, or an `id` is required (the JSDoc on the prop says why). Pass `id` and point a `<Label htmlFor>` at it where the field has a visible label, `aria-labelledby` where that label is some other node, and `aria-label` for a bare control — a toolbar filter, a repeated row. `field.SelectField` needs none of the three: `FieldItem` owns the label and hands the control its `id`.
97
+
96
98
  `FileUpload` composes `FileUpload.DropZone` (the dashed box, owning the drag / error visuals) with `FileUpload.FileList` (selected files; `invalid` tints the rows). It deliberately ships **no** rejection copy — the wording is product copy, so read the current rejections with `useFileUploadRejections()` inside `FileUpload` and write the sentence yourself. Reach for `useFileDropTarget` only when there is no box to render: it returns `getRootProps()` / `isDragActive` for an affordance you style, and `noClick` / `noKeyboard` keep an already-interactive subtree (a composer, a form) from opening the file dialog on every click.
97
99
 
100
+ **`InputGroup.Addon` (`align="inline-start"` / `"inline-end"`) keeps its own content width** — it no longer gives up space to the input, so a badge or a rendered selection stays fully visible instead of being squeezed to an ellipsis. Bound long, caller-controlled content yourself (a `max-w-[…] overflow-hidden` on the addon) if it isn't a fixed-size icon or button. `InputGroup`'s padding/sizing rules reach any descendant `input`/`textarea`, not just a direct child, so a consumer that wraps the input in its own layout (chips, an overlay) picks up that padding automatically — cancel it with `!px-0`/`!py-0` where the wrapping layout already owns its own edge padding (see `MultiCombobox`).
101
+
98
102
  ### Status surfaces
99
103
 
100
104
  | Use | When |
@@ -176,6 +180,7 @@ The config holds resolved `rgb(…)` strings, not `var()` references — vega do
176
180
  | Tailwind classes | All ad-hoc layout: `tw:flex tw:flex-col tw:gap-4`, `tw:grid …`, `tw:p-6` |
177
181
  | `AppShell` | App-root shell that follows the active brand (sidebar vs top-nav) |
178
182
  | `PageLayout` | Top-level page shape: header, tabs, toolbar, content, footer |
183
+ | `AuthLayout` | Signed-out page frame: sign-in, sign-up, password reset, MFA, project picker |
179
184
  | `PageToolbar` | Index-page toolbar: search + filters / sort / actions / view + chips row |
180
185
  | `Card` | Semantic content surface with `Card.Header` / `Card.Title` / `Card.Content` slots |
181
186
  | `Sidebar` | App-shell left navigation rail (host inside `AppShell.Sidebar`) |
@@ -248,7 +253,153 @@ The `label` prop implies `orientation="horizontal"` — labels are not rendered
248
253
  | `CodeEditor` | Generic code editing (JSON, YAML, shell) |
249
254
  | `SqlEditor` | SQL editing (syntax + autocomplete) |
250
255
 
251
- `Select` is wrong above ~10 items — use `Combobox`. `CodeEditor`/`SqlEditor` load the full CodeMirror bundle and ship from the `@keboola/design/editors` subpath (not the root); reserve for real code editing.
256
+ `Select` is wrong above ~10 items — use `Combobox`. How richly the **selected** value has to render decides nothing here: both controls carry a rich selection (see below), so a short enum whose selection is a badge is still a `Select`. `CodeEditor`/`SqlEditor` load the full CodeMirror bundle and ship from the `@keboola/design/editors` subpath (not the root); reserve for real code editing.
257
+
258
+ #### Clear affordance
259
+
260
+ | Control | Clear affordance |
261
+ | ---------------------------- | --------------------------------------------------------- |
262
+ | `Combobox` / `MultiCombobox` | Default on; `allowClear={false}` to withhold it |
263
+ | `Select` | None — add an explicit "None" / "All" item to the `items` |
264
+
265
+ Withhold it where clearing must not be one click away: a required single-value field, or a control
266
+ whose empty state the surrounding form cannot represent. The button only mounts once there is a
267
+ value, so an empty control renders identically either way. A **required multi** field keeps
268
+ clear-all — the chips are individually removable, so the empty state is reachable regardless and
269
+ withholding the button removes convenience without preventing anything. That is why the schema-driven
270
+ `SelectWidget` derives `allowClear={!required}` while `MultiComboboxField` does not.
271
+
272
+ #### Chip content in a `MultiCombobox`
273
+
274
+ | Use | When |
275
+ | ------------------------------- | ------------------------------------------------------------------------ |
276
+ | `itemToChipLabel` | Anything one chip contains — plain text, a link, an action of its own |
277
+ | `itemToChipColor` | A `Badge` colour per value (deleted, linked, stale) |
278
+ | `pinnedChipLabels` | A chip the control shows but the user does not own — nothing in `value` |
279
+ | `renderChips` + `Combobox.Chip` | One node standing for the whole selection, or a chip count ≠ value count |
280
+
281
+ `itemToChipLabel` returns a `ReactNode`, and a chip yields Enter and Space to interactive content
282
+ inside it — so a link or a button in a chip is not a reason to reach for `renderChips`. Reach for
283
+ the slot only when no per-value accessor can express the area: it replaces the whole thing, so
284
+ `maxVisible` and its `+N` chip become the caller's job too.
285
+
286
+ #### One creatable entry, several values
287
+
288
+ A `MultiCombobox` with `allowCreate` splits a created entry on `createSeparator`, so a pasted list
289
+ becomes one chip per value. It is **on by default** (`','`) — the paste is the reason a multi
290
+ control is creatable at all, and a chip literally named `id1, id2, id3` is never what the user
291
+ meant.
292
+
293
+ | Use | When |
294
+ | ----------------------------------------------------- | ---------------------------------------------------------------------------- |
295
+ | the default (`','`) | Values that arrive as a list — tags, column names, primary keys, ids, emails |
296
+ | `createSeparator={';'}`, `createSeparator={/[\s,]+/}` | Another convention — an Outlook address list, whitespace-separated ids |
297
+ | `createSeparator={null}` | A value can itself contain the separator — free text, a sentence, a name |
298
+
299
+ Each value is trimmed and the empty ones dropped, and repeats within one entry collapse. **The row
300
+ is offered whenever the entry adds anything to the selection, and names exactly those values** — a
301
+ value naming an existing item counts, because committing selects that item rather than minting a
302
+ twin of it. It withholds itself only when the selection already holds every value in the entry, or
303
+ when a single value names an item — that item's own row is the affordance there.
304
+
305
+ `queryToCreateLabel` is handed those values, re-joined with the entry's own separator, so the row
306
+ can neither name a value it drops nor stay silent about one it selects. `queryToCreateValue` is
307
+ called once per value, which is where a per-value projection or normalisation belongs, and
308
+ `canCreate` gates each value rather than the whole entry — so a pasted list keeps the values whose
309
+ format passes and drops the rest, and a value naming an existing item never reaches the gate
310
+ because committing selects that item instead of minting one. `Combobox` has no counterpart: one
311
+ entry there can only be one value.
312
+
313
+ #### How richly the selected value renders
314
+
315
+ | Use | When |
316
+ | ---------------------------------- | ----------------------------------------------------------------------------------- |
317
+ | `Select` — `label` | The trigger shows the item's `label` node as-is, Radix mirrors it from the row |
318
+ | `Select` — `icon` / `hint` | The **row** carries more than the trigger should — both sit outside the mirror |
319
+ | `Combobox` — `itemToStringLabel` | The selection is plain text, which is also the input's value and the search subject |
320
+ | `Combobox` — `itemToSelectedLabel` | The closed control needs a badge, an icon or a secondary line |
321
+
322
+ The three `Combobox` label accessors answer three different questions and are free to disagree:
323
+ `itemToLabel` styles the open list's rows (and takes the query, for `MarkedText`),
324
+ `itemToStringLabel` is the input's text, and `itemToSelectedLabel` is what the closed control shows.
325
+ The last one is decoration in front of the input, not a replacement for it — the input keeps the
326
+ string label as its value, so the control still reports its selection to a screen reader, and a
327
+ query dissolves the node and hands the row back to the search. Drawing the selection yourself in
328
+ `prefix` with `value` pinned to `null` gets the same picture and costs exactly that accessible value.
329
+
330
+ #### Who owns the typed query
331
+
332
+ The control owns its input text, always — a selection snaps it back to that item's label, and
333
+ creating or clearing a value rewrites it. Neither combobox takes a controlled `inputValue`, because
334
+ a parent holding it would have to reproduce all three of those writers. Two hooks cover what a call
335
+ site actually needs from the query:
336
+
337
+ | Use | When |
338
+ | --------------------- | ----------------------------------------------------------------------------- |
339
+ | `inputValueSanitizer` | The typed text has a character set — a bucket, table or column identifier |
340
+ | `onQueryChange` | The owner needs the query itself — a server-side search, an option order |
341
+ | `canCreate` | The value has a **format** the user must get right; the gate is not a rewrite |
342
+ | `queryToCreateValue` | Only the created item needs shaping, not the text the user sees while typing |
343
+
344
+ The last two are about the **value**, not the text: reach for them when the query is fine as typed
345
+ and only what it becomes needs gating or shaping.
346
+
347
+ #### Search semantics — `filter`
348
+
349
+ `Combobox` and `MultiCombobox` match the typed query as **one substring** of the item's label, so
350
+ `frankfurt europe` finds nothing in a list holding `Europe (Frankfurt)`. Any multi-word label wants
351
+ one of these instead:
352
+
353
+ | Use | When |
354
+ | ----------------------------------------------- | -------------------------------------------------------------------- |
355
+ | `filter={filterByWords}` | The searchable text **is** the label — every multi-word label |
356
+ | `filter={(item, q) => matchByWords([a, b], q)}` | Something the label omits is searchable too (an email behind a name) |
357
+
358
+ #### Blank values — `''` and `' '`
359
+
360
+ Some predicates are about blankness itself: "this Storage column equals the empty string" versus
361
+ "equals a single space". Whether a control can carry those depends on the primitive under it.
362
+
363
+ | Control | `''` / `' '` as a value |
364
+ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
365
+ | `MultiCombobox` | **Genuine values** — the selection shows as chips, so a blank one is nameable |
366
+ | `Combobox` | Genuine values; add `itemToSelectedLabel={namedLabel}` to name a blank selection in the closed control |
367
+ | `Select` | **Impossible** — Radix reads an empty root value as "nothing selected", so an item carrying it renders a blank trigger. Use a named sentinel item (`'none'`) and map it at the call site |
368
+
369
+ `withBlankValues` offers both blanks ahead of a value list and collapses duplicates — a column's
370
+ distinct values can already hold a blank, and two `Object.is`-equal rows both report `aria-selected`
371
+ and toggle each other.
372
+
373
+ ```tsx
374
+ import { MultiCombobox, withBlankValues } from '@keboola/design';
375
+
376
+ <MultiCombobox
377
+ allowCreate
378
+ items={withBlankValues(columnValues)}
379
+ value={values}
380
+ onValueChange={setValues}
381
+ placeholder="Add a value"
382
+ />;
383
+ ```
384
+
385
+ **Rows and chips name a blank; the input does not.** A row or a chip rendering as nothing is
386
+ indistinguishable from an empty control, so the display accessors name blanks `[empty string]`,
387
+ `[space character]`, and `[whitespace]` for anything else whose text is all whitespace — only the
388
+ first two read distinctly. `itemToStringLabel` is left alone on purpose: it is the item's string
389
+ identity, the default search text **and** the input's accessible value (see `toLabel`). Two
390
+ consequences at the call site:
391
+
392
+ - Typing never reaches a blank row, whatever `allowCreate` says, because the search text is the raw
393
+ value. Clear the query to pick one. `allowCreate` cannot create a blank either — it trims the
394
+ query — so fixed rows are the only way in.
395
+ - Naming a blank selection in a closed `Combobox` is `itemToSelectedLabel`'s job, **not**
396
+ `itemToStringLabel`'s: the slot renders a node in front of the input and leaves identity, search
397
+ and the accessible value intact.
398
+
399
+ Overriding a display accessor — `itemToLabel`, `itemToChipLabel`, `itemToSelectedLabel`, or the
400
+ `renderChips` slot, which replaces the chip area wholesale — composes `namedLabel` rather than
401
+ retyping the labels. `allowSelectAll` needs nothing: it commits the whole item set, blanks along
402
+ with it.
252
403
 
253
404
  ## Composition recipes
254
405
 
@@ -497,6 +648,20 @@ Both router wirings (TanStack Router, and a custom named router) are in [`RECIPE
497
648
  />
498
649
  ```
499
650
 
651
+ **Action picker `Select`** — a `Select` whose options are actions ("convert all names to UPPERCASE") rather than a value the form holds. Pin `value` to `''`: the trigger keeps showing its `placeholder`, and every pick fires, including the same one twice running.
652
+
653
+ ```tsx
654
+ <Select
655
+ aria-label="Convert all column names"
656
+ placeholder="Convert All Names To"
657
+ value=""
658
+ items={CONVERSIONS}
659
+ onValueChange={renameEveryColumn}
660
+ />
661
+ ```
662
+
663
+ Radix routes the root value through `useControllableState`, which calls `onValueChange` only when the picked value differs from the current one — on the uncontrolled path (no `value` at all) it compares against the previous internal state, so leaving `value` off does **not** help. Storing the last action and feeding it back makes re-picking it a silent no-op, which matters whenever the action is still worth repeating: the user edited one of the renamed columns by hand and wants the bulk rename applied again.
664
+
500
665
  **Pinned chip in a `MultiCombobox`** — a chip the control shows but the user does not own (a set of packages the runtime already ships, a role every member has). `pinnedChipLabels` is a render-only slot, so nothing enters `value`: there is no sentinel to strip on save and the control's own counting never sees a non-item.
501
666
 
502
667
  ```tsx
@@ -510,6 +675,31 @@ Both router wirings (TanStack Router, and a custom named router) are in [`RECIPE
510
675
 
511
676
  Don't fake one by prepending a sentinel to `value` — that costs the caller a strip on every change plus a label branch, and leaks the sentinel into the primitive's counting.
512
677
 
678
+ **Selection summarised in a `MultiCombobox`** — `renderChips` renders the chip area from the
679
+ selected values, for the shape no per-value accessor can express: one node standing for the whole
680
+ selection. `Combobox.Chip` builds a removable chip for the values that still get one, and Base UI
681
+ removes the chip at an index, so render one per entry of `value`, in order.
682
+
683
+ ```tsx
684
+ <MultiCombobox
685
+ items={tables}
686
+ value={selected}
687
+ onValueChange={save}
688
+ renderChips={(values) =>
689
+ values.length === 1 ? (
690
+ <Combobox.Chip>{labelOf(values[0])}</Combobox.Chip>
691
+ ) : (
692
+ <Badge color="blue">{values.length} tables selected</Badge>
693
+ )
694
+ }
695
+ />
696
+ ```
697
+
698
+ A chip that merely holds a link or an action of its own does not need this — `itemToChipLabel`
699
+ takes the node, and the chip lets it have the keyboard.
700
+
701
+ **Select All in a `MultiCombobox`** — `allowSelectAll` means the **whole item set**, never the filtered one, so the row hides itself as soon as a query narrows the list. Don't reach for it to mean "select the matches" — a filtered bulk select needs its own labelled affordance stating the count, not a row whose meaning changes with the input.
702
+
513
703
  **Sheet** — use `Sheet.Header title="…" description="…"` for a styled title + close icon; do **not** hand-roll with the raw `Sheet.Title`, which renders an unstyled `<h2>`. `Sheet.Body` provides horizontal padding only (`tw:px-4`) — add your own vertical rhythm/gap:
514
704
 
515
705
  ```tsx
package/CHANGELOG.md CHANGED
@@ -1,5 +1,363 @@
1
1
  # @keboola/design
2
2
 
3
+ ## 11.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `Markdown` draws a fenced `flint` block as a chart, the way it already draws `mermaid` as a diagram. `flint-chart` is admitted as an alias. `DIAGRAM_LANGUAGES` is exported for callers that stream an answer in and have to leave a fence that is still open as plain code. A spec is refused before anything compiles it if it carries a key that would fetch or evaluate — `url`, `expr`, `signal`, `signals`, `params`, or any `*Expr` property — and the reader is shown the source instead of a chart. Where flint keys a map by column name — `data.values` rows, `semantic_types`, `field_display_names` — the key is the column's, not the spec's, so a chart of landing-page URLs still draws.
8
+
9
+ - `clampPercentage` and `formatPercentage` are exported from `AIKit`, so a usage percentage is clamped and formatted in one place. `clampPercentage` normalizes non-finite input to 0, which the app copies it replaces did not — a `NaN` percentage no longer reaches a `Progress` value, whose indicator is unclamped.
10
+
11
+ - `AuthLayout` floors the form column in the split geometry, and takes `asideCollapse`; `BrandLogo` takes `label`
12
+ - The split's form track is now `minmax(<column + gutters>, 1fr)` instead of `1fr`, so between `lg` and 1536px (`width="form"`) or 1824px (`width="wide"`) it stops handing the column less than `width` asks for — a two-field row there measured ~180px per field.
13
+ - The form column no longer carries `min-h-svh` of its own in the split geometry: the grid root has it and the rows stretch, so `asideCollapse="stack"` no longer reserves a whole empty screen under a short card.
14
+ - `asideCollapse="stack"` keeps the `aside` below `lg`, in its `asideSide` order, for a page whose pitch has to survive a phone. The default stays `hide`.
15
+ - `BrandLogo label` names the logo for a screen reader instead of the brand's registry `name` — a link around the mark otherwise announces the picker's disambiguating suffix (`Keboola (V2)`).
16
+
17
+ - `Combobox` and `MultiCombobox` are now clearable by default — `allowClear` defaults to `true`.
18
+
19
+ Consumer-visible behaviour change. The clear button only mounts once there is something to clear
20
+ (single: a selection; multi: at least one chip), so an empty control renders exactly as before and
21
+ individual chips stay removable regardless. 16 existing call sites across `kbc-ui`,
22
+ `developer-portal` and `theme-generator` gain the affordance; the 10 that already pass `allowClear`
23
+ are unaffected, as are the 4 adder-pattern comboboxes pinned to `value={null}` (nothing is ever
24
+ selected, so the button never mounts).
25
+ - Pass `allowClear={false}` where clearing must not be one click away — a required field, or a
26
+ control whose empty state the surrounding form cannot represent.
27
+ - `RjsfSchemaEditor`'s `SelectWidget` and `DynamicSelectWidget` now both derive it from the schema
28
+ (`allowClear={!required}`), so a schema-required field stays non-clearable. `DynamicSelectWidget`
29
+ passed no `allowClear` before, so its non-required fields gain the button as intended.
30
+ - `MultiComboboxField` deliberately does **not** derive it: a required multi field's chips are
31
+ individually removable, so withholding clear-all prevents nothing.
32
+ - Radix `Select` still has no clear affordance; model "no selection" as an explicit item there.
33
+
34
+ `allowSelectAll` is unchanged (still `false`) — see UT-5422.
35
+
36
+ - `Combobox` and `MultiCombobox` take `inputValueSanitizer` and `onQueryChange`
37
+ - `inputValueSanitizer` rewrites what the user types before the control adopts it, so an identifier field (a bucket, table or column name) can keep the query to its own character set. It runs on typing only, never on the selection's label.
38
+ - `onQueryChange` reports the query the list filters by, sanitizer already applied — for an owner driving a server-side search, an option order, or the name a create form opens with. It goes empty once the input shows the selection's label, and reports from the mount render on, so a search driven by it needs no first fetch of its own.
39
+ - Neither hands the text over: the control keeps owning it, so there is still no controlled `inputValue`.
40
+
41
+ - `Combobox` takes `itemToSelectedLabel` — how the selection renders once the popup closes.
42
+
43
+ Additive. Left unset the closed control shows `itemToStringLabel`'s text, exactly as before.
44
+ - The node renders ahead of the input and the input's own text turns transparent behind it, so the
45
+ selection reads once. Typing dissolves it and hands the row back to the query.
46
+ - The input keeps the string label as its **value**, so the control still reports its selection to a
47
+ screen reader. That is the reason to reach for this over drawing the selection into `prefix` with
48
+ `value` pinned to `null` — same picture, and the input has no accessible value.
49
+ - The three label accessors stay independent: `itemToLabel` styles the open list's rows (and takes
50
+ the query, for `MarkedText`), `itemToStringLabel` is the input's text and the search subject.
51
+
52
+ `Combobox` also now tracks an uncontrolled selection (`defaultValue`, or no `value` at all) the same
53
+ way it tracks a controlled one. Beyond feeding the new accessor, that fixes the list collapsing to
54
+ the selected item after an uncontrolled pick: as with a controlled combobox, reopening now browses
55
+ the whole list instead of filtering by the selection's own label.
56
+
57
+ Radix `Select` needs nothing here — its trigger already mirrors the item's `label` node, and `icon`
58
+ / `hint` sit outside that mirror for a row that carries more than the trigger should.
59
+
60
+ - Add `AuthLayout` — the signed-out page frame (sign-in, sign-up, password reset, MFA, project picker).
61
+
62
+ Compound: `AuthLayout.Header` (the active brand's mark beside the `productName` it is given, or your own lockup as children; takes `actions`), `AuthLayout.Content`, `AuthLayout.Footer`. `width` picks the column (`form` 28rem | `wide` 34rem); passing `aside` switches the page to the split geometry, with `asideSide` choosing the panel's side.
63
+
64
+ - `Select` requires an accessible name — `aria-label`, `aria-labelledby`, or an `id` — and forwards
65
+ `aria-labelledby` to the trigger, which it previously dropped.
66
+
67
+ ```tsx
68
+ // A visible label — point it at the control.
69
+ <Label htmlFor="stage">Stage</Label>
70
+ <Select id="stage" items={STAGES} value={stage} onValueChange={setStage} />
71
+
72
+ // A bare control (toolbar filter, repeated row) — name it directly.
73
+ <Select aria-label="Rows per page" items={ROWS_PER_PAGE} value={rows} onValueChange={setRows} />
74
+ ```
75
+
76
+ The trigger is a `role="combobox"` button, which is not a name-from-content role: neither the
77
+ selected item's label nor the `placeholder` names it, so a `Select` with none of the three reached
78
+ a screen reader as an unnamed control.
79
+ - The requirement sits on `Select.Trigger` as well, so the compound route is covered too. Passing
80
+ more than one route is still allowed — an `id` for the `<Label htmlFor>` plus an `aria-label`
81
+ that overrides it is a legitimate pairing.
82
+ - An `id` alone still names nothing: it counts only when a `<label for>` points at it, and no type
83
+ can check that pair.
84
+ - `field.SelectField` needs none of the three — `FieldItem` owns the label and hands the control
85
+ its `id`. It no longer accepts `aria-label` / `aria-labelledby`, which nothing passed: a name
86
+ from the call site could only disagree with the label the field already renders.
87
+
88
+ - `MultiCombobox` takes `canCreate`, the gate `Combobox` already had. It runs on the typed query once
89
+ that query matches no item and nothing already selected, and returning `false` hides the create row
90
+ so the empty state shows instead of an "Add …" row the caller would only reject afterwards.
91
+
92
+ The case it is for is a free-form value with a format the caller can state — a column name a
93
+ JSON-Schema `pattern` constrains, an email address. Without it a multi creatable control had no way
94
+ to say no before the value was in the selection.
95
+
96
+ The Select All row no longer leaks into the chips. Base UI holds it in `value` like any other item,
97
+ so a complete selection grew a third chip and handed the row's own entry to the caller's
98
+ `itemToChipLabel` / `itemToLabel` as if it were a value — a control that projects its items (an id
99
+ to a label) then rendered that object as a child and React threw. The chip area now filters the
100
+ control's internal entries out before anything reaches a caller callback. The row is also gone
101
+ entirely when there is nothing to select all of, where `0 === 0` had read as "everything is
102
+ selected".
103
+
104
+ `itemToLabel` defaults to `itemToStringLabel` rather than to the built-in label reader, so a control
105
+ whose items are ids with the label beside them states the projection once instead of passing the
106
+ same function twice. Passing both still works, and is what a row richer than the input's own text
107
+ wants.
108
+
109
+ - `MultiCombobox` takes `renderChips` — a slot that renders the chip area from the selected values, for the shape no per-value accessor can express: one node standing for the whole selection, or a chip count that is not the value count. `Combobox.Chip` is exposed as the removable-chip building block a custom render composes from.
110
+
111
+ A chip now also yields Enter and Space to interactive content inside it, so `itemToChipLabel` can return a link or an action and have it activate from the keyboard.
112
+
113
+ - `MultiCombobox` accepts `isItemEqualToValue` and forwards it to Base UI, as `Combobox` already did.
114
+ Without it Base UI matches the selection against `items` by reference, so a control fed object items
115
+ lost its selection the moment the store handed back structurally equal items with fresh references —
116
+ the workaround being a caller-side `string[]` of ids plus an id→label projection.
117
+
118
+ Both controls now build the comparer from one shared factory, with two consequences. An injected
119
+ "Select All" or create row equals only a sentinel of the same marker, so a caller item whose `value`
120
+ is the literal `__select_all__` is a plain item rather than the Select All row. And the comparer
121
+ keeps its identity between renders, where an unmemoized one re-rendered every option row on each
122
+ render of the control.
123
+
124
+ - `MultiCombobox` with `allowCreate` splits a created entry into one value per comma, and takes
125
+ `createSeparator` to change or switch off the split.
126
+
127
+ Consumer-visible behaviour change, on by default. Pasting `id1, id2, id3` into a creatable multi
128
+ control added one chip literally named `id1, id2, id3`; it now adds three. The paste is the reason a
129
+ multi control is creatable at all — tags, column lists, primary keys, allowed component ids, email
130
+ lists — so the split is the default rather than a prop each site has to remember. `Combobox` is
131
+ unchanged: one entry there can only be one value.
132
+ - `createSeparator` takes a string or a `RegExp` for another convention (`';'` for an Outlook
133
+ address list, `/[\s,]+/` where a value can never contain a space), and `null` where a value can
134
+ itself contain the separator and one entry must stay one chip.
135
+ - Whitespace is deliberately not part of the default — tags and column names contain spaces, and
136
+ every value is trimmed anyway, so `id1, id2` needs nothing but the comma.
137
+ - `queryToCreateValue` is called once per value, each already trimmed, so a per-value projection or
138
+ normalisation needs no change. This is also where the legacy wrapper's `trimMultiCreatedValues`
139
+ lands.
140
+ - `canCreate` gates each value rather than the whole entry, so a pasted list keeps the values whose
141
+ format passes and drops the rest instead of being rejected wholesale. A value naming an existing
142
+ item never reaches the gate — committing selects that item rather than minting one.
143
+ - A value repeated inside one entry, or one the selection already holds, is added once. A value
144
+ naming an existing item selects that item rather than minting a twin standing for the same thing.
145
+ - The row is offered whenever the entry adds anything to the selection, and withholds itself only
146
+ when the selection already holds every value, or when a single value names an item (that item's
147
+ own row is the affordance). That is why an entry naming only existing items keeps the row: the
148
+ substring filter matches no item for a multi-value entry, so the row is the only thing that can
149
+ act on such a paste.
150
+ - `queryToCreateLabel` now receives the values committing the row will add, re-joined with the
151
+ entry's own separator, rather than the entry as typed — so the row can neither name a value it
152
+ drops nor stay silent about one it selects. For a single-value entry that string is the entry
153
+ itself, so an existing label reads exactly as before.
154
+
155
+ - Add `AnswerActions` to the AI Kit: the copy-and-vote row for a finished agent answer.
156
+ - Always rendered rather than revealed on hover, so it exists for a reader without a pointer.
157
+ - `aria-pressed` says which vote is recorded, and both buttons stay focusable — a vote can be
158
+ changed but not withdrawn.
159
+ - No copy button when `copyText` is empty: a turn that produced only tool calls has nothing to
160
+ take away.
161
+ - The recorded vote and the write stay with the caller; the two chats vote against different
162
+ endpoints.
163
+
164
+ - `useSidebar(side)` now exposes `setFill(fill: boolean)` alongside `toggleFill()`.
165
+
166
+ `toggleFill` requires the caller to know the current value, so two callers in one commit read the
167
+ same stale flag and undo each other, and a caller whose view of the layout disagrees with the flag
168
+ skips the change entirely. `setFill` states the value it wants, so it is idempotent. `toggleFill`
169
+ is unchanged and still the right call for a user-facing toggle control.
170
+
171
+ - `UsageStats` accepts `open` / `onOpenChange`, so a caller can hold the hover card open past a pointer leave — needed when the card contains a popover of its own.
172
+
173
+ ### Patch Changes
174
+
175
+ - `PlanApproval`, `CollapsibleStepHeader` and `UsageStatsRing` follow the active colour scheme and
176
+ brand. All three pinned fixed grey and white values, so the plan-approval card rendered as a light
177
+ card with near-black text on a dark page, and ignored a customer brand's own surface tokens in
178
+ either scheme. They now consume the semantic tokens throughout: the card's shell is the recessed
179
+ `background` tier, its plan body, action bar and change-request form lift off it on `card`, and each
180
+ tier paints its own foreground so no surface inherits ink from whatever rendered it. Step counts
181
+ read as `muted-foreground`, dividers as `border`, and the usage ring's track as a muted mark.
182
+
183
+ A dismissed step's icon takes its colour from the step's own variant rather than a pinned grey, so
184
+ it now matches the muted label beside it; a failed step keeps the destructive tone. The card's inner
185
+ dividers are a touch more visible in light mode, since one shared border token now draws them and
186
+ the frame alike.
187
+
188
+ - AIKit collapsibles move instead of popping: `PlanApproval`'s plan body and `AnswerSummary`'s answers ease open and closed on `grid-template-rows`, `AnswerSummary` reports a manual toggle through `onExpandToggle`, and `ToolCallStatusHeader` turns one chevron rather than swapping the up and down glyphs.
189
+
190
+ - `AnswerSummary` collapses flush against its header: the spacing before the answers lives inside the collapsible body, so the closed state no longer keeps a 16px gap under the header.
191
+
192
+ - `Combobox` / `MultiCombobox` hold `''` and `' '` as genuine values: `withBlankValues`, `namedLabel`.
193
+
194
+ "This column equals the empty string" and "equals a single space" are real filter predicates against
195
+ Storage data. Base UI keys "is something selected" off `value != null` rather than truthiness, so a
196
+ blank string already round-trips through `onValueChange` verbatim and keeps its clear affordance —
197
+ no sentinel option and no mapping on save. What was missing was the display half: a blank row and a
198
+ blank chip render as nothing, indistinguishable from an empty control.
199
+ - `withBlankValues(values)` offers both blanks ahead of a value list and collapses duplicates. A
200
+ column's distinct values can already hold one, and two `Object.is`-equal rows both report
201
+ `aria-selected` and toggle each other.
202
+ - **Rows and chips** name a blank — `[empty string]`, `[space character]`, and `[whitespace]` for
203
+ anything else whose text is all whitespace. `namedLabel` exposes the rule so an `itemToLabel` /
204
+ `itemToChipLabel` / `renderChips` override can keep it.
205
+ - **`itemToStringLabel` is deliberately not named.** It is the item's string identity, the default
206
+ search text and the input's accessible value, and Base UI counts `''` as a selection — naming it
207
+ would paint text over the placeholder wherever "nothing selected" is spelled `''`. Typing
208
+ therefore never reaches a blank row, and naming a blank selection in a closed `Combobox` is
209
+ `itemToSelectedLabel`'s job.
210
+ - `allowCreate` cannot create a blank: it trims the typed query, so fixed rows are the only way in.
211
+ - Radix `Select` is unaffected and still cannot carry a blank — it reserves `''` for its own empty
212
+ state. The split is documented in `AGENTS.md`.
213
+
214
+ - `Combobox` and `MultiCombobox` popups register in Radix's dismissable-layer stack, so Escape closes
215
+ the popup and leaves the surrounding surface alone. Base UI portals the popup out of the DOM, so a
216
+ `Modal`, `Sheet` or `Popover` around it still counted itself as topmost and took the keystroke — a
217
+ combobox in a modal form lost the whole half-filled form on the key that was meant to dismiss its
218
+ own list. The popup now sits above that surface as a layer of its own; Base UI still owns the
219
+ dismissal itself, and Escape on a closed popup reaches the surface as before.
220
+
221
+ - `Combobox`'s `itemToSelectedLabel` node now paints over the input's own content box instead of
222
+ sitting beside it as a flex sibling. Previously the two boxes shared one row, so the caret and text
223
+ selection lived roughly a badge-width past where the visible label ended.
224
+
225
+ - `Combobox` stores the text the user typed rather than the text it displays, and derives the input's
226
+ value, the search query and the rich selected label from that one piece of state.
227
+
228
+ The two meanings did not fit in one variable, so the difference was reconstructed around it: an
229
+ effect snapping the input back to the controlled value's label, a counter re-triggering that effect
230
+ when `value` stayed put, a string comparison standing in for "this is a selection, not a query", and
231
+ a ref reconstructing why the popup had closed. `typedText: string | null` says it directly, with
232
+ `null` for "nothing typed, showing the selection".
233
+ - Only Base UI's `input-change` reason writes to it. Every other reason it reports — an item press,
234
+ a clear, the label it syncs back on unmount — means "show the selection", which `null` already
235
+ says. Its sync-back is a write to ignore rather than adopt: it stringifies Base UI's own selected
236
+ value, which for a created item is the internal sentinel and not the value the owner holds.
237
+ - The state resets the moment the popup starts closing, so a control the user has walked away from
238
+ shows what its value says. Base UI's own sync-back cannot carry that: it skips the write whenever
239
+ the input's text already matches the label, which a query edited back to the label reaches.
240
+ - Whitespace alone is never adopted as a query. Base UI gates opening the popup on the trimmed
241
+ text, so a control that took it would sit closed showing text its value does not say, with no
242
+ close to discard it.
243
+ - "Showing the selection" is the input's text matching the selection's label, not merely nothing
244
+ having been typed. A query edited back to the label lands on the same screen as never having
245
+ typed: the rich `itemToSelectedLabel` node returns and the list stays browsable rather than
246
+ narrowing to the item already chosen. Base UI's own `shouldBypassFiltering` covers only a fresh
247
+ open — its `queryChangedAfterOpen` flips on the first keystroke and clears on close, never when
248
+ the text returns to the label.
249
+
250
+ One behaviour change: a `value` arriving from elsewhere while the user is mid-query no longer yanks
251
+ the text away on the keystroke. The query stands until it is committed or discarded, and the new
252
+ label shows when the popup closes.
253
+
254
+ - `Combobox` / `MultiCombobox` get a shared word-by-word `filter`: `filterByWords` and `matchByWords`.
255
+
256
+ Both controls match the typed query as one substring of the label, so `frankfurt europe` finds
257
+ nothing in a list holding `Europe (Frankfurt)`. `filter={filterByWords}` requires every word of the
258
+ query to occur somewhere in the label, in any order, ignoring case, matching by containment;
259
+ `matchByWords([a, b], query)` applies the same rule to texts the label does not show — an email
260
+ behind a name, a table id behind a display name.
261
+ - `filterByWords` searches **only** what the control displays, so a filter cannot match an id the
262
+ user cannot see. Extra fields are named explicitly through `matchByWords`.
263
+ - Comparison is a lowercased `includes`, not Base UI's `Intl.Collator` `contains`, so passing
264
+ either helper trades diacritic-insensitive matching (`Muller` finds `Müller`) for word order.
265
+ - Defaults are unchanged — both are opt-in per call site.
266
+
267
+ - `Badge.Button` shows a focus ring. The `focus-visible` block sat on `BadgeRoot`, a `div` that never
268
+ takes focus, so every clickable badge was keyboard-focusable with no visible focus. The ring is
269
+ inset, because a badge root clips its overflow.
270
+
271
+ - `MarkdownEditor`'s plugins, `RjsfSchemaEditor` and `Popconfirm` read their callback props through
272
+ `useStableCallback` instead of a ref assigned in the component body. Each of those assignments wrote
273
+ to shared memory during render, where a render React discards can still leave the ref carrying a
274
+ value that never committed. Two refs in `RjsfSchemaEditor` are gone rather than converted — one
275
+ wrapped a `useState` setter and one a `useMemo` with no dependencies, both already stable, and the
276
+ ref only hid that from the dependency list. `ImagePastePlugin` now decides whether to claim paste
277
+ and drop from a boolean derived from `onImageUpload`, so an editor that gains or loses an uploader
278
+ re-registers its Lexical commands instead of keeping the first answer forever.
279
+
280
+ - `RjsfSchemaEditor`'s enum fields have an accessible name: `SelectWidget` and `DynamicSelectWidget`
281
+ forward rjsf's `id` to their `Combobox`, which is where `FieldTemplate`'s `<Label htmlFor>` points.
282
+ They were the only built-in widgets dropping it, so a screen reader reported every enum field in a
283
+ component configuration as an unnamed combobox and clicking the visible label focused nothing. No
284
+ visual change.
285
+
286
+ Still unnamed, and not addressed here: `RadioWidget` and `CodeEditorWidget` forward the `id` to a
287
+ `div`, which no `<Label htmlFor>` can name — naming those needs `aria-labelledby` from the template
288
+ and a route into the editor's own textarea.
289
+
290
+ - Smoother sidebar motion, and a composer a host can refocus.
291
+ - The sidebar settles instead of sliding linearly: its geometry transitions run
292
+ `cubic-bezier(0.32, 0.72, 0, 1)` over 420ms, the off-canvas slide is composited, and
293
+ all of it drops out under `prefers-reduced-motion`.
294
+ - `Sidebar` takes `mountAfterEnter`: a heavy panel holds its content back until the slide
295
+ has finished, rather than mounting into the same commit and blocking every frame of it.
296
+ Ignored under reduced motion, where there is no transition end to wait for.
297
+ - `Workbench` marks its two boxes `data-workbench-slot="bar"` / `"panel"`, so a floating
298
+ surface can measure the space the dock leaves. The root's own rect is the bar alone
299
+ whatever the state, because the panel is absolutely positioned.
300
+ - `PromptInput` takes `textareaRef`, publishing its textarea so a host that keeps one
301
+ composer across layout changes can put the caret back. Published from the ref callback,
302
+ so it still points at the live element after a host layout restructure recreates it.
303
+
304
+ - `InputGroup.Addon` keeps its own content width instead of giving up space to the input. The input
305
+ was `w-full`, so as a flex item its basis was the whole row — flexbox then shrank a sibling addon in
306
+ proportion to that basis, squeezing even a short badge-plus-label selection to an ellipsis well
307
+ before the field was full. The input is now `flex-1 min-w-0` and `inline-start` / `inline-end`
308
+ addons ship `shrink-0`; behaviour is unchanged where there is no addon, and every existing addon
309
+ holds small non-shrinking content (icons, buttons) so this is a no-op for them. `Combobox`'s rich
310
+ selected-value addon drops the `max-w-[70%] shrink-0` workaround this replaces, keeping only the
311
+ `max-w-[70%]` bound needed for an arbitrarily long selection.
312
+
313
+ `InputGroup`'s padding and sizing rules now target any descendant `input` / `textarea` instead of
314
+ only a direct child, so a consumer that wraps the input in its own layout keeps its padding without
315
+ re-implementing the sizing table by hand. `MultiCombobox` drops the manual `px-4 py-1.5` this
316
+ replaces, and instead cancels the group's rule on its own nested input (`!px-0 !py-0`) since that
317
+ input shares a row with chips rather than sitting at the group's own edge.
318
+
319
+ - `Kbd variant="primary"` and `Kbd.Separator` follow the tooltip chrome they render on. `primary`
320
+ pinned `bg-white/20` + `text-white`, which is only legible on the legacy dark tooltip. Argo's tooltip
321
+ is a light popover, so every shortcut hint on it — Kai's "New chat", the SQL editor's panel toggles,
322
+ flow undo/redo, the sidebar nav — painted white keys on white and vanished, leaving the keys'
323
+ layout width behind as apparent trailing whitespace.
324
+
325
+ Both now consume the muted chrome tokens by default and invert to white only under
326
+ `[data-tooltip-content="true"]`, the attribute the legacy dark tooltip already marks itself with. The
327
+ legacy rendering is unchanged. Call sites no longer need the `tw:[&>span]:text-white/70` override
328
+ that forced the separator white — `Kbd.Separator` handles it.
329
+
330
+ - `MultiCombobox`'s `allowSelectAll` row now hides itself while a query narrows the list. It always
331
+ selected the whole item set, but stayed visible during filtering — type `prod`, see three matches,
332
+ click Select All, end up with all two hundred selected, with nothing on screen saying so. Select All
333
+ keeps meaning "everything"; the reading it can be confused with is no longer on offer. This matches
334
+ the legacy `react/common/Select` wrapper, which hid its own Select-All row while searching.
335
+
336
+ - `PageHeader.Detail`'s documented breadcrumb contract now matches how it's actually used: the
337
+ `breadcrumbs` chain ends at the entity's parent list/hierarchy and never repeats the entity's own
338
+ name as a final, non-link crumb — it's already the `title` right below. The prop doc, `AGENTS.md`
339
+ rules and the `Detail` Storybook examples (Bucket, Table, Workspace, Job, Data App, Shared Code, Add
340
+ Component, Flow) are updated to the same shape.
341
+
342
+ - A chart or diagram inside a `ThemeScope` now follows the scope's scheme, and a chart in dark mode
343
+ keeps readable labels.
344
+
345
+ `ThemeScope` re-points the colour tokens for its subtree, but a canvas that bakes colours into
346
+ markup resolved them from `<html>`: `useThemeAttributes` read the root's `data-theme`, and the
347
+ token resolvers read `getComputedStyle(document.documentElement)`. A chart in a dark-scoped panel
348
+ therefore drew a white plot with black labels on a dark surface.
349
+ - `ThemeScope` publishes its resolved scheme and a container carrying its tokens; `useThemeAttributes`
350
+ prefers both and folds the container into `cacheKey`, so a palette built before the scope answered
351
+ is not served afterwards.
352
+ - `createChannelResolver` / `createTokenResolver` (and the vega, flint and mermaid palettes built on
353
+ them) take the element to read live `--*` values off, defaulting to `<html>` as before.
354
+ - The flint palette now fills `ink.text` and `ink.structure` from the tokens. Flint's own defaults
355
+ assume a light canvas, so axis labels and gridlines were near-black in **any** dark context — not
356
+ only inside a scope.
357
+ - The root barrel grows ~70 B with the scope context; the size budget moves 344→345 KB.
358
+
359
+ - `Timeline` draws its connector line, `Navbar` its accent hairline, and the default `Switch` its keyboard focus ring. All three wrote a pseudo-element variant before another variant — `before:group-data-[variant=solid]/timeline:bg-border`, `before:data-[accent=blue]:bg-sidebar-primary`, `before:focus-visible:ring-4` — which Tailwind compiles to a rule that matches nothing, so none of them had ever rendered for any consumer. Twenty rules were dead. Nothing else changes — no markup, no props. A consumer that already renders these components gains the paint it asked for. `check:css` now fails the build on any class that places a variant after a pseudo-element one, `hover:` and `active:` excepted.
360
+
3
361
  ## 11.4.0
4
362
 
5
363
  ### Minor Changes