@keboola/design 11.4.0 → 11.6.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 (184) hide show
  1. package/AGENTS.md +198 -7
  2. package/CHANGELOG.md +383 -0
  3. package/COMPONENT-API.md +73 -52
  4. package/dist/{AIKit-DDg5DBCG.js → AIKit-BExvt9pt.js} +390 -70
  5. package/dist/AIKit-BExvt9pt.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/DiagramSource-C5_d8Gdj.js +11 -0
  37. package/dist/DiagramSource-C5_d8Gdj.js.map +1 -0
  38. package/dist/{FieldItem-CCd9-xnp.js → FieldItem-B7XMmZSz.js} +3 -3
  39. package/dist/{FieldItem-CCd9-xnp.js.map → FieldItem-B7XMmZSz.js.map} +1 -1
  40. package/dist/{Form-k6PH-em3.js → Form-CsttTHn4.js} +7 -7
  41. package/dist/Form-CsttTHn4.js.map +1 -0
  42. package/dist/{FormGroup-DbcDtrgU.js → FormGroup-DHvTFdno.js} +3 -3
  43. package/dist/{FormGroup-DbcDtrgU.js.map → FormGroup-DHvTFdno.js.map} +1 -1
  44. package/dist/{FullScreenModal-BvPuYibe.js → FullScreenModal-CN9nAs72.js} +50 -2
  45. package/dist/FullScreenModal-CN9nAs72.js.map +1 -0
  46. package/dist/{InputGroup-BnLHY0eq.js → InputGroup-DcZRlVnN.js} +22 -14
  47. package/dist/InputGroup-DcZRlVnN.js.map +1 -0
  48. package/dist/{Kbd-CvSM3D3-.js → Kbd-Cvvt91Q8.js} +14 -4
  49. package/dist/Kbd-Cvvt91Q8.js.map +1 -0
  50. package/dist/{MermaidDiagram-WkonYErO.js → MermaidDiagram-DSI7xMu2.js} +21 -19
  51. package/dist/MermaidDiagram-DSI7xMu2.js.map +1 -0
  52. package/dist/{MultiActionButtonGroup-CxWH7yWK.js → MultiActionButtonGroup-DokHbPgM.js} +2 -2
  53. package/dist/{MultiActionButtonGroup-CxWH7yWK.js.map → MultiActionButtonGroup-DokHbPgM.js.map} +1 -1
  54. package/dist/{Navbar-B2UsolVU.js → Navbar-Liy1TPI7.js} +6 -6
  55. package/dist/{Navbar-B2UsolVU.js.map → Navbar-Liy1TPI7.js.map} +1 -1
  56. package/dist/PageHeader-Q3IpCnQd.js.map +1 -1
  57. package/dist/{PageToolbar-DnbFt14Q.js → PageToolbar-DExqEYGl.js} +2 -2
  58. package/dist/{PageToolbar-DnbFt14Q.js.map → PageToolbar-DExqEYGl.js.map} +1 -1
  59. package/dist/{Popconfirm-CILjmzKI.js → Popconfirm-5pBzT_HQ.js} +10 -11
  60. package/dist/{Popconfirm-CILjmzKI.js.map → Popconfirm-5pBzT_HQ.js.map} +1 -1
  61. package/dist/{RadioGroup-XFzFd1ti.js → RadioGroup-DXPbSj8P.js} +2 -2
  62. package/dist/{RadioGroup-XFzFd1ti.js.map → RadioGroup-DXPbSj8P.js.map} +1 -1
  63. package/dist/{Search-4Bxtt8hS.js → Search-5uQAiNIP.js} +2 -2
  64. package/dist/{Search-4Bxtt8hS.js.map → Search-5uQAiNIP.js.map} +1 -1
  65. package/dist/{SearchInput-f_oY9T4Z.js → SearchInput-D4UgAXJK.js} +3 -3
  66. package/dist/{SearchInput-f_oY9T4Z.js.map → SearchInput-D4UgAXJK.js.map} +1 -1
  67. package/dist/{SectionNav-BpJlisbT.js → SectionNav-OkKCjSA-.js} +3 -3
  68. package/dist/{SectionNav-BpJlisbT.js.map → SectionNav-OkKCjSA-.js.map} +1 -1
  69. package/dist/{Select-DpcgKqKB.js → Select-DV3Hak_l.js} +3 -4
  70. package/dist/Select-DV3Hak_l.js.map +1 -0
  71. package/dist/{Sidebar-Cm6I4dE1.js → Sidebar-D3cY1kha.js} +44 -20
  72. package/dist/Sidebar-D3cY1kha.js.map +1 -0
  73. package/dist/{Slider-pPC_3gz9.js → Slider-CRf-bB4y.js} +2 -2
  74. package/dist/{Slider-pPC_3gz9.js.map → Slider-CRf-bB4y.js.map} +1 -1
  75. package/dist/{Switch-ChfPKcTE.js → Switch-B0WdGSjz.js} +7 -7
  76. package/dist/{Switch-ChfPKcTE.js.map → Switch-B0WdGSjz.js.map} +1 -1
  77. package/dist/{TextInput-DHM6dHmn.js → TextInput-DH5SkbsE.js} +3 -3
  78. package/dist/{TextInput-DHM6dHmn.js.map → TextInput-DH5SkbsE.js.map} +1 -1
  79. package/dist/{ThemeProvider-DrQJtzpd.js → ThemeProvider-2RMdVGxO.js} +17 -12
  80. package/dist/{ThemeProvider-DrQJtzpd.js.map → ThemeProvider-2RMdVGxO.js.map} +1 -1
  81. package/dist/{Timeline-6btyJl4D.js → Timeline-BG5Xogxh.js} +11 -11
  82. package/dist/{Timeline-6btyJl4D.js.map → Timeline-BG5Xogxh.js.map} +1 -1
  83. package/dist/{Workbench-BzE-Vvd9.js → Workbench-UR2FBC7I.js} +3 -1
  84. package/dist/Workbench-UR2FBC7I.js.map +1 -0
  85. package/dist/{ZoomableSvg-BWnDkR61.js → ZoomableSvg-pooPb1Cu.js} +3 -3
  86. package/dist/{ZoomableSvg-BWnDkR61.js.map → ZoomableSvg-pooPb1Cu.js.map} +1 -1
  87. package/dist/editors.js +4 -4
  88. package/dist/flint.js +136 -9
  89. package/dist/flint.js.map +1 -1
  90. package/dist/index.d.ts +801 -324
  91. package/dist/index.js +41 -40
  92. package/dist/internal/blocks/AIKit.js +2 -2
  93. package/dist/internal/blocks/AuthLayout.js +118 -0
  94. package/dist/internal/blocks/AuthLayout.js.map +1 -0
  95. package/dist/internal/blocks/BrandEditor.js +1 -1
  96. package/dist/internal/blocks/ClipboardButton.js +1 -1
  97. package/dist/internal/blocks/ColorInput.js +1 -1
  98. package/dist/internal/blocks/Command.js +1 -1
  99. package/dist/internal/blocks/CopyField.js +1 -1
  100. package/dist/internal/blocks/DateInput.js +1 -1
  101. package/dist/internal/blocks/DateRangeInput.js +1 -1
  102. package/dist/internal/blocks/ExportMenu.js +1 -1
  103. package/dist/internal/blocks/Form/connectors/MultiComboboxField.js +1 -1
  104. package/dist/internal/blocks/Form.js +1 -1
  105. package/dist/internal/blocks/FormGroup.js +1 -1
  106. package/dist/internal/blocks/MultiActionButtonGroup.js +1 -1
  107. package/dist/internal/blocks/MultiCombobox.js +107 -48
  108. package/dist/internal/blocks/MultiCombobox.js.map +1 -1
  109. package/dist/internal/blocks/NumberInput.js +1 -1
  110. package/dist/internal/blocks/PageToolbar.js +1 -1
  111. package/dist/internal/blocks/PasswordInput.js +2 -2
  112. package/dist/internal/blocks/SearchInput.js +1 -1
  113. package/dist/internal/blocks/SearchInputWithNavigation.js +2 -2
  114. package/dist/internal/blocks/SectionNav.js +1 -1
  115. package/dist/internal/blocks/TextInput.js +1 -1
  116. package/dist/internal/blocks/Textarea.js +1 -1
  117. package/dist/internal/blocks/TimeInput.js +2 -2
  118. package/dist/internal/components/Alert.js +1 -1
  119. package/dist/internal/components/Avatar.js +1 -1
  120. package/dist/internal/components/Badge.js +1 -1
  121. package/dist/internal/components/BrandLogo.js +1 -1
  122. package/dist/internal/components/Breadcrumb.js +1 -1
  123. package/dist/internal/components/Checkbox.js +1 -1
  124. package/dist/internal/components/Clipboard.js +1 -1
  125. package/dist/internal/components/CollapsiblePanel.js +1 -1
  126. package/dist/internal/components/Combobox.js +2 -2
  127. package/dist/internal/components/FullScreenModal.js +1 -1
  128. package/dist/internal/components/InputGroup.js +1 -1
  129. package/dist/internal/components/Kbd.js +1 -1
  130. package/dist/internal/components/Navbar.js +1 -1
  131. package/dist/internal/components/Popconfirm.js +1 -1
  132. package/dist/internal/components/Progress.js +3 -1
  133. package/dist/internal/components/Progress.js.map +1 -1
  134. package/dist/internal/components/RadioGroup.js +1 -1
  135. package/dist/internal/components/Search.js +1 -1
  136. package/dist/internal/components/Select.js +1 -1
  137. package/dist/internal/components/Sidebar.js +1 -1
  138. package/dist/internal/components/Slider.js +1 -1
  139. package/dist/internal/components/Switch.js +1 -1
  140. package/dist/internal/components/ThemeProvider.js +1 -1
  141. package/dist/internal/components/Timeline.js +1 -1
  142. package/dist/internal/components/Tooltip.js +1 -1
  143. package/dist/internal/components/Truncated.js +1 -1
  144. package/dist/internal/components/Workbench.js +1 -1
  145. package/dist/internal/components/ZoomableSvg.js +1 -1
  146. package/dist/internal/utils.js +2 -2
  147. package/dist/markdown-editor.js +29 -23
  148. package/dist/markdown-editor.js.map +1 -1
  149. package/dist/mermaid.js +1 -1
  150. package/dist/rjsf.js +22 -23
  151. package/dist/rjsf.js.map +1 -1
  152. package/dist/styles.css +1 -1
  153. package/dist/{svgTransforms-C9njt92h.js → svgTransforms-CJAYPk7o.js} +2 -10
  154. package/dist/svgTransforms-CJAYPk7o.js.map +1 -0
  155. package/dist/testing.js +1 -1
  156. package/dist/{useColorSchemeState-klihG9-i.js → themeScopeContext-D7fSyb06.js} +16 -3
  157. package/dist/themeScopeContext-D7fSyb06.js.map +1 -0
  158. package/dist/{themeTokens-D70HeNVF.js → themeTokens-qP1-55O7.js} +37 -10
  159. package/dist/themeTokens-qP1-55O7.js.map +1 -0
  160. package/dist/{utils-B5Z52hYs.js → utils-DSvrhU8E.js} +6 -2
  161. package/dist/{utils-B5Z52hYs.js.map → utils-DSvrhU8E.js.map} +1 -1
  162. package/dist/vega.d.ts +1 -1
  163. package/dist/vega.js +3 -3
  164. package/dist/vega.js.map +1 -1
  165. package/dist/{vegaTheme-DfoEQhOy.js → vegaTheme-CSvFEMBl.js} +6 -6
  166. package/dist/vegaTheme-CSvFEMBl.js.map +1 -0
  167. package/package.json +11 -9
  168. package/src/styles.css +3 -0
  169. package/dist/AIKit-DDg5DBCG.js.map +0 -1
  170. package/dist/Badge-Ch26CXoP.js.map +0 -1
  171. package/dist/BrandLogo-Dmi7jo21.js.map +0 -1
  172. package/dist/Combobox-Cs_1pj2q.js.map +0 -1
  173. package/dist/Form-k6PH-em3.js.map +0 -1
  174. package/dist/FullScreenModal-BvPuYibe.js.map +0 -1
  175. package/dist/InputGroup-BnLHY0eq.js.map +0 -1
  176. package/dist/Kbd-CvSM3D3-.js.map +0 -1
  177. package/dist/MermaidDiagram-WkonYErO.js.map +0 -1
  178. package/dist/Select-DpcgKqKB.js.map +0 -1
  179. package/dist/Sidebar-Cm6I4dE1.js.map +0 -1
  180. package/dist/Workbench-BzE-Vvd9.js.map +0 -1
  181. package/dist/svgTransforms-C9njt92h.js.map +0 -1
  182. package/dist/themeTokens-D70HeNVF.js.map +0 -1
  183. package/dist/useColorSchemeState-klihG9-i.js.map +0 -1
  184. 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 |
@@ -108,12 +112,13 @@ This file answers **which** component to use; [`COMPONENT-API.md`](./COMPONENT-A
108
112
 
109
113
  ### Loading
110
114
 
111
- | Use | When |
112
- | ------------------ | ----------------------------------------------------------------------------------------------- |
113
- | `Skeleton` | Layout-aware placeholder shown in the slot the real content will occupy (initial load, refetch) |
114
- | `SkeletonList` | Pre-composed list shell (icon + row, repeated) for Table / Card-list pages |
115
- | `Button isLoading` | In-flight action on a known button — swaps the icon for a spinner and disables the button |
116
- | `DotsLoader` | Indeterminate background work without a layout slot (small inline progress signal) |
115
+ | Use | When |
116
+ | ------------------- | -------------------------------------------------------------------------------------------------- |
117
+ | `Skeleton` | Layout-aware placeholder shown in the slot the real content will occupy (initial load, refetch) |
118
+ | `SkeletonList` | Pre-composed list shell (icon + row, repeated) for Table / Card-list pages |
119
+ | `Button isLoading` | In-flight action on a known button — swaps the icon for a spinner and disables the button |
120
+ | `DotsLoader` | Indeterminate background work without a layout slot (small inline progress signal) |
121
+ | `ThinkingIndicator` | An agent turn that is in flight with nothing to show yet — every Keboola chat says it the same way |
117
122
 
118
123
  **Never** build a new "Loader" / "BoxLoader" / "ActionLoader" wrapper component. Compose `Skeleton` directly into the slot the real content will occupy.
119
124
 
@@ -176,6 +181,7 @@ The config holds resolved `rgb(…)` strings, not `var()` references — vega do
176
181
  | Tailwind classes | All ad-hoc layout: `tw:flex tw:flex-col tw:gap-4`, `tw:grid …`, `tw:p-6` |
177
182
  | `AppShell` | App-root shell that follows the active brand (sidebar vs top-nav) |
178
183
  | `PageLayout` | Top-level page shape: header, tabs, toolbar, content, footer |
184
+ | `AuthLayout` | Signed-out page frame: sign-in, sign-up, password reset, MFA, project picker |
179
185
  | `PageToolbar` | Index-page toolbar: search + filters / sort / actions / view + chips row |
180
186
  | `Card` | Semantic content surface with `Card.Header` / `Card.Title` / `Card.Content` slots |
181
187
  | `Sidebar` | App-shell left navigation rail (host inside `AppShell.Sidebar`) |
@@ -248,7 +254,153 @@ The `label` prop implies `orientation="horizontal"` — labels are not rendered
248
254
  | `CodeEditor` | Generic code editing (JSON, YAML, shell) |
249
255
  | `SqlEditor` | SQL editing (syntax + autocomplete) |
250
256
 
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.
257
+ `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.
258
+
259
+ #### Clear affordance
260
+
261
+ | Control | Clear affordance |
262
+ | ---------------------------- | --------------------------------------------------------- |
263
+ | `Combobox` / `MultiCombobox` | Default on; `allowClear={false}` to withhold it |
264
+ | `Select` | None — add an explicit "None" / "All" item to the `items` |
265
+
266
+ Withhold it where clearing must not be one click away: a required single-value field, or a control
267
+ whose empty state the surrounding form cannot represent. The button only mounts once there is a
268
+ value, so an empty control renders identically either way. A **required multi** field keeps
269
+ clear-all — the chips are individually removable, so the empty state is reachable regardless and
270
+ withholding the button removes convenience without preventing anything. That is why the schema-driven
271
+ `SelectWidget` derives `allowClear={!required}` while `MultiComboboxField` does not.
272
+
273
+ #### Chip content in a `MultiCombobox`
274
+
275
+ | Use | When |
276
+ | ------------------------------- | ------------------------------------------------------------------------ |
277
+ | `itemToChipLabel` | Anything one chip contains — plain text, a link, an action of its own |
278
+ | `itemToChipColor` | A `Badge` colour per value (deleted, linked, stale) |
279
+ | `pinnedChipLabels` | A chip the control shows but the user does not own — nothing in `value` |
280
+ | `renderChips` + `Combobox.Chip` | One node standing for the whole selection, or a chip count ≠ value count |
281
+
282
+ `itemToChipLabel` returns a `ReactNode`, and a chip yields Enter and Space to interactive content
283
+ inside it — so a link or a button in a chip is not a reason to reach for `renderChips`. Reach for
284
+ the slot only when no per-value accessor can express the area: it replaces the whole thing, so
285
+ `maxVisible` and its `+N` chip become the caller's job too.
286
+
287
+ #### One creatable entry, several values
288
+
289
+ A `MultiCombobox` with `allowCreate` splits a created entry on `createSeparator`, so a pasted list
290
+ becomes one chip per value. It is **on by default** (`','`) — the paste is the reason a multi
291
+ control is creatable at all, and a chip literally named `id1, id2, id3` is never what the user
292
+ meant.
293
+
294
+ | Use | When |
295
+ | ----------------------------------------------------- | ---------------------------------------------------------------------------- |
296
+ | the default (`','`) | Values that arrive as a list — tags, column names, primary keys, ids, emails |
297
+ | `createSeparator={';'}`, `createSeparator={/[\s,]+/}` | Another convention — an Outlook address list, whitespace-separated ids |
298
+ | `createSeparator={null}` | A value can itself contain the separator — free text, a sentence, a name |
299
+
300
+ Each value is trimmed and the empty ones dropped, and repeats within one entry collapse. **The row
301
+ is offered whenever the entry adds anything to the selection, and names exactly those values** — a
302
+ value naming an existing item counts, because committing selects that item rather than minting a
303
+ twin of it. It withholds itself only when the selection already holds every value in the entry, or
304
+ when a single value names an item — that item's own row is the affordance there.
305
+
306
+ `queryToCreateLabel` is handed those values, re-joined with the entry's own separator, so the row
307
+ can neither name a value it drops nor stay silent about one it selects. `queryToCreateValue` is
308
+ called once per value, which is where a per-value projection or normalisation belongs, and
309
+ `canCreate` gates each value rather than the whole entry — so a pasted list keeps the values whose
310
+ format passes and drops the rest, and a value naming an existing item never reaches the gate
311
+ because committing selects that item instead of minting one. `Combobox` has no counterpart: one
312
+ entry there can only be one value.
313
+
314
+ #### How richly the selected value renders
315
+
316
+ | Use | When |
317
+ | ---------------------------------- | ----------------------------------------------------------------------------------- |
318
+ | `Select` — `label` | The trigger shows the item's `label` node as-is, Radix mirrors it from the row |
319
+ | `Select` — `icon` / `hint` | The **row** carries more than the trigger should — both sit outside the mirror |
320
+ | `Combobox` — `itemToStringLabel` | The selection is plain text, which is also the input's value and the search subject |
321
+ | `Combobox` — `itemToSelectedLabel` | The closed control needs a badge, an icon or a secondary line |
322
+
323
+ The three `Combobox` label accessors answer three different questions and are free to disagree:
324
+ `itemToLabel` styles the open list's rows (and takes the query, for `MarkedText`),
325
+ `itemToStringLabel` is the input's text, and `itemToSelectedLabel` is what the closed control shows.
326
+ The last one is decoration in front of the input, not a replacement for it — the input keeps the
327
+ string label as its value, so the control still reports its selection to a screen reader, and a
328
+ query dissolves the node and hands the row back to the search. Drawing the selection yourself in
329
+ `prefix` with `value` pinned to `null` gets the same picture and costs exactly that accessible value.
330
+
331
+ #### Who owns the typed query
332
+
333
+ The control owns its input text, always — a selection snaps it back to that item's label, and
334
+ creating or clearing a value rewrites it. Neither combobox takes a controlled `inputValue`, because
335
+ a parent holding it would have to reproduce all three of those writers. Two hooks cover what a call
336
+ site actually needs from the query:
337
+
338
+ | Use | When |
339
+ | --------------------- | ----------------------------------------------------------------------------- |
340
+ | `inputValueSanitizer` | The typed text has a character set — a bucket, table or column identifier |
341
+ | `onQueryChange` | The owner needs the query itself — a server-side search, an option order |
342
+ | `canCreate` | The value has a **format** the user must get right; the gate is not a rewrite |
343
+ | `queryToCreateValue` | Only the created item needs shaping, not the text the user sees while typing |
344
+
345
+ The last two are about the **value**, not the text: reach for them when the query is fine as typed
346
+ and only what it becomes needs gating or shaping.
347
+
348
+ #### Search semantics — `filter`
349
+
350
+ `Combobox` and `MultiCombobox` match the typed query as **one substring** of the item's label, so
351
+ `frankfurt europe` finds nothing in a list holding `Europe (Frankfurt)`. Any multi-word label wants
352
+ one of these instead:
353
+
354
+ | Use | When |
355
+ | ----------------------------------------------- | -------------------------------------------------------------------- |
356
+ | `filter={filterByWords}` | The searchable text **is** the label — every multi-word label |
357
+ | `filter={(item, q) => matchByWords([a, b], q)}` | Something the label omits is searchable too (an email behind a name) |
358
+
359
+ #### Blank values — `''` and `' '`
360
+
361
+ Some predicates are about blankness itself: "this Storage column equals the empty string" versus
362
+ "equals a single space". Whether a control can carry those depends on the primitive under it.
363
+
364
+ | Control | `''` / `' '` as a value |
365
+ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
366
+ | `MultiCombobox` | **Genuine values** — the selection shows as chips, so a blank one is nameable |
367
+ | `Combobox` | Genuine values; add `itemToSelectedLabel={namedLabel}` to name a blank selection in the closed control |
368
+ | `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 |
369
+
370
+ `withBlankValues` offers both blanks ahead of a value list and collapses duplicates — a column's
371
+ distinct values can already hold a blank, and two `Object.is`-equal rows both report `aria-selected`
372
+ and toggle each other.
373
+
374
+ ```tsx
375
+ import { MultiCombobox, withBlankValues } from '@keboola/design';
376
+
377
+ <MultiCombobox
378
+ allowCreate
379
+ items={withBlankValues(columnValues)}
380
+ value={values}
381
+ onValueChange={setValues}
382
+ placeholder="Add a value"
383
+ />;
384
+ ```
385
+
386
+ **Rows and chips name a blank; the input does not.** A row or a chip rendering as nothing is
387
+ indistinguishable from an empty control, so the display accessors name blanks `[empty string]`,
388
+ `[space character]`, and `[whitespace]` for anything else whose text is all whitespace — only the
389
+ first two read distinctly. `itemToStringLabel` is left alone on purpose: it is the item's string
390
+ identity, the default search text **and** the input's accessible value (see `toLabel`). Two
391
+ consequences at the call site:
392
+
393
+ - Typing never reaches a blank row, whatever `allowCreate` says, because the search text is the raw
394
+ value. Clear the query to pick one. `allowCreate` cannot create a blank either — it trims the
395
+ query — so fixed rows are the only way in.
396
+ - Naming a blank selection in a closed `Combobox` is `itemToSelectedLabel`'s job, **not**
397
+ `itemToStringLabel`'s: the slot renders a node in front of the input and leaves identity, search
398
+ and the accessible value intact.
399
+
400
+ Overriding a display accessor — `itemToLabel`, `itemToChipLabel`, `itemToSelectedLabel`, or the
401
+ `renderChips` slot, which replaces the chip area wholesale — composes `namedLabel` rather than
402
+ retyping the labels. `allowSelectAll` needs nothing: it commits the whole item set, blanks along
403
+ with it.
252
404
 
253
405
  ## Composition recipes
254
406
 
@@ -497,6 +649,20 @@ Both router wirings (TanStack Router, and a custom named router) are in [`RECIPE
497
649
  />
498
650
  ```
499
651
 
652
+ **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.
653
+
654
+ ```tsx
655
+ <Select
656
+ aria-label="Convert all column names"
657
+ placeholder="Convert All Names To"
658
+ value=""
659
+ items={CONVERSIONS}
660
+ onValueChange={renameEveryColumn}
661
+ />
662
+ ```
663
+
664
+ 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.
665
+
500
666
  **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
667
 
502
668
  ```tsx
@@ -510,6 +676,31 @@ Both router wirings (TanStack Router, and a custom named router) are in [`RECIPE
510
676
 
511
677
  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
678
 
679
+ **Selection summarised in a `MultiCombobox`** — `renderChips` renders the chip area from the
680
+ selected values, for the shape no per-value accessor can express: one node standing for the whole
681
+ selection. `Combobox.Chip` builds a removable chip for the values that still get one, and Base UI
682
+ removes the chip at an index, so render one per entry of `value`, in order.
683
+
684
+ ```tsx
685
+ <MultiCombobox
686
+ items={tables}
687
+ value={selected}
688
+ onValueChange={save}
689
+ renderChips={(values) =>
690
+ values.length === 1 ? (
691
+ <Combobox.Chip>{labelOf(values[0])}</Combobox.Chip>
692
+ ) : (
693
+ <Badge color="blue">{values.length} tables selected</Badge>
694
+ )
695
+ }
696
+ />
697
+ ```
698
+
699
+ A chip that merely holds a link or an action of its own does not need this — `itemToChipLabel`
700
+ takes the node, and the chip lets it have the keyboard.
701
+
702
+ **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.
703
+
513
704
  **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
705
 
515
706
  ```tsx