@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.
- package/AGENTS.md +191 -1
- package/CHANGELOG.md +358 -0
- package/COMPONENT-API.md +68 -47
- package/dist/{AIKit-DDg5DBCG.js → AIKit-CNX6aiKm.js} +208 -62
- package/dist/AIKit-CNX6aiKm.js.map +1 -0
- package/dist/{Alert-DHJFrWnS.js → Alert-Ba5b2YPq.js} +2 -2
- package/dist/{Alert-DHJFrWnS.js.map → Alert-Ba5b2YPq.js.map} +1 -1
- package/dist/{Badge-Ch26CXoP.js → Badge-D0pXdekt.js} +2 -2
- package/dist/Badge-D0pXdekt.js.map +1 -0
- package/dist/{BrandEditor-Go7w8Zj2.js → BrandEditor-CSANsxUz.js} +6 -6
- package/dist/{BrandEditor-Go7w8Zj2.js.map → BrandEditor-CSANsxUz.js.map} +1 -1
- package/dist/{BrandLogo-Dmi7jo21.js → BrandLogo-CoKBk4MI.js} +4 -4
- package/dist/BrandLogo-CoKBk4MI.js.map +1 -0
- package/dist/{Breadcrumb-uEkM6adI.js → Breadcrumb-g_xn_-Nf.js} +2 -2
- package/dist/{Breadcrumb-uEkM6adI.js.map → Breadcrumb-g_xn_-Nf.js.map} +1 -1
- package/dist/{Checkbox-fC0BuO2B.js → Checkbox-CUIn2jap.js} +2 -2
- package/dist/{Checkbox-fC0BuO2B.js.map → Checkbox-CUIn2jap.js.map} +1 -1
- package/dist/{Clipboard-ChPdiUy3.js → Clipboard-BTW6C-b_.js} +2 -2
- package/dist/{Clipboard-ChPdiUy3.js.map → Clipboard-BTW6C-b_.js.map} +1 -1
- package/dist/{ClipboardButton-D9pDjgkB.js → ClipboardButton-CwZJdRaz.js} +2 -2
- package/dist/{ClipboardButton-D9pDjgkB.js.map → ClipboardButton-CwZJdRaz.js.map} +1 -1
- package/dist/{CodeEditor-Cxy3HnKH.js → CodeEditor-C9zy7PxK.js} +6 -6
- package/dist/{CodeEditor-Cxy3HnKH.js.map → CodeEditor-C9zy7PxK.js.map} +1 -1
- package/dist/{ColorInput-Bxj-1eLT.js → ColorInput-BZV-u_94.js} +3 -3
- package/dist/{ColorInput-Bxj-1eLT.js.map → ColorInput-BZV-u_94.js.map} +1 -1
- package/dist/{Combobox-Cs_1pj2q.js → Combobox-BfIfaVMA.js} +273 -104
- package/dist/Combobox-BfIfaVMA.js.map +1 -0
- package/dist/{Command-B7ML9YMv.js → Command-Bfa1kpbt.js} +2 -2
- package/dist/{Command-B7ML9YMv.js.map → Command-Bfa1kpbt.js.map} +1 -1
- package/dist/{CopyField-BO_sh-HO.js → CopyField-DpT0Ngz1.js} +2 -2
- package/dist/{CopyField-BO_sh-HO.js.map → CopyField-DpT0Ngz1.js.map} +1 -1
- package/dist/{DateInput-ZdaMQo5i.js → DateInput-DiEd5uCL.js} +3 -3
- package/dist/{DateInput-ZdaMQo5i.js.map → DateInput-DiEd5uCL.js.map} +1 -1
- package/dist/{DateRangeInput-DkvNh_sT.js → DateRangeInput-DPfVYchl.js} +3 -3
- package/dist/{DateRangeInput-DkvNh_sT.js.map → DateRangeInput-DPfVYchl.js.map} +1 -1
- package/dist/{FieldItem-CCd9-xnp.js → FieldItem-B7XMmZSz.js} +3 -3
- package/dist/{FieldItem-CCd9-xnp.js.map → FieldItem-B7XMmZSz.js.map} +1 -1
- package/dist/{Form-k6PH-em3.js → Form-CsttTHn4.js} +7 -7
- package/dist/Form-CsttTHn4.js.map +1 -0
- package/dist/{FormGroup-DbcDtrgU.js → FormGroup-DHvTFdno.js} +3 -3
- package/dist/{FormGroup-DbcDtrgU.js.map → FormGroup-DHvTFdno.js.map} +1 -1
- package/dist/{InputGroup-BnLHY0eq.js → InputGroup-DcZRlVnN.js} +22 -14
- package/dist/InputGroup-DcZRlVnN.js.map +1 -0
- package/dist/{Kbd-CvSM3D3-.js → Kbd-Cvvt91Q8.js} +14 -4
- package/dist/Kbd-Cvvt91Q8.js.map +1 -0
- package/dist/{MermaidDiagram-WkonYErO.js → MermaidDiagram-CXG5tC6J.js} +19 -18
- package/dist/MermaidDiagram-CXG5tC6J.js.map +1 -0
- package/dist/{MultiActionButtonGroup-CxWH7yWK.js → MultiActionButtonGroup-DokHbPgM.js} +2 -2
- package/dist/{MultiActionButtonGroup-CxWH7yWK.js.map → MultiActionButtonGroup-DokHbPgM.js.map} +1 -1
- package/dist/{Navbar-B2UsolVU.js → Navbar-Liy1TPI7.js} +6 -6
- package/dist/{Navbar-B2UsolVU.js.map → Navbar-Liy1TPI7.js.map} +1 -1
- package/dist/PageHeader-Q3IpCnQd.js.map +1 -1
- package/dist/{PageToolbar-DnbFt14Q.js → PageToolbar-DExqEYGl.js} +2 -2
- package/dist/{PageToolbar-DnbFt14Q.js.map → PageToolbar-DExqEYGl.js.map} +1 -1
- package/dist/{Popconfirm-CILjmzKI.js → Popconfirm-5pBzT_HQ.js} +10 -11
- package/dist/{Popconfirm-CILjmzKI.js.map → Popconfirm-5pBzT_HQ.js.map} +1 -1
- package/dist/{RadioGroup-XFzFd1ti.js → RadioGroup-DXPbSj8P.js} +2 -2
- package/dist/{RadioGroup-XFzFd1ti.js.map → RadioGroup-DXPbSj8P.js.map} +1 -1
- package/dist/{Search-4Bxtt8hS.js → Search-5uQAiNIP.js} +2 -2
- package/dist/{Search-4Bxtt8hS.js.map → Search-5uQAiNIP.js.map} +1 -1
- package/dist/{SearchInput-f_oY9T4Z.js → SearchInput-D4UgAXJK.js} +3 -3
- package/dist/{SearchInput-f_oY9T4Z.js.map → SearchInput-D4UgAXJK.js.map} +1 -1
- package/dist/{SectionNav-BpJlisbT.js → SectionNav-OkKCjSA-.js} +3 -3
- package/dist/{SectionNav-BpJlisbT.js.map → SectionNav-OkKCjSA-.js.map} +1 -1
- package/dist/{Select-DpcgKqKB.js → Select-DV3Hak_l.js} +3 -4
- package/dist/Select-DV3Hak_l.js.map +1 -0
- package/dist/{Sidebar-Cm6I4dE1.js → Sidebar-D3cY1kha.js} +44 -20
- package/dist/Sidebar-D3cY1kha.js.map +1 -0
- package/dist/{Slider-pPC_3gz9.js → Slider-CRf-bB4y.js} +2 -2
- package/dist/{Slider-pPC_3gz9.js.map → Slider-CRf-bB4y.js.map} +1 -1
- package/dist/{Switch-ChfPKcTE.js → Switch-B0WdGSjz.js} +7 -7
- package/dist/{Switch-ChfPKcTE.js.map → Switch-B0WdGSjz.js.map} +1 -1
- package/dist/{TextInput-DHM6dHmn.js → TextInput-DH5SkbsE.js} +3 -3
- package/dist/{TextInput-DHM6dHmn.js.map → TextInput-DH5SkbsE.js.map} +1 -1
- package/dist/{ThemeProvider-DrQJtzpd.js → ThemeProvider-2RMdVGxO.js} +17 -12
- package/dist/{ThemeProvider-DrQJtzpd.js.map → ThemeProvider-2RMdVGxO.js.map} +1 -1
- package/dist/{Timeline-6btyJl4D.js → Timeline-BG5Xogxh.js} +11 -11
- package/dist/{Timeline-6btyJl4D.js.map → Timeline-BG5Xogxh.js.map} +1 -1
- package/dist/{Workbench-BzE-Vvd9.js → Workbench-UR2FBC7I.js} +3 -1
- package/dist/Workbench-UR2FBC7I.js.map +1 -0
- package/dist/{ZoomableSvg-BWnDkR61.js → ZoomableSvg-pooPb1Cu.js} +3 -3
- package/dist/{ZoomableSvg-BWnDkR61.js.map → ZoomableSvg-pooPb1Cu.js.map} +1 -1
- package/dist/editors.js +4 -4
- package/dist/flint.js +134 -8
- package/dist/flint.js.map +1 -1
- package/dist/index.d.ts +537 -135
- package/dist/index.js +40 -39
- package/dist/internal/blocks/AIKit.js +2 -2
- package/dist/internal/blocks/AuthLayout.js +118 -0
- package/dist/internal/blocks/AuthLayout.js.map +1 -0
- package/dist/internal/blocks/BrandEditor.js +1 -1
- package/dist/internal/blocks/ClipboardButton.js +1 -1
- package/dist/internal/blocks/ColorInput.js +1 -1
- package/dist/internal/blocks/Command.js +1 -1
- package/dist/internal/blocks/CopyField.js +1 -1
- package/dist/internal/blocks/DateInput.js +1 -1
- package/dist/internal/blocks/DateRangeInput.js +1 -1
- package/dist/internal/blocks/ExportMenu.js +1 -1
- package/dist/internal/blocks/Form/connectors/MultiComboboxField.js +1 -1
- package/dist/internal/blocks/Form.js +1 -1
- package/dist/internal/blocks/FormGroup.js +1 -1
- package/dist/internal/blocks/MultiActionButtonGroup.js +1 -1
- package/dist/internal/blocks/MultiCombobox.js +107 -48
- package/dist/internal/blocks/MultiCombobox.js.map +1 -1
- package/dist/internal/blocks/NumberInput.js +1 -1
- package/dist/internal/blocks/PageToolbar.js +1 -1
- package/dist/internal/blocks/PasswordInput.js +2 -2
- package/dist/internal/blocks/SearchInput.js +1 -1
- package/dist/internal/blocks/SearchInputWithNavigation.js +2 -2
- package/dist/internal/blocks/SectionNav.js +1 -1
- package/dist/internal/blocks/TextInput.js +1 -1
- package/dist/internal/blocks/Textarea.js +1 -1
- package/dist/internal/blocks/TimeInput.js +2 -2
- package/dist/internal/components/Alert.js +1 -1
- package/dist/internal/components/Avatar.js +1 -1
- package/dist/internal/components/Badge.js +1 -1
- package/dist/internal/components/BrandLogo.js +1 -1
- package/dist/internal/components/Breadcrumb.js +1 -1
- package/dist/internal/components/Checkbox.js +1 -1
- package/dist/internal/components/Clipboard.js +1 -1
- package/dist/internal/components/CollapsiblePanel.js +1 -1
- package/dist/internal/components/Combobox.js +2 -2
- package/dist/internal/components/InputGroup.js +1 -1
- package/dist/internal/components/Kbd.js +1 -1
- package/dist/internal/components/Navbar.js +1 -1
- package/dist/internal/components/Popconfirm.js +1 -1
- package/dist/internal/components/RadioGroup.js +1 -1
- package/dist/internal/components/Search.js +1 -1
- package/dist/internal/components/Select.js +1 -1
- package/dist/internal/components/Sidebar.js +1 -1
- package/dist/internal/components/Slider.js +1 -1
- package/dist/internal/components/Switch.js +1 -1
- package/dist/internal/components/ThemeProvider.js +1 -1
- package/dist/internal/components/Timeline.js +1 -1
- package/dist/internal/components/Tooltip.js +1 -1
- package/dist/internal/components/Truncated.js +1 -1
- package/dist/internal/components/Workbench.js +1 -1
- package/dist/internal/components/ZoomableSvg.js +1 -1
- package/dist/internal/utils.js +2 -2
- package/dist/markdown-editor.js +29 -23
- package/dist/markdown-editor.js.map +1 -1
- package/dist/mermaid.js +1 -1
- package/dist/rjsf.js +22 -23
- package/dist/rjsf.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/testing.js +1 -1
- package/dist/{useColorSchemeState-klihG9-i.js → themeScopeContext-D7fSyb06.js} +16 -3
- package/dist/themeScopeContext-D7fSyb06.js.map +1 -0
- package/dist/{themeTokens-D70HeNVF.js → themeTokens-qP1-55O7.js} +37 -10
- package/dist/themeTokens-qP1-55O7.js.map +1 -0
- package/dist/{utils-B5Z52hYs.js → utils-DSvrhU8E.js} +6 -2
- package/dist/{utils-B5Z52hYs.js.map → utils-DSvrhU8E.js.map} +1 -1
- package/dist/vega.d.ts +1 -1
- package/dist/vega.js +3 -3
- package/dist/vega.js.map +1 -1
- package/dist/{vegaTheme-DfoEQhOy.js → vegaTheme-CSvFEMBl.js} +6 -6
- package/dist/vegaTheme-CSvFEMBl.js.map +1 -0
- package/package.json +12 -10
- package/src/styles.css +3 -0
- package/dist/AIKit-DDg5DBCG.js.map +0 -1
- package/dist/Badge-Ch26CXoP.js.map +0 -1
- package/dist/BrandLogo-Dmi7jo21.js.map +0 -1
- package/dist/Combobox-Cs_1pj2q.js.map +0 -1
- package/dist/Form-k6PH-em3.js.map +0 -1
- package/dist/InputGroup-BnLHY0eq.js.map +0 -1
- package/dist/Kbd-CvSM3D3-.js.map +0 -1
- package/dist/MermaidDiagram-WkonYErO.js.map +0 -1
- package/dist/Select-DpcgKqKB.js.map +0 -1
- package/dist/Sidebar-Cm6I4dE1.js.map +0 -1
- package/dist/Workbench-BzE-Vvd9.js.map +0 -1
- package/dist/themeTokens-D70HeNVF.js.map +0 -1
- package/dist/useColorSchemeState-klihG9-i.js.map +0 -1
- 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
|