@keboola/design 11.0.1 → 11.2.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 (153) hide show
  1. package/AGENTS.md +16 -1
  2. package/CHANGELOG.md +2138 -0
  3. package/COMPONENT-API.md +40 -1
  4. package/dist/{AIKit-Dre37SJr.js → AIKit-DiS0CVOW.js} +6 -6
  5. package/dist/{AIKit-Dre37SJr.js.map → AIKit-DiS0CVOW.js.map} +1 -1
  6. package/dist/{AlertModal-BUC4Es8o.js → AlertModal-Dp_XuJBW.js} +3 -6
  7. package/dist/{AlertModal-BUC4Es8o.js.map → AlertModal-Dp_XuJBW.js.map} +1 -1
  8. package/dist/{BrandEditor-B8o9zdZO.js → BrandEditor-DyzgfVkl.js} +6 -6
  9. package/dist/{BrandEditor-B8o9zdZO.js.map → BrandEditor-DyzgfVkl.js.map} +1 -1
  10. package/dist/{Breadcrumb-DJp7WwSv.js → Breadcrumb-Frst8MTA.js} +3 -3
  11. package/dist/{Breadcrumb-DJp7WwSv.js.map → Breadcrumb-Frst8MTA.js.map} +1 -1
  12. package/dist/{Button-nzmDNS2q.js → Button-6xBX0nNl.js} +15 -15
  13. package/dist/Button-6xBX0nNl.js.map +1 -0
  14. package/dist/{ClipboardButton-ez5V3fkk.js → ClipboardButton-ClcluT0C.js} +2 -2
  15. package/dist/{ClipboardButton-ez5V3fkk.js.map → ClipboardButton-ClcluT0C.js.map} +1 -1
  16. package/dist/{CodeEditor-BfQJfbhL.js → CodeEditor-DyaN-Ij2.js} +58 -10
  17. package/dist/CodeEditor-DyaN-Ij2.js.map +1 -0
  18. package/dist/{ColorInput-Dx8Dsv7Y.js → ColorInput-B8z4j4Y_.js} +2 -2
  19. package/dist/{ColorInput-Dx8Dsv7Y.js.map → ColorInput-B8z4j4Y_.js.map} +1 -1
  20. package/dist/{ColorModeSwitch-CCqj7tjE.js → ColorModeSwitch-dZFZyqy1.js} +3 -3
  21. package/dist/{ColorModeSwitch-CCqj7tjE.js.map → ColorModeSwitch-dZFZyqy1.js.map} +1 -1
  22. package/dist/{Combobox-c_xBjf0P.js → Combobox-Dgm6Z4ib.js} +9 -5
  23. package/dist/Combobox-Dgm6Z4ib.js.map +1 -0
  24. package/dist/{Command-CQ_bQsJp.js → Command-C0aZfVTq.js} +2 -2
  25. package/dist/{Command-CQ_bQsJp.js.map → Command-C0aZfVTq.js.map} +1 -1
  26. package/dist/{DateInput-CMD3dY-T.js → DateInput-B3UdzRfW.js} +3 -3
  27. package/dist/{DateInput-CMD3dY-T.js.map → DateInput-B3UdzRfW.js.map} +1 -1
  28. package/dist/{DatePicker-BbFbmsGl.js → DatePicker-Dw3DpbUh.js} +2 -2
  29. package/dist/{DatePicker-BbFbmsGl.js.map → DatePicker-Dw3DpbUh.js.map} +1 -1
  30. package/dist/{DateRangeInput-D3BhiKyk.js → DateRangeInput-DTuySEiq.js} +3 -3
  31. package/dist/{DateRangeInput-D3BhiKyk.js.map → DateRangeInput-DTuySEiq.js.map} +1 -1
  32. package/dist/{DateRangePicker-BiUgtt_M.js → DateRangePicker-DCmMHg4s.js} +4 -4
  33. package/dist/{DateRangePicker-BiUgtt_M.js.map → DateRangePicker-DCmMHg4s.js.map} +1 -1
  34. package/dist/{DropdownMenu-B6ZbYvaM.js → DropdownMenu-t90tVw6l.js} +8 -8
  35. package/dist/DropdownMenu-t90tVw6l.js.map +1 -0
  36. package/dist/FieldItem-CAPj9lj3.d.ts +111 -0
  37. package/dist/FieldItem-D-h1NeBo.js +108 -0
  38. package/dist/FieldItem-D-h1NeBo.js.map +1 -0
  39. package/dist/{FileUpload-sXRv7JiY.js → FileUpload-BRv0TaZk.js} +2 -2
  40. package/dist/{FileUpload-sXRv7JiY.js.map → FileUpload-BRv0TaZk.js.map} +1 -1
  41. package/dist/{Form-BDCef6O9.js → Form-DHvcFoEi.js} +18 -78
  42. package/dist/{Form-BDCef6O9.js.map → Form-DHvcFoEi.js.map} +1 -1
  43. package/dist/{FormGroup-S9mPNHKH.js → FormGroup-C41n65cE.js} +7 -7
  44. package/dist/FormGroup-C41n65cE.js.map +1 -0
  45. package/dist/{FullScreenModal-CJa2VkJ2.js → FullScreenModal-Cb3PGgh4.js} +2 -2
  46. package/dist/{FullScreenModal-CJa2VkJ2.js.map → FullScreenModal-Cb3PGgh4.js.map} +1 -1
  47. package/dist/{InputGroup-BJWsI35s.js → InputGroup-CdTq8H7c.js} +2 -2
  48. package/dist/{InputGroup-BJWsI35s.js.map → InputGroup-CdTq8H7c.js.map} +1 -1
  49. package/dist/{MermaidDiagram-BB06gymm.js → MermaidDiagram-ChL3WIQT.js} +2 -2
  50. package/dist/{MermaidDiagram-BB06gymm.js.map → MermaidDiagram-ChL3WIQT.js.map} +1 -1
  51. package/dist/{Modal-BA5Wc3sx.js → Modal-wqG8nJiK.js} +2 -2
  52. package/dist/{Modal-BA5Wc3sx.js.map → Modal-wqG8nJiK.js.map} +1 -1
  53. package/dist/{MultiActionButtonGroup-CiBHxsSk.js → MultiActionButtonGroup-BiIIubiL.js} +2 -2
  54. package/dist/{MultiActionButtonGroup-CiBHxsSk.js.map → MultiActionButtonGroup-BiIIubiL.js.map} +1 -1
  55. package/dist/{Navbar-t16usFZQ.js → Navbar-BfaPeLUL.js} +5 -2
  56. package/dist/Navbar-BfaPeLUL.js.map +1 -0
  57. package/dist/{PageToolbar-0_1OBzaZ.js → PageToolbar-OQg3mPF-.js} +4 -4
  58. package/dist/PageToolbar-OQg3mPF-.js.map +1 -0
  59. package/dist/{Pagination-CM_Rszz-.js → Pagination-Cjfda8KG.js} +2 -2
  60. package/dist/{Pagination-CM_Rszz-.js.map → Pagination-Cjfda8KG.js.map} +1 -1
  61. package/dist/{Popconfirm-BGe1jLov.js → Popconfirm-D7_vfxFd.js} +4 -7
  62. package/dist/{Popconfirm-BGe1jLov.js.map → Popconfirm-D7_vfxFd.js.map} +1 -1
  63. package/dist/{Popover-Ca0V6AD1.js → Popover-CzeHjvrB.js} +6 -13
  64. package/dist/Popover-CzeHjvrB.js.map +1 -0
  65. package/dist/{SearchInput-D2fcOv7E.js → SearchInput-C7dF_Ir-.js} +2 -2
  66. package/dist/{SearchInput-D2fcOv7E.js.map → SearchInput-C7dF_Ir-.js.map} +1 -1
  67. package/dist/{SectionNav--7bw2b6u.js → SectionNav-8NoTOKO8.js} +3 -3
  68. package/dist/{SectionNav--7bw2b6u.js.map → SectionNav-8NoTOKO8.js.map} +1 -1
  69. package/dist/{Select-4YX38G5h.js → Select-Is0VkePH.js} +2 -2
  70. package/dist/{Select-4YX38G5h.js.map → Select-Is0VkePH.js.map} +1 -1
  71. package/dist/{Sheet-BQrX8-h5.js → Sheet-Dw9BDvjI.js} +2 -2
  72. package/dist/{Sheet-BQrX8-h5.js.map → Sheet-Dw9BDvjI.js.map} +1 -1
  73. package/dist/{Sidebar-ROaR_AqC.js → Sidebar-Dcb4rinA.js} +2 -2
  74. package/dist/{Sidebar-ROaR_AqC.js.map → Sidebar-Dcb4rinA.js.map} +1 -1
  75. package/dist/{Slider-Da_3mS5v.js → Slider-BOPfMOMm.js} +2 -2
  76. package/dist/{Slider-Da_3mS5v.js.map → Slider-BOPfMOMm.js.map} +1 -1
  77. package/dist/{Tabs-EAihyqCt.js → Tabs-UMQuDoW8.js} +2 -2
  78. package/dist/{Tabs-EAihyqCt.js.map → Tabs-UMQuDoW8.js.map} +1 -1
  79. package/dist/{TextInput-BGed-V13.js → TextInput-CMuN1WfY.js} +2 -2
  80. package/dist/{TextInput-BGed-V13.js.map → TextInput-CMuN1WfY.js.map} +1 -1
  81. package/dist/{TimePicker-B_NegL-7.js → TimePicker-D9zktB8A.js} +2 -2
  82. package/dist/{TimePicker-B_NegL-7.js.map → TimePicker-D9zktB8A.js.map} +1 -1
  83. package/dist/{ZoomableSvg-DG9PnAm8.js → ZoomableSvg-CZEu6sAq.js} +3 -3
  84. package/dist/{ZoomableSvg-DG9PnAm8.js.map → ZoomableSvg-CZEu6sAq.js.map} +1 -1
  85. package/dist/editors.d.ts +71 -9
  86. package/dist/editors.js +58 -3
  87. package/dist/editors.js.map +1 -1
  88. package/dist/index.d.ts +314 -312
  89. package/dist/index.js +41 -40
  90. package/dist/internal/blocks/AIKit.js +1 -1
  91. package/dist/internal/blocks/BrandEditor.js +1 -1
  92. package/dist/internal/blocks/ClipboardButton.js +1 -1
  93. package/dist/internal/blocks/ColorInput.js +1 -1
  94. package/dist/internal/blocks/ColorModeSwitch.js +1 -1
  95. package/dist/internal/blocks/Command.js +1 -1
  96. package/dist/internal/blocks/DateInput.js +1 -1
  97. package/dist/internal/blocks/DateRangeInput.js +1 -1
  98. package/dist/internal/blocks/ExportMenu.js +2 -2
  99. package/dist/internal/blocks/Form/connectors/MultiComboboxField.js +56 -0
  100. package/dist/internal/blocks/Form/connectors/MultiComboboxField.js.map +1 -0
  101. package/dist/internal/blocks/Form.js +1 -1
  102. package/dist/internal/blocks/FormGroup.js +1 -1
  103. package/dist/internal/blocks/MultiActionButtonGroup.js +1 -1
  104. package/dist/internal/blocks/MultiCombobox.js +8 -3
  105. package/dist/internal/blocks/MultiCombobox.js.map +1 -1
  106. package/dist/internal/blocks/NumberInput.js +1 -1
  107. package/dist/internal/blocks/PageToolbar.js +1 -1
  108. package/dist/internal/blocks/PasswordInput.js +1 -1
  109. package/dist/internal/blocks/SearchInput.js +1 -1
  110. package/dist/internal/blocks/SearchInputWithNavigation.js +2 -2
  111. package/dist/internal/blocks/SectionNav.js +1 -1
  112. package/dist/internal/blocks/SplitButton.js +2 -2
  113. package/dist/internal/blocks/TextInput.js +1 -1
  114. package/dist/internal/blocks/Textarea.js +1 -1
  115. package/dist/internal/blocks/TimeInput.js +2 -2
  116. package/dist/internal/components/AlertModal.js +1 -1
  117. package/dist/internal/components/Breadcrumb.js +1 -1
  118. package/dist/internal/components/Button.js +1 -1
  119. package/dist/internal/components/Combobox.js +1 -1
  120. package/dist/internal/components/DatePicker.js +1 -1
  121. package/dist/internal/components/DateRangePicker.js +1 -1
  122. package/dist/internal/components/DropdownMenu.js +1 -1
  123. package/dist/internal/components/FileUpload.js +1 -1
  124. package/dist/internal/components/FullScreenModal.js +1 -1
  125. package/dist/internal/components/InputGroup.js +1 -1
  126. package/dist/internal/components/Modal.js +1 -1
  127. package/dist/internal/components/Navbar.js +1 -1
  128. package/dist/internal/components/Pagination.js +1 -1
  129. package/dist/internal/components/Popconfirm.js +1 -1
  130. package/dist/internal/components/Popover.js +1 -1
  131. package/dist/internal/components/Select.js +1 -1
  132. package/dist/internal/components/Sheet.js +1 -1
  133. package/dist/internal/components/Sidebar.js +1 -1
  134. package/dist/internal/components/Slider.js +1 -1
  135. package/dist/internal/components/Tabs.js +1 -1
  136. package/dist/internal/components/TimePicker.js +1 -1
  137. package/dist/internal/components/ZoomableSvg.js +1 -1
  138. package/dist/markdown-editor.js +5 -5
  139. package/dist/mermaid.js +1 -1
  140. package/dist/rjsf.d.ts +13 -1
  141. package/dist/rjsf.js +233 -173
  142. package/dist/rjsf.js.map +1 -1
  143. package/dist/styles.css +1 -1
  144. package/package.json +7 -6
  145. package/dist/Button-nzmDNS2q.js.map +0 -1
  146. package/dist/CodeEditor-BfQJfbhL.js.map +0 -1
  147. package/dist/Combobox-c_xBjf0P.js.map +0 -1
  148. package/dist/DropdownMenu-B6ZbYvaM.js.map +0 -1
  149. package/dist/FormGroup-S9mPNHKH.js.map +0 -1
  150. package/dist/Navbar-t16usFZQ.js.map +0 -1
  151. package/dist/PageToolbar-0_1OBzaZ.js.map +0 -1
  152. package/dist/Popover-Ca0V6AD1.js.map +0 -1
  153. package/dist/constants-D6JcsD4_.d.ts +0 -41
package/CHANGELOG.md ADDED
@@ -0,0 +1,2138 @@
1
+ # @keboola/design
2
+
3
+ ## 11.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `AlertModal` and `Popconfirm` close an async confirm on its outcome, not on it settling.
8
+
9
+ Both wrapped a thenable `onConfirm` in `.finally`, which runs on rejection too. A confirm action that threw produced exactly what the user sees on success — the dialog or popover disappeared, with no error and no other trace. The destructive thing they had just confirmed silently had not happened, and nothing at the design-system level told the author that each call site owed its own `try`/`catch`.
10
+
11
+ Both now close only once the action resolves; a rejected action leaves the surface up so the failure stays visible. The busy state still clears either way, so a failed action can be retried or cancelled rather than leaving a stuck spinner.
12
+
13
+ The rejection is still left unhandled and reaches the host's global handler, unchanged from before — `.finally` propagated it too. Catching it here would have stopped it being reported.
14
+
15
+ This changes behaviour for every consumer passing an async `onConfirm`: 15 non-demo `Popconfirm` roots and the async `AlertModal` sites across `apps/kbc-ui`, `apps/developer-portal` and `@keboola/auth`. Consumers that already surface their own error now show it with the surface still open. `skipAsyncWait` is unaffected and still closes on click without waiting.
16
+
17
+ ### Patch Changes
18
+
19
+ - Fix `FieldItem` hiding an error on the first submit after `form.reset()`. The reveal gate compares `submissionAttempts` against the count captured when the field mounted, and `reset()` puts the counter back to 0 without unmounting anything — so a field that had already counted one submit was left with a baseline the counter had to climb past twice. The visible result was a Save that appeared to do nothing: submit blocked, no message anywhere, and a second press needed before the field explained itself. The baseline now follows the counter down.
20
+
21
+ ## 11.1.0
22
+
23
+ ### Minor Changes
24
+
25
+ - `RjsfSchemaEditor` honours `options.hidden` on every field, not only on the ones no widget claims.
26
+
27
+ `generateUiSchema` encoded hiddenness as `ui:widget: 'hidden'`, the one channel RJSF reads. Twelve later branches in the same loop assign `ui:widget` — `sync-action`, `test-connection`, `password`, `textarea`, `code-editor`, `checkboxes`, `radio`, `select`, `dynamic-select`, `async-select`, `async-multi-select`, `template-watch` — and each one erased the fact that the schema asked for the field to be hidden.
28
+
29
+ `ui:options.hidden` is now written last, after every widget branch, so it survives all of them. The templates read that instead of comparing widget names.
30
+
31
+ The visible effect on the published component schemas: seven `sync-action` buttons across `keboola.app-embeddings-v2`, `keboola.wr-pgvector-embeddings`, `keboola.wr-pinecone-embeddings` and `keboola.wr-qdrant-embeddings` set `options.hidden` and rendered a "Test Connection" button anyway. They no longer do — which is what the legacy `@json-editor/json-editor` did with them, since its object editor hid any child whose `options.hidden` was set.
32
+
33
+ Hidden fields stay mounted inside a `display: none` wrapper, matching json-editor: a hidden leaf's mount-phase `onChange` writes the schema default that sibling `options.dependencies` resolve against. Hidden containers are dropped from the tree instead, so large `_metadata_` subtrees never mount — which now also covers an object- or array-typed `template`/`watch` field, whose widget RJSF never runs anyway.
34
+
35
+ Three layouts that ignored `options.hidden` entirely now respect it. A hidden field inside a `format: grid` object no longer leaves a phantom grid cell; a hidden category inside a `format: tabs` object no longer gets an empty tab, which could even be the tab selected on load; and a hidden property of a `format: table` array's items no longer gets a column header and a row of empty cells (new rows still seed its default).
36
+
37
+ - `RjsfSchemaEditor` takes `rootContext` — read-only entries exposed to the schema's `watch` paths alongside the form data.
38
+
39
+ It merges one level deep over the root form data, so an entry the form itself carries keeps the keys `rootContext` does not name. It reaches no widget's `formData`, no validation and no `onChange`.
40
+
41
+ Host apps mixed this kind of context into `value` instead, which put it inside the input the editor uses for external-change detection: a change to it counted as an external edit, so the form remounted from `value` and the pending debounced write was cancelled — discarding whatever the user had typed. Context that changes on its own schedule, like a job writing configuration state, now re-renders the widgets reading it and nothing else.
42
+
43
+ A **visible** `template`/`watch` field whose every watched path reads a key `rootContext` declares also stops writing its computed value into the form data. Such a field views context the configuration does not own, so persisting it would copy that context into the saved configuration on every save. Three things follow:
44
+ - Fields computed from the configuration's own values keep writing — a URL assembled from a `domain` the user typed is read back from the saved configuration by the component.
45
+ - Keys the form itself owns under the same top-level name keep writing too; only what `rootContext` declares is treated as a view.
46
+ - Hidden `template`/`watch` helpers always write, which is what feeds `options.dependencies`.
47
+
48
+ Declare a stable set of keys, empty entries included: a key that comes and goes flips such a field between viewing its value and saving it.
49
+
50
+ - Two new form field wrappers for `useAppForm`: **`MultiComboboxField`** (from `@keboola/design`) and **`CodeEditorField`** (from `@keboola/design/editors`).
51
+
52
+ Neither is registered on `field.*`. `fieldComponents` is an object literal handed to `createFormHook`, so no bundler shakes it per key: whatever is registered there is carried by every `useAppForm` consumer, including ones that only render a `TextField`. Both read the field from context, so composition inside `form.AppField` is identical either way — only the import differs.
53
+
54
+ `MultiComboboxField` holds a `string[]` — searchable, chips above the input, `allowCreate` for an enum the user may extend. It is locked to string items rather than generic over the item shape: a form value is what gets submitted, and a field holding `{ label, value }` objects would put presentation into the payload. Project labels with `itemToLabel`.
55
+
56
+ `CodeEditorField` holds the editor's **raw text**, never a parsed value. A JSON field is invalid for most of the time someone is typing in it, so parsing per keystroke would throw away either their text or their cursor — parse in the schema or at submit.
57
+
58
+ It ships from **`@keboola/design/editors`**: registering it on the root form hook pulled CodeMirror and its language modes (~170 kB brotlied) into `@keboola/design`'s main entry, which `size-limit` caught. Compose it as `<form.AppField name="schema">{() => <CodeEditorField label="Schema" />}</form.AppField>`.
59
+
60
+ `MultiComboboxField` is a named export of the main entry for the same reason at smaller scale: registering it cost `import { useAppForm }` **+45 kB brotlied** (134.93 → 179.90 kB), Base UI's combobox plus `@tanstack/react-virtual`, which `Combobox` imports at module scope. As a named export the weight is opt-in, and anyone rendering a multi-select already pays it through `MultiCombobox`. A new `import { useAppForm }` budget pins this at 140 kB — the existing `import *` budgets cannot see per-key shaking, because `*` retains everything by construction.
61
+
62
+ Both were previously unreachable: there was no multi-select or code connector, and `FieldItem` / `useFieldContext` are internal, so a consumer needing either had to hand-roll a `Label` + control pair and lose the label / required-marker / help-and-error-id wiring the connectors own. `apps/developer-portal`'s component edit form needs 4 multi-selects and 8 JSON fields.
63
+
64
+ Supporting changes:
65
+ - **`FieldItem` takes `labelling`** (`'for'` by default, or `'aria-labelledby'`). CodeMirror's editable node is a `div[role="textbox"]` — not a labelable element — so a `<label for>` aimed at it is invalid HTML and names nothing. Under `'aria-labelledby'` the label carries an id and the control points back at it. Additive; every existing connector keeps `for`. It stays off the connectors' own prop type: which mechanism names a control follows from what the control is, so the connector picks it, and a caller-supplied value would be ignored and then land in the rest spread — on `TextField` that reaches the DOM `<input>`.
66
+ - **`MultiCombobox` forwards `name`, `onBlur` and the `aria-describedby` / `aria-invalid` / `aria-required` set** to its input. Without the `aria-*` set a form connector cannot hand over the wiring it derives from the field, which is most of what makes a field usable with a screen reader; `aria-invalid` also drives the input group's error styling. `onBlur` is what lets a connector mark its field blurred, which is the gate `FieldItem` reveals errors on — the input is Base UI's own, so the prop had no way through before.
67
+ - **`CodeEditor` takes `onBlur`**, bound as a CodeMirror DOM handler on the content node rather than the wrapper, since the wrapper is not what gets focused. Installed only when the prop is passed, so an editor without it keeps the extension set it had. `SqlEditor` already exposed the same signal as `onBlurEditor`; the plain editor had none, which left `CodeEditorField` unable to report blur.
68
+ - **`CodeEditor` takes `ariaLabelledBy` / `ariaDescribedBy` / `ariaInvalid` / `ariaRequired`**, alongside the existing `ariaLabel`. All five now go onto CodeMirror's content node through one memoised `contentAttributes` extension — the wrapper `<div>` is not what gets focused, so it is not where AT reads them from.
69
+ - **The editor's string token becomes a light/dark pair.** `buildCustomTheme` now takes the `scheme` that `useThemedExtension` was already passing and discarding, and paints strings `warning[800]` in light, `warning[300]` in dark. One amber cannot clear AA on both grounds, and the editor sits on two surfaces per scheme: the theme's `background` setting is the page background, but `variant="field"` — which `CodeEditorField` defaults to — paints on `muted`, which the theme never sees. Light: `700` was 4.45:1 on the field surface (axe `color-contrast` fails it) and 4.67:1 plain; `800` is 6.89 / 7.23. Dark: `800` would be **1.99:1** on the field surface and 2.45:1 plain; `300` is 10.97 / 13.53. A dark/light story pair now measures the ratio rather than comparing a hex, which is what let the dark direction go unnoticed — Storybook pins `colorScheme: 'light'` and `a11y.test` is `'todo'`. The rest of the palette is still light-only in both schemes (UT-5194).
70
+ - **`CodeEditor` takes `allowBlankDocument`.** `jsonParseLinter` reports "Unexpected end of input" for empty content — right for a file, wrong for an _optional_ form field, where clearing the editor is how the value is removed. `CodeEditorField` passes `!required`; everything else keeps today's behaviour, deliberately: `ex-generic/JsonConfiguration.tsx`, `flows/TaskParamsModalContent.tsx` and `stream/PayloadTest.tsx` all gate a button on `isValidJsonConfig`, which is `false` for `''`, with no `disabledReason` — so the diagnostic is the only visible reason the button is dead. It is not derivable from `variant` either: `ex-generic/FunctionControl.tsx` is `variant="field"` and empty is an error there too. The wrapper (`skipBlankDocument`) is exported and tested; a story pins an optional and a required field side by side.
71
+
72
+ `size-limit` changes, all measured on a clean `dist` (a stale one inflates the whole-dist figure by ~12 kB):
73
+ - `editors` consumer budget 310 → 315 kB (measured 311.28 kB — the field wrapper pulls `FieldItem`'s label / help / tooltip chrome into that subpath) and its own-code budget 7.5 → 8 kB (7.08 → 7.87 kB).
74
+ - Whole ESM dist 330 → 332 kB (329.20 → 330.39 kB). Exporting `MultiComboboxField` by name makes it separately reachable, so its chunk merges less tightly — +1.19 kB shipped to save consumers 45 kB downloaded, which is the trade worth making.
75
+ - New budget: `import { useAppForm }` at 140 kB (measured 134.93 kB). This is the gap that let the connector's cost land unseen — every other consumer entry uses `import *`, which retains everything by construction and so can never catch a per-key shaking regression.
76
+
77
+ The root entry stays inside its 1.48 MB budget at 1.45 MB, which is the point of keeping both wrappers off `field.*`: registering `CodeEditorField` there took it to **1.65 MB**.
78
+
79
+ - Put the notification recipient pickers on the design combobox, so they follow the colour scheme.
80
+
81
+ Both pickers on a configuration's Notifications tab (colleagues/email and webhook URL) were the
82
+ legacy react-select, whose control filled with a fixed `#edf0f5` and whose menu was painted white by
83
+ its own emotion styles. Under `[data-theme='dark']` that left a light slab in a dark card and a white
84
+ menu whose option labels — inherited from the themed card — were white-on-white. They are now
85
+ `Combobox`, so the field, the popup and the recipient rows read the semantic tokens in both schemes;
86
+ the rows' fixed `@border` rule moves to `border-border` and drops out of `flow.less`.
87
+
88
+ `Combobox` gains two things the migration needed:
89
+ - **`canCreate`** — gates the `allowCreate` row on the typed query, so an entry that is not a valid
90
+ email address or URL shows the empty state instead of an "Add …" row the caller has to reject.
91
+ - **`itemToLabel` now receives the current query** as its second argument, so a custom item label can
92
+ highlight the match (`MarkedText`) instead of losing the highlight the legacy select had.
93
+
94
+ Typing and pressing Enter now commits the first match. Base UI highlights nothing until you press
95
+ ArrowDown, so Enter used to close the popup and clear the input without selecting anything — a typed
96
+ address was silently dropped. Every `Combobox` highlights its first match while filtering, which also
97
+ makes Enter commit the `allowCreate` row.
98
+
99
+ It also snaps its input back to the controlled value's label after **every** selection, not only when
100
+ the value changes. A combobox whose owner does not adopt the pick no longer shows something its value
101
+ does not say, which is what makes the "adder" shape work: pin `value` to `null` and each pick is
102
+ handed off while the input clears for the next one. The `Combobox` stories now hold their value in
103
+ React's `useState` rather than `storybook/internal/preview-api`'s, whose setter never re-rendered the
104
+ story — the controlled stories were only green because the old lenient input handling hid it.
105
+
106
+ ### Patch Changes
107
+
108
+ - `RjsfSchemaEditor` decides whether a `template`/`watch` field views `rootContext` by own properties only.
109
+
110
+ A plain-object lookup also answers for the prototype chain, so a path naming `toString`, `constructor` or any other inherited member counted as context and the field silently stopped saving its computed value.
111
+
112
+ - The legacy `Button` and `Popover` chrome follows the colour scheme.
113
+
114
+ `Button`'s legacy `outline` and `inline` variants, `ButtonInline`'s `dark` colour and
115
+ `Popover.Content`'s `default` surface pinned literals — `bg-white`, `text-neutral-800`,
116
+ `border-neutral-200`, `border-neutral-100` — so under `[data-theme='dark']` an outline button
117
+ stayed a white slab, a `color="dark"` inline button rendered near-black ink on a near-black
118
+ canvas, and every popover that had not opted into the argo surface was a white panel with dark
119
+ text. Only the argo variants of each were token-driven, but the brands that carry the theme
120
+ switcher render the legacy chrome.
121
+
122
+ Each literal maps to the semantic token that plays its role, so the light scheme shifts only
123
+ within a shade: `bg-card` / `bg-popover` (`#fff`, identical), `text-foreground` /
124
+ `text-popover-foreground` (`#1e293b` for `#222529`), `!border-input` (`#cbd5e1` for `#c5cbd6`),
125
+ `border-border` (`#e5e7eb` for `#edf0f5`), and `text-muted-foreground` on icons (`#64748b` for
126
+ `#7c8594`). `outline`'s hover fill, hover border and pressed fill become `muted-foreground`
127
+ tints over the base surface (`~#e8eaee` for `#edf0f5`, `~#a2acb9` for `#a2aab8`, `~#c9ced7` for
128
+ `#c5cbd6`) — `bg-muted` sits 4/255 from `bg-card` in the dark scheme, so a tint is what steps
129
+ visibly in both.
130
+
131
+ `outline`'s `aria-disabled` override moves with the base fill — it carries `!important`, so
132
+ leaving it on `#fff` would have kept disabled outline buttons white in the dark scheme.
133
+
134
+ - The legacy `DropdownMenu` chrome follows the colour scheme.
135
+
136
+ `DropdownMenu.Content`'s `default` surface pinned `bg-white` / `border-neutral-100` /
137
+ `text-neutral-800`, and its parts pinned the rest: item hover and focus (`bg-neutral-100`,
138
+ `text-neutral-900`), the separator rule (`bg-neutral-150`), item icons (`text-neutral-400`) and
139
+ the checkbox indicator (`bg-white`, `border-neutral-200`). Under `[data-theme='dark']` every menu
140
+ was a white panel with dark text — including `SplitButton`'s action list, which is a
141
+ `DropdownMenu`.
142
+
143
+ The panel now reads `bg-popover` / `text-popover-foreground` / `border-border` in both variants,
144
+ leaving radius (`rounded-md` vs `rounded-2xl`) as the only thing they differ in, and the parts
145
+ read `muted` / `foreground` / `border` / `input`. Light values shift only within a shade:
146
+ `#fff` identical, `#1e293b` for `#222529`, `#e5e7eb` for `#edf0f5`, the separator `#e5e7eb` for
147
+ `#d9dde5`, and the item highlight `#f1f5f9` for `#edf0f5`.
148
+
149
+ - Four fixes from a review pass over `apps/developer-portal`.
150
+
151
+ **`PageToolbar.Search` now stretches its search field.** The slot sizes itself between `min-w-64` and `max-w-md`, but never told its child to fill it — so a `SearchInput` stayed content-width (≈224px inside a 448px slot) and **truncated its own placeholder**. The visible symptom was `Search by name, ID or d…`, and the natural workaround was for every caller to hand-pick a width class, which is exactly what a sized slot exists to avoid. The first child now gets `flex-1` plus `min-w-0`, so a long value shrinks the input instead of overflowing the toolbar. Only the first — the documented composition is the search field followed by filter pills, and stretching every child would size the pills to match the input.
152
+
153
+ **`Navbar.MenuButton` styles the active page.** `Sidebar.MenuButton` has always styled `aria-current="page"`; the navbar equivalent had hover and `aria-expanded` states but nothing for the current page — so a top-nav-shelled app had no way to show where the user is, and the gap read as a caller mistake rather than a missing rule. Added to the `default` and `emphasized` variants, using `aria-current` (not a prop) because with `asChild` the slot is an `<a>`, which is where a router puts it.
154
+
155
+ Both are additive: a toolbar whose search field already carried a width keeps it (`className` still wins), and a navbar with no active link is unchanged.
156
+
157
+ **`FieldItem` reveals a field's error once that field has been blurred, or after a submit it was on screen for — not as soon as it is "touched".**
158
+
159
+ ⚠️ Behavioural, and it affects every form built on `useAppForm`. It is also a bug fix rather than a preference: `isTouched` does not mean "the user has been here". `FormApi.setFieldValue` sets it, so filling a field **for** the user marks it touched — as do `validateField` and the `validateAllFields` a submit runs. What that looked like in practice: registering a component, picking the type first put **"Required" under the ID field** — a control the user had not visited, written programmatically from name + type.
160
+
161
+ `isBlurred` is per-field and only a real blur sets it: the user was in this control and left it. The submit arm is scoped to the submits a field was mounted for, because `submissionAttempts` never goes back down — a bare `> 0` would mark every later-mounted control invalid on its first paint, so failing Save and then switching a type would greet the user with "Required" under fields they had not reached.
162
+
163
+ `setServerFieldErrors` marks `isBlurred` for the same reason: called from a mutation's `onError` there is no submit attempt to fall back on and no blur, so the message would otherwise sit in `errorMap` and render nowhere.
164
+
165
+ Consumers get strictly less noise; nothing that was hidden becomes visible. `FieldItem` now reads form context, which every call site in this repo already provides (`form.AppField` inside `form.AppForm`, as `Form/AGENTS.md` requires).
166
+
167
+ **`FormGroup.Error` deduplicates its messages.** TanStack keeps one error entry per **cause**, so a message arriving under two of them — a schema registered on `onChange` and `onBlur`, or a server error repeating what the client already said — rendered twice, as `RequiredRequired`. Two sources agreeing is still one thing to tell the reader. Deduping also retires the array-index `key` that list needed. (Register the schema under `onChange` alone: a second copy under `onBlur` leaves a stale entry in its own slot, which no dedupe can fix.)
168
+
169
+ - The published tarball now carries a `CHANGELOG.md`, so a consumer can read what changed in a release — breaking changes included — without leaving their `node_modules`.
170
+
171
+ It could not simply be added to `files`. This repo is private, so every reference `@changesets/changelog-github` emits is a dead link for anyone reading from npm: PR links, commit SHAs, author handles, and the Linear and cross-repo links that changeset prose carries. Across the publishable packages that came to 490 PR links, 863 commit links, 490 author credits and 102 dependency-bump blocks.
172
+
173
+ The published file is generated, not maintained. `scripts/public-changelog.mjs` removes those links ahead of the publish and leaves the prose. An identifier the author typed themselves stays as text — `UT-4009`, `connection#8040` — because it is part of the sentence and, without its URL, resolves to nothing outside Keboola. The repo-side `CHANGELOG.md` keeps every link, because that is how a release gets traced internally. It is rewritten only for the moment the tarballs are packed, then restored — which the release also depends on: `changesets/action` reads each changelog back off disk _after_ the publish command returns, to build that version's GitHub Release body, so without the restore the internal releases would carry the public text. A reference the rules do not cover fails the publish rather than shipping.
174
+
175
+ One incidental fix: a hex colour written as `#222529` in changeset prose had been autolinked into a link to issue 222529. Unwrapping restores the colour, so the published notes read as the author wrote them.
176
+
177
+ - Updated dependencies:
178
+ - @keboola/brand-registry@1.9.3
179
+ - @keboola/codemirror-lang-sql@0.7.2
180
+ - @keboola/tailwind-config@1.1.1
181
+
182
+ ## 11.0.1
183
+
184
+ ### Patch Changes
185
+
186
+ - Build with tsdown (rolldown) instead of the now-unmaintained tsup. Output layout, exports map, and shipped declarations are unchanged (attw-verified per package); chunk byte sizes shift slightly with rolldown's codegen, and `@keboola/design`'s size budgets are trued up to the new measurements (largest delta: the main bundle cap moves 320→330 KB). `@keboola/design` builds with `platform: 'browser'`, so bundled CJS deps (react-dropzone, react-day-picker) resolve to their ESM builds instead of a UMD `main` that would drag a Node-only runtime require into browser loads.
187
+
188
+ - Updated dependencies:
189
+ - @keboola/brand-registry@1.9.2
190
+ - @keboola/codemirror-lang-sql@0.7.1
191
+
192
+ ## 11.0.0
193
+
194
+ ### Major Changes
195
+
196
+ - Remove the deprecated `Collapse` component. Use `Collapsible` instead.
197
+
198
+ Every call site in this repo has been migrated and the export is gone from the barrel. The `collapseDown` animation and keyframe go with it — `Collapse` was their only consumer.
199
+
200
+ Migrating is not a rename; the two components differ in three ways:
201
+ - **Closed content is unmounted.** `Collapse` kept children mounted at zero height; Radix's `Collapsible` takes them out of the DOM. Anything that read, measured or held local state in collapsed content needs `forceMount` or a lift of that state.
202
+ - **The toggle moves to `Collapsible.Trigger`**, a real `<button>` with `aria-expanded`. It cannot wrap a header that already contains buttons — where the header carries its own actions, keep the existing click handler and drive `Collapsible` through `open` / `onOpenChange`.
203
+ - **`header` is a child, not a prop.** Render the header element directly, then `Collapsible.Content` for the body. `data-state` still lands on the root, so `group-data-[state=open]:` rules keep working once the root carries `tw:group`.
204
+
205
+ `Collapsible` is **not** the right replacement for a clamped preview — a "show more" block whose collapsed state still shows part of the content. It hides the content outright; clamp it with your own `max-height`/grid rule instead.
206
+
207
+ `CollapsiblePanel` and `PanelWithDetails` now build on `Collapsible`. Their props are unchanged, but both inherit the unmount-when-closed behaviour above.
208
+
209
+ **Disclosure motion slows to match.** `Collapsible`'s `collapsible-open` / `collapsible-close` go from 0.2s `ease-out` to 300ms `ease` — the motion the retired `Collapse` had, so migrated call sites keep their timing. Pre-existing `Collapsible` consumers get the slower curve too.
210
+
211
+ `Collapsible.Trigger` now carries `tailwindcss-preflight`. It renders a bare `<button>`, and because the package's reset is opt-in, a trigger without `asChild` previously picked up UA button chrome — a `2px outset` border and a `buttonface` fill — in any host app without a global reset. Triggers using `asChild` already inherited the reset from the design component they wrapped and are unaffected.
212
+
213
+ ### Minor Changes
214
+
215
+ - `ErrorState.Title` takes `as` to choose its heading tag, matching `Card.Title`. It still renders `h3`
216
+ by default; pass `as="h2"` where the error state sits directly under a page's `h1`, which otherwise
217
+ skips a level and reports as an axe `heading-order` violation.
218
+
219
+ - `RjsfSchemaEditor` renders a `template`/`watch` field as a read-only input unless the schema marks it `options.hidden`.
220
+
221
+ The pair maps to the `template-watch` widget, which resolves the template against the watched form-data paths. Until now it always rendered nothing and `FieldTemplate` hid its wrapper, so a schema could compute a value but never show it. That is right for the helper fields feeding `options.dependencies`, and wrong for the computed URLs json-editor used to display — those schemas carry a title and a description written for a reader.
222
+
223
+ `options.hidden` now decides. Set, the field stays the invisible helper it was; absent, the resolved value appears in a read-only `TextInput` under the field's own label. Watched objects and arrays serialize as JSON rather than collapsing to `[object Object]`, so a whole subtree can be shown.
224
+
225
+ Across the component schemas published on the AWS EU, GCP us-east4 and Azure north-europe stacks, 59 of the 63 `template`/`watch` fields set `options.hidden` and are unaffected.
226
+
227
+ ### Patch Changes
228
+
229
+ - `font-sans` resolves to the Inter/brand stack again in the consuming apps.
230
+
231
+ Design keeps the v3 `fontFamily.sans` scale through its `@config` bridge, but the apps have no such bridge, so after the Tailwind v4 migration `font-sans` there fell back to v4's stock `--font-sans` — no `Inter`, and no `--font-sans` brand hook from `ThemeProvider`. Every element that sets the utility explicitly (kbc-ui's top-bar nav, inputs, menu descriptions) rendered in a system face while its siblings inherited Inter from `html`. `theme.css` now re-registers the token with the v3 value, so the utility follows the brand first and the canonical Inter stack otherwise.
232
+
233
+ - Three fixes to the read-only `template`/`watch` field in `RjsfSchemaEditor`.
234
+ - A visible one now honours `readOnly` on the editor and outlines itself when its computed value fails validation — it forwarded neither before.
235
+ - One gated behind `options.dependencies` stays mounted while the dependency hides it, so its value keeps tracking the watched paths instead of freezing until the dependency flips back.
236
+ - An object- or array-typed one stays hidden. RJSF ignores `ui:widget` on containers, so the widget never runs and could not suppress its own subtree.
237
+
238
+ - Guard every custom utility `theme.css` promises against the v4 failure mode where an unregistered
239
+ token resolves in another namespace instead of erroring, and move the three an app's own build
240
+ could not reach into it. `not-disabled` is the one that changes behaviour: outside design it fell
241
+ through to v4's core `not-*` variant, which drops the `[data-disabled="true"]` half, so a control
242
+ disabled that way still took hover and focus styling.
243
+
244
+ - `MermaidDiagram` no longer renders an empty box when the fast synchronous renderer silently gives up on a layout.
245
+
246
+ `beautiful-mermaid` does not throw for every diagram it cannot lay out. An `erDiagram` whose quoted entity name contains a space — `"sample (Sample_Data_From_CSV)"`, the shape Kai emits when it labels a table with both its name and its display name — comes back as a well-formed SVG with `viewBox="0 0 0 0"` and an empty `<defs>`. That output is truthy, so it was cached and mounted as a zero-height diagram, and the async `mermaid` renderer, which parses the same source without complaint, never got a turn. No console error, because nothing failed.
247
+
248
+ `tryFastRender` now discards a fast render with no drawn content and returns `null`, taking the async path instead. The check is `hasDrawnContent` in `svgTransforms`: a zero width or height on the root `viewBox` means degenerate; a missing `viewBox` counts as drawn, since mermaid sizes some diagram types via `width`/`height` alone. Diagrams the fast renderer handles are unaffected and stay on the synchronous path.
249
+
250
+ A follow-on from the same path: a diagram streamed a chunk at a time is invalid at almost every intermediate length, and those partial frames now reach the async renderer instead of being masked by the zero-sized fast render. Mermaid rejects them correctly, which surfaced as a `console.error` and a mid-stream flip to the raw-code `<pre>` on diagrams that render fine once complete. Mermaid's jison parsers report the end of input as token `1`, so `hasDrawnContent`'s sibling `isTruncatedDiagram` treats that as "awaiting more input" — the skeleton stays and the log drops to debug — while a real token still fails through to the `<pre>`. The hold is bounded: a stream that dies mid-entity never sends another chunk, so after a short grace period the source is shown rather than a skeleton that never resolves.
251
+
252
+ - `Sidebar` gives a `sided-floating` rail the side border `inset` already had. The variant leaned on a shadow to separate itself from the page, and a shadow needs a lighter ground to fall on — on the dark scheme `--sidebar` is darker than `--background`, so the rail's inner edge dissolved into the page beside it and callers restated the border by reaching through `[data-slot=sidebar]`.
253
+
254
+ - Stop the `Table` header row from reacting to hover. `TableRow` carries `hover:bg-muted/50` and every part of the table — including the row inside `Table.Header` — is a `Table.Row`, so a `<tr>`-level `:hover` (specificity 0,2,0) outranked the `[&_tr]:bg-table-header` that `Table.Header` paints on its row: pointing at a column header lightened the whole header strip from `table-header` to muted. `Table.Header` now pins `[&_tr]:hover:bg-table-header`, so the header keeps its surface while body-row hover and `aria-selected` highlighting are untouched.
255
+
256
+ - Shipped docs and comments spell Tailwind classes the v4 way (`tw:flex`, not `tw-flex`).
257
+
258
+ A `tw-` class compiles to nothing under the v4 prefix, so every one of these examples handed a reader a class name that silently produces no styles. `brand-registry`'s `AGENTS.md` and `brand-audit`'s `README.md` go out in the tarball, and `tailwind-config` ships `index.js` / `colors.ts` with their comments intact.
259
+
260
+ `brand-audit`'s README also stops attributing its false positives to Tailwind v3 inlining resolved triplets into the bundle. That mechanism is gone — v4 emits `rgb(var(--token))` — but a scan of `apps/boilerplate/dist` reports 2 661 findings against a v4 build, up from the 1 082 the README quotes. The surviving cause is the scanner's own skip logic: `stripSkippableContent` blanks `var(...)` to spaces and leaves the colour function around it, so `rgb(var(--background))` becomes `rgb( )` and the `rgb()` pattern matches it. It fires on precisely the idiom that makes a bundle brand-safe.
261
+
262
+ `--tw-*` custom properties, the `tw-animate-css` package name, and the deliberate `never tw-flex` counter-examples in `AGENTS.md` are left alone.
263
+
264
+ - Docs stop teaching two class spellings that do not compile.
265
+
266
+ `tw:shadow-sm/md/lg` and `tw:rounded-sm/md/lg` were prose shorthand for three classes each. Under the v3 prefix that was merely terse; under v4 `/` is the opacity modifier, so the shorthand parses as one broken class. Spelled out in `tailwind-config`'s shipped `index.js` comments and in `brand-registry`'s `AGENTS.md`, which goes out in the tarball.
267
+
268
+ `@keboola/design`'s `DESIGN.md` token table referred to the prose surface as `.tw:prose`. In a selector an unescaped colon reads as `.tw` with a `:prose` pseudo-class; `src/styles.css` already writes `.tw\:prose`, so the docs now match it. The bare `tw:prose` class token elsewhere in that file is unchanged.
269
+
270
+ - Fix `ZoomableSvg`: menus opened from the fullscreen toolbar are now usable. The
271
+ fullscreen overlay sat at `z-1060`, above the `z-1055` layer shared by
272
+ `DropdownMenu` / `Popover` / `Select` / `Combobox` — and since both portal to
273
+ `document.body`, any menu opened inside fullscreen (e.g. an injected
274
+ `ExportMenu`) rendered underneath the overlay, so its items could not be
275
+ clicked. The overlay now sits at `z-1051`, the Modal/Sheet band, so those panels
276
+ layer above it exactly as they do over a modal.
277
+ - Updated dependencies:
278
+ - @keboola/codemirror-lang-sql@0.7.0
279
+ - @keboola/tailwind-config@1.1.0
280
+ - @keboola/brand-registry@1.9.1
281
+
282
+ ## 10.1.1
283
+
284
+ ### Patch Changes
285
+
286
+ - A closed `Collapse` no longer lengthens the nearest scroll container by the height of its hidden body.
287
+
288
+ The body is hidden by animating a grid row from `0fr` to `1fr` with `overflow-hidden` on the inner wrapper. That hides it visually and keeps it out of its ancestors' layout height — but the hidden content was still reported to the nearest scroll container's _scrollable overflow_, so the container could scroll past the end of everything visible. On a settings page with an internally-scrolling content region, a collapsed box with a tall body left roughly 390px of dead space under the last card, and the taller the collapsed content the bigger the void.
289
+
290
+ The wrapper now takes `contain-paint` while closed, which keeps the hidden subtree out of that overflow calculation. Scoped to the closed state deliberately: paint containment also clips, so applying it to an open box could cut off content that legitimately overflows. `overflow: clip` was the other candidate and is worse — it creates no scroll container, so the full body height leaks instead.
291
+
292
+ `Collapsible` is unaffected: Radix unmounts closed content, so there is nothing to leak.
293
+
294
+ - `Collapsible.Content` stays clipped for the whole open/close animation, so its content can no longer paint outside the box it is animating into — over whatever card or section follows it (UT-5169). Every `Alert mode="collapsible"` gets the same fix.
295
+
296
+ The clipping lives in the keyframes, and the keyframes were not ours. `tw-animate-css` ships `collapsible-down` / `collapsible-up` carrying height only; every consumer imports it unlayered (its `@utility` rules cannot be nested in a layer), unlayered beats `tailwind-config`'s `@config`-bridged same-named pair, so ours never applied. Reordering does not help either — Tailwind emits `@theme`-sourced keyframes unlayered at the tail, in an order each consuming app decides.
297
+
298
+ So the animation moves to `design/src/theme.css` under `collapsible-open` / `collapsible-close`, names no vendored library claims. That is also the better home on its own: `@keboola/tailwind-config`'s theme reaches design's own bundle, while `design/theme` reaches every consumer app's utility generation. Both keyframes now carry `overflow: hidden` on **both** endpoints — `overflow` animates discretely, so the old `visible → hidden` pair left the first half of the collapse unclipped regardless of which definition won. No fill mode, so a settled-open panel keeps its own `overflow: visible` and its own height.
299
+
300
+ Two guards, because the two halves fail differently. A unit test asserts our `@keyframes` names stay disjoint from `tw-animate-css`'s, so the next colliding name fails at author time instead of silently losing the cascade; the dead `collapsible-*` and `caret-blink` entries are gone from `tailwind-config` to make that true. A `Collapsible` story test then reads the winning rule out of a real browser and asserts every frame of it clips.
301
+
302
+ - `CollapsiblePanel` no longer points a nested panel's chevron up while its body stays collapsed.
303
+
304
+ The chevron rotated on `group-data-[state=open]`, and `group` sits on the `Collapse` wrapper that holds both the header and the body — so `group-*`, which matches any ancestor, rotated every descendant chevron as soon as an outer panel opened. Nesting is the only case that shows it. The chevron now carries its own `data-state` and rotates on that, so it can only ever report the panel it belongs to.
305
+
306
+ - Bring `Badge`'s tinted variants up to WCAG-AA on their own tint, and make the contrast audit measure every variant (UT-5117).
307
+
308
+ Three `-inverse` variants shipped below the 4.5:1 that Badge's 12px text needs. Each moves down its own ramp to the `-800`/`-900` foreground its `teal` / `cyan` / `purple` / `green` siblings already use:
309
+
310
+ | Variant | Before | After |
311
+ | ---------------- | -------------------------------------- | -------------------------------------- |
312
+ | `blue-inverse` | `blue-600` on `blue-100`, 3.90:1 | `blue-800`, 7.58:1 |
313
+ | `orange-inverse` | `warning-500` on `warning-100`, 2.11:1 | `warning-800`, 6.89:1 |
314
+ | `amber-inverse` | `warning-700` on `warning-100`, 4.45:1 | `warning-800` on `warning-200`, 6.28:1 |
315
+
316
+ `blue-inverse` and `orange-inverse` keep their dark pairings, which were already ≥9:1. `amber-inverse` had none — it painted a light `warning-100` chip on a dark page — and now inverts like its seven siblings (`warning-200` on `warning-800`, 6.28:1). Its fill sits one rung deeper than `orange-inverse` at both ends, which is what keeps caution and warning distinguishable now that both foregrounds are `warning-800`.
317
+
318
+ Two `Badge` story tests assert this, measuring rendered colour in a real browser rather than restating the class list: every tinted variant, in both schemes, via the package's existing `contrastProbe`. They check polarity as well as ratio — a tint that keeps its light fill under the dark scheme scores a fine ratio against its own text while reading as a bright slab. Note these run in the non-blocking `story-tests` job, so they report rather than block.
319
+
320
+ `audit:contrast` now covers all of it, reading `badgeColors` directly instead of scraping the source, so a new variant is measured for free. It reports both families with their light and dark pairings side by side — including the solid fills, whose sub-AA readings (`green` 2.27:1, `teal` and `orange` 2.32:1, `cyan` 2.33:1, `blue` 3.27:1) were absent from a document describing itself as a backlog of sub-AA pairings. Those stay recorded rather than fixed: `AGENTS.md` pins the decorative _fill_ across brands but says nothing about the foreground, so retuning it is a design call.
321
+
322
+ The audit had also stopped measuring anything: semantic tokens were renamed from `--color-<name>` to bare `--<name>` and the extractor's regex never followed, so all 16 pairings rendered as "token not set — inherits" while the script still exited 0. It now resolves both spellings, follows one level of `var()` aliasing so `sidebar-primary` is measured, and throws on an empty block. `check:contrast` joins `ci-checks` alongside `check:api` and `check:icons`, so the generated file can no longer drift unnoticed.
323
+
324
+ - Guard against Tailwind v4's `currentColor` border default. `DiffView`'s sticky header goes back to `tw:shadow-sm` — `tailwind-config` pins `boxShadow.sm` to the v3 value, so v4's rename never applied here, and `shadow-xs` had silently opted the header out of a brand's `theme.shadows`. Every story now doubles as a border-colour check, so a width utility shipped without a colour fails the story-test lane.
325
+
326
+ - `--shadow-card` and `--shadow-table` are now real runtime tokens, and follow the colour scheme.
327
+
328
+ Both existed only as `@theme` tokens, and v4 inlines an `@theme` value into the utility it generates
329
+ without emitting a custom property for it — so `var(--shadow-table)` resolved to nothing. Declaring
330
+ them in `:root` gives a rule something to read, and the dark block overrides both.
331
+
332
+ Dark swaps the light-tuned pale blue-grey (`rgba(100, 135, 153, …)`) for a black shadow, the same
333
+ correction `--shadow-raised` already had: on a dark surface the pale value lights up instead of
334
+ reading as depth, and under a sticky table header it looked like a glowing seam. `--shadow-table` is
335
+ the softer of the two because a table header casts a full-width band.
336
+
337
+ Light values are unchanged, and the `shadow-card` / `shadow-table` utilities are untouched — they
338
+ keep the literal v4 inlined at build time. Making those utilities scheme-aware needs the token to
339
+ carry a `var(--runtime, fallback)` shape and is left as a separate change.
340
+
341
+ - Ship one build chunk per module the root barrel re-exports, so importing from
342
+ it costs only what you import. `dist/index.js` held roughly half the library in
343
+ a single module — `Card`, `Table`, `Modal`, `Stepper` and all of AIKit — so
344
+ importing `Card` also pulled `sonner`, `cmdk`, `input-otp` and `react-markdown`,
345
+ and made the consumer's bundler emit the whole mermaid graph behind AIKit's lazy
346
+ boundary. An app importing `Card` alone drops from 316 kB to 93 kB gzipped and
347
+ from 83 emitted chunks to 1. No API change: every export keeps its name and
348
+ subpath.
349
+
350
+ - `Search` follows the active colour scheme instead of painting itself white.
351
+
352
+ The `default` variant fills with `bg-card`, the wrapper takes the paired `text-card-foreground`,
353
+ the placeholder reads `text-muted-foreground`, and the readonly fill is `bg-secondary`. The `ai`
354
+ variant's inset flood — the mask that leaves its gradient visible only in the border box — follows
355
+ `--card` too, so the gradient border survives on a dark surface.
356
+
357
+ The `default` fill is unchanged in light mode — `--card` is `255 255 255`. The text colour
358
+ (`#222529` → `#1e293b`) and placeholder (`#7c8594` → `#64748b`) shift by a few units onto their
359
+ semantic tokens. The readonly fill moves `#d9dde5` → `#e2e8f0`: `bg-secondary` rather than
360
+ `bg-muted`, because `--muted` is within a few units of `--card` in both schemes and would have left
361
+ a readonly field looking editable.
362
+
363
+ - `MermaidDiagram` now follows the active colour scheme and brand. Both renderers
364
+ (the synchronous `beautiful-mermaid` fast path and the async `mermaid` fallback)
365
+ take their palette from the semantic `--card` / `--foreground` / `--muted` /
366
+ `--border` / `--accent` / status tokens instead of hardcoded light values, and
367
+ the SVG cache is keyed on the theme so a scheme or brand switch re-renders the
368
+ diagram instead of serving the previously rendered one. Applies to ```mermaid
369
+ fences rendered through `Markdown` too.
370
+
371
+ - `PageLayout.Content nav` lets the page scroll instead of containing the body, and sticky page chrome paints past the content column.
372
+
373
+ The body used to be its own scroll region (`max-h` + `overflow-y-auto`). A scrollbar can only reach the window's far edge if the element that scrolls reaches it, so that drew a page-sized scrollbar at the capped content column's right edge — a few hundred pixels in from the window, beside the host shell's own. The body now carries no height cap, no overflow and no mask; the nav column stays a bounded sticky region, so a long nav still scrolls inside itself.
374
+
375
+ Two consequences for consumers of the `nav` layout:
376
+ - **A page pinning its own sticky element now pins against the page header**, not against a region edge. The offset is published on the body slot as `--page-content-sticky-top` (the header's height today); kbc-ui reads it for `.thead.is-sticky`. Published rather than derived by the consumer so that reverting this layout makes the variable disappear and the consumer falls back to `0` — the body-scroll offset — instead of mispinning.
377
+ - **Nothing clips or masks the body**, so a flush control keeps its focus ring without slack, and hand-rolled `position: fixed` markup inside a page is no longer trapped by a mask.
378
+
379
+ `--section-nav-body-available` and `--section-nav-bottom-inset` are gone; `--section-nav-available` remains and is now corrected by the host's scroll position, so a resize performed halfway down the page can no longer hand the nav a height taller than the window.
380
+
381
+ Separately, `Header sticky` and `Footer sticky` now paint an 8px background band past the column. Content stops at the column but its decoration does not — a card's `0 2px 6px` shadow blurs past its own box — so chrome that stopped at the column left a sliver of shadow smearing down its side as the page scrolled under it. The band is the page colour and costs no layout, so neither element's measured box changes.
382
+
383
+ - `SectionNav` no longer paints an icon on a sub-section row, in either the tree or the collapsed dropdown, since the group's icon already stands for its children. Sub-section rows pass `showIcon={false}` at render time rather than losing their `icon`, so an item keeps its glyph for surfaces that flatten the tree into peer destinations such as a tab row.
384
+
385
+ - Fix two chrome regressions on the dark colour scheme.
386
+
387
+ `Sidebar` no longer teleports when an `off-canvas` rail opens or closes. Tailwind v4 compiles `translate-x-full` to the standalone `translate` property rather than to `transform`, but the wrapper's transition list still named `transition-[width,transform]` — a v3-era holdover — so the slide had no transitionable property and snapped. The list now names `translate` too. The gap element's `transition-width` (never a real utility, so it silently fell through to the CSS-initial `transition-property: all`) is now `transition-[width]`.
388
+
389
+ `Navbar` pairs `bg-sidebar` with `text-sidebar-foreground`. It painted the sidebar surface without setting a foreground, so anything inheriting `currentColor` — a `BrandLogo` whose SVG fills with `currentColor`, plain text — fell back to the UA's black and disappeared on the dark scheme. Existing call sites that pin their own colours (kbc-ui's `AppSiteHeader`) are unaffected.
390
+
391
+ - Internal refactor of the DOM-read theme signal that `MermaidDiagram` and the
392
+ CodeMirror editors build their palettes from. It is now
393
+ `ThemeProvider/useThemeAttributes`, which returns the active scheme and brand
394
+ alongside the cache key, so a palette can no longer be resolved under one scheme
395
+ and cached under another's key. `isDarkScheme` is gone — `useThemedExtension`
396
+ passes the scheme to its builder instead. No behaviour change for consumers.
397
+
398
+ - Make the `ToggleGroup` pressed segment readable in the dark scheme
399
+ - Off-state labels are `muted-foreground` and the pressed one `foreground`, so the state reads from the label — the fill is a near-invisible step on either scheme's track.
400
+ - Under `[data-theme='dark']` the pressed fill is `secondary` rather than `background`: `background` is darker than the `muted` track there, so the light-mode lift inverted into a hole.
401
+ - The pressed drop shadow is dropped in dark, where a black shadow on a dark fill renders nothing.
402
+
403
+ - Updated dependencies:
404
+ - @keboola/tailwind-config@1.0.3
405
+ - @keboola/brand-registry@1.9.0
406
+
407
+ ## 10.1.0
408
+
409
+ ### Minor Changes
410
+
411
+ - Five AI-client brand marks join `@keboola/design/icons`: `Claude`, `ChatGpt`, `Cursor`, `Make` and `Devin`.
412
+
413
+ The connect surfaces rendered these as ~16px PNGs, so they were soft on retina and could not take a colour treatment. Each is now an SVG component on Lucide's 24-unit grid, typed `LucideIcon`, sourced from the vendor's own brand or press page — see `src/icons/vendors/README.md` for the source and usage restriction per mark.
414
+
415
+ `ChatGpt` and `Devin` are monochrome and inherit `currentColor`; `Claude`, `Cursor` and `Make` are fixed vendor colours that keep their own fills and ignore `color`.
416
+
417
+ Every vendor mark now takes **`mono`**, which renders it in `currentColor` instead of its own brand colours — `<Claude mono />`. Defaults are unchanged, so this is additive. The `Eu` / `Us` region flags are deliberately excluded: a flag's meaning is its colours. The Icon Gallery drives `mono` and icon `size` from Storybook controls.
418
+
419
+ - `AlertModal` accepts `isLoading` and `AlertModal.Content` accepts `confirmDisabled`, so a host can drive the confirm action's state without routing it through `onConfirm`'s return value.
420
+
421
+ `AlertModal` only knew loading it inferred itself: return a thenable from `onConfirm` and it holds the dialog open, locks Escape, disables cancel and spins the action until the promise settles. That covers a host which owns the request inside the handler — and nothing else. A host tracking the request elsewhere (a store flag, a mutation hook's `isPending`) had no way in, and a confirm that should stay blocked until a precondition is met had none either.
422
+
423
+ `isLoading` sits on the root next to the promise-driven state and merges with it there, so the context hands every consumer one answer for "the confirm action is running" no matter which way the host drives it. `confirmDisabled` sits on `Content`, where the button it gates is rendered, and is narrower on purpose: it blocks confirm only, leaving cancel available, because an unmet precondition is not a reason to trap the user in the dialog.
424
+
425
+ Both default to `false` and neither reaches the DOM, so nothing changes for existing callers.
426
+
427
+ - `AppShell.Header` and `AppShell.Aside` accept `flush`, which drops the slot's own padding, background and border so a hosted chrome component renders edge to edge.
428
+
429
+ The slots paint themselves, which is right when content goes straight into them and wrong when the child is already a full-bleed surface — a `Navbar` in the header, or a `Sidebar side="right"` in the aside. The child then sits inset inside a second painted surface, with the slot's background showing through as gutters at each end.
430
+
431
+ Every consumer was re-stating the same overrides at the call site: four scaffold templates carried `tw:items-stretch tw:gap-0 tw:bg-transparent tw:px-0`, and kbc-ui carries that plus `tw:w-auto tw:border-l-0 tw:bg-transparent tw:p-0 tw:overflow-visible` on its Kai rail. Hand-written overrides also have to win a cascade fight, which is how kbc-ui's nested `Sidebar` ended up needing a load-bearing `tw:!top-12`.
432
+
433
+ Defaults are unchanged, so this is additive — existing overrides keep working; `flush` just makes them unnecessary.
434
+
435
+ - Add `headerSpan` to `AppShell`, so a full-bleed header row above both rails is a supported geometry rather than a call-site override. The sidebar shell defaults to `content` (header starts after the left rail — unchanged), while `headerSpan="full"` switches the row template to `"header header header" / "sidebar main aside"`, aligning the brand mark to the viewport's left edge. Reach for it when the header carries app-wide identity — brand mark, project switcher, account menu — rather than page-level navigation. It is a no-op under the top-nav geometry, whose header already spans the width, and the prop is dropped there rather than forwarded to the DOM.
436
+
437
+ Neither shipped geometry covered this shape before: `TopNavShell` spans the header but hides the sidebar slot outright, so apps wanting a full-width header _and_ a left rail had to override `gridTemplateAreas` with an inline style — which then had to win a cascade fight against `Sidebar`'s hardcoded `inset-y-0`.
438
+
439
+ That second half is handled too. `Sidebar`'s inner wrapper is `fixed inset-y-0` (it owns that for its collapse/resize animation), so under a full-width header it anchors to the viewport and paints over the bar — the reason kbc-ui also carries `!top-12` on both rails. Under `headerSpan="full"` the shell promotes the rail slots to containing blocks, re-anchoring those fixed children to the slot's own grid box. No `!important` to out-specify `inset-y-0`, and the shell never needs to know the header's height.
440
+
441
+ - charts: `LineChart` takes a `yDomain` prop to move the y-axis off zero
442
+
443
+ Two series of similar magnitude far from zero (credits used vs budgeted, rows
444
+ processed vs quota) used to occupy the top fifth of the plot, hiding the
445
+ difference between them. `yDomain={[min, max]}` sets the value-axis bounds —
446
+ explicit numbers, `'auto'`, or padded data extremes
447
+ (`['dataMin - 500', 'dataMax + 500']`) — and applies whether or not the axis is
448
+ shown. The default is unchanged: charts still start at zero.
449
+
450
+ It is exposed on `LineChart` only. A line encodes value by position, so the
451
+ baseline is a viewport; a bar's length and an area's fill are measured from the
452
+ baseline, and truncating those misstates the ratios.
453
+
454
+ - `DateRangePicker` grows the props its first real consumer needed.
455
+
456
+ `withTime` adds a From/To row under the calendar, built on the existing `TimePicker`, so the
457
+ emitted `from` / `to` carry hours and minutes. `requireApply` holds day and time edits in a draft
458
+ and emits once, on Apply — for a filter that refetches on change, an `onChange` per calendar click
459
+ is not affordable. `disabledDays` forwards a react-day-picker matcher to `Calendar`, so
460
+ `{ after: new Date() }` rules out future days (named to match `DateInput`).
461
+
462
+ A `DateRangePreset` may now carry an opaque `value`, returned through the new `onPresetSelect`
463
+ instead of `onChange`, and `label` replaces the formatted range on the trigger. Together they cover
464
+ consumers whose stored form of a range is not two dates — a relative token such as `-7 days` that a
465
+ server resolves per request, which would otherwise freeze at pick time. `range` on a preset is now
466
+ optional, so a preset can be value-only ("All time"). `formatDate` lets an app render each end of
467
+ the range in its own date style while the component keeps the from → to join; the built-in format
468
+ is ISO, which suits no app in particular.
469
+
470
+ `open` / `onOpenChange` / `defaultOpen` and `align` / `alignOffset` / `sideOffset` now pass through
471
+ to the popover, matching `DatePicker` and `TimePicker`.
472
+
473
+ The trigger renders with the same chrome as `SelectTrigger` rather than `Button variant="secondary"`,
474
+ so a `DateRangePicker` lines up with `Select` and `Combobox` in a filter row. `SelectTrigger`'s own
475
+ rendering is unchanged; the variants both share moved to `OverlayTrigger/styles.ts` as
476
+ `overlayTriggerVariants`.
477
+
478
+ - `FileUpload` follow-ups from the UT-5008 review:
479
+ - **`FileDropTargetOptions`** and **`FileDropTarget`** are exported, and `FileUpload`'s props derive from them instead of re-declaring the drop-target options.
480
+ - **`FileUploadRejectionCode`** exports the closed set of rejection reasons — type a rejection-copy map `Record<FileUploadRejectionCode, string>` and it is checkably exhaustive.
481
+ - **`FileUpload.DropZone` root now has `role="button"`** instead of react-dropzone's `role="presentation"`, which is ignored on focusable elements — so `aria-label` / `aria-labelledby` on the zone actually reaches assistive tech now. Pass your own `role` to override.
482
+ - Correction to the previous release's notes: `FileUploadRejection.file` is deliberately a plain `File` — a handler reading react-dropzone's `file.path` does not type-check against it; key on `file.name` instead.
483
+
484
+ - `SectionNav` — in-page grouped navigation — plus a nav-aware `PageLayout.Content nav` slot and `PageLayout.Header sticky`.
485
+
486
+ A page with more sections than a tab row can hold has had no shared answer, so kbc-ui grew one locally for its Settings area: a vertical tree, a dropdown when the column narrows, a group row that both navigates and toggles, and four pieces of CSS that break silently if any of them is re-typed slightly differently. Every app we ship wants that shape — control-ui, boilerplate, the account screens, external data apps — so it moves here.
487
+
488
+ `SectionNav` renders the tree and the collapsed dropdown from one row renderer, so the two forms cannot drift. It is **router-agnostic**: `renderItem` returns the clickable element, which is how `NavLink` / TanStack `Link` stay out of the design system (the same seam `packages/auth`'s `SignIn` uses for its Google button). `id` is identity and navigation target in one, so there is nothing to keep in sync; a group with children navigates to its first child. Visibility, permission filtering and the group model stay in the app — those are the composition, documented as `RECIPES.md#section-nav` with a `patterns/` story and two router wirings.
489
+
490
+ `PageLayout.Content nav` owns the geometry, because that is the part that fails quietly:
491
+ - `@container/section-nav` goes on its own element — a container query matches descendants only, so the element naming the container can never read it, and it cannot be the nav's own wrapper either, whose width is what the switch changes.
492
+ - The query reads the content area's width, never the viewport: a product sidebar or a chat panel narrows the page without the window resizing.
493
+ - `self-start` on the nav column — a stretched flex item is as tall as its row, leaving `sticky` nothing to move within.
494
+ - The sticky offset plus `max-h-[calc(100dvh - header - gutter)]` and `overflow-y-auto`.
495
+
496
+ `PageLayout.Header sticky` pins and paints the header, mirroring the prop `PageLayout.Footer` already had, and publishes its own measured height as `--page-header-height` through a `ResizeObserver` — so the nav pins directly beneath it and no consuming app has to wire a measurement hook. `Patterns/SectionNav → StickyHeaderAndNav` is that behaviour's only honest test: real Chromium, real scroll, asserting the variable reaches the nav column and that both boxes hold their pinned positions.
497
+
498
+ One consequence of `renderItem` is worth stating outright: the rows are elements the **app** created, so they inherit no CSS scope the block does not open. The design system's reset is confined to `.tailwindcss-preflight`, so the tree's `<nav>` carries that class itself — otherwise the rows are `content-box`, `w-full` + `px-3` measures 24px wider than the 256px column, and every row bleeds past it behind a horizontal scrollbar. Sub-sections also get a hair of left padding so an active row's `bg-accent` clears the group's rule instead of sitting on it, and descriptions break long tokens rather than painting out of the column.
499
+
500
+ Additive throughout: `PageLayout.Content` without `nav` renders the same element with the same classes as before, and `Header` without `sticky` is unchanged.
501
+
502
+ `SectionNav` is deliberately **not** `Sidebar`. `Sidebar` is the app's left rail — global provider state, icon-collapse, one per shell, outside `PageLayout`. `SectionNav` is scoped to one page's sections and holds nothing but an open flag per group. Its `AGENTS.md` states this outright so the two don't get merged.
503
+
504
+ - SQL syntax highlighting reads correctly again (UT-5024, customer feedback after the CodeMirror 6 unification).
505
+
506
+ `@keboola/codemirror-lang-sql`:
507
+ - `replace` paints as a keyword in both dialects, so `create or replace table` reads as one DDL phrase — without giving up the function: it stays in the function lists (`KEYWORD_PAINTED_FUNCTIONS` keeps it out of the highlight input only), and the keyword completion carries the `REPLACE()` signature and docs.
508
+ - The token→tag map is re-declared inside the package (`language.configure({ props: [styleTags({…})] })`). Highlight themes match lezer tags by numeric id; a bundler that pre-bundles `@codemirror/lang-sql` with a second copy of `@lezer/highlight` (vite dev deps do) gives _modified_ tags (`standard(name)`, `special(string)`) ids the app's theme can't see, so `Builtin`/`QuotedIdentifier` silently fell back to their base tags. Re-tagging pins every rule to the copy the app modules share.
509
+ - **Breaking:** `getSqlDialect(name)` is replaced by `getSqlLanguageSupport(name)` — the re-tagged language plus keyword completion. Building on the raw `SQLDialect` bypassed the re-tagging.
510
+ - `installExtensions` no longer installs `classHighlighter`; its only purpose was serving design's `.cm-line .tok-string` CSS override, which is gone.
511
+
512
+ `@keboola/design` — `CodeEditor`/`SqlEditor` get a corrected syntax palette on top of github-light, applied to every language:
513
+ - identifiers and quoted SQL identifiers (`"a column"`) render as plain foreground (github.com's own choice) instead of sharing one green with functions and variables;
514
+ - string literals are amber everywhere (previously amber in `SqlEditor` only, github navy elsewhere — JSON editors change visibly);
515
+ - functions get purple, special variables cyan, and brackets stop borrowing the comment gray.
516
+
517
+ - Add `Workbench` — a bottom rail with a panel that expands over the page, for tooling that outlives one page (VS Code's bottom dock). Compose it from `Workbench.Bar` (`BarStart` / `BarMiddle` / `BarEnd`) and `Workbench.Panel` (`PanelHeader` / `PanelTitle` / `PanelActions` / `PanelContent`), with `Workbench.Trigger` and `Workbench.FullScreenTrigger` for the three states — collapsed, expanded, full screen. `useWorkbench()` reads that state from any depth, and throws outside a `<Workbench>`.
518
+
519
+ The panel's top edge is a drag handle: `panelHeight` / `defaultPanelHeight` / `onPanelHeightChange` carry the height in pixels, keyboard steps come from ↑ / ↓ on the focused handle, and dragging into the ceiling enters full screen (dragging back out leaves it). The panel can never cover the bar, whatever height it is given.
520
+
521
+ ### Patch Changes
522
+
523
+ - `AlertModal`'s cancel is a neutral `outline` button instead of a filled `secondary` one.
524
+
525
+ `Button variant="secondary"` paints `secondary-500` — a saturated blue — in both design languages. Next to a `danger` confirm that put two fully-coloured buttons in the footer, and the blue one read as the primary action even though it's the one that does nothing.
526
+
527
+ `Popconfirm.Cancel`, the design system's other confirmation primitive, has always used `outline` for exactly this reason. This aligns the two, so the confirm is the only coloured action in either component's footer.
528
+
529
+ Visible wherever `AlertModal` is already used — control-ui, `@keboola/auth`, theme-generator — not only under the legacy design language.
530
+
531
+ - Fix the aside slot painting under sticky page content under `AppShell headerSpan="full"`. The slot's containing-block promotion traps the hosted `Sidebar`'s `z-50` inside a stacking context that competed at `z-index: auto`, so positioned content in `main` — sticky table headers at `z-10` — painted over a filled right rail (the Kai assistant panel). The aside slot now carries `z-30`: above content-level stickies (including `PageLayout`'s sticky header at `z-20`), below the sidebar slot's `z-50` so an expand-on-hover left rail still paints above a filled aside.
532
+
533
+ - Fix the left rail painting under page content under `AppShell headerSpan="full"`. Promoting the rail slots to containing blocks also makes them stacking contexts, which traps `Sidebar`'s own `z-50` inside the slot — and because the sidebar slot precedes `main` in source order, an expand-on-hover rail expanded over the page ended up _behind_ main's positioned content. The sidebar slot now carries the z-index itself. The aside slot now carries `z-30` (see the aside-slot stacking changeset): source order only beats z-index-free content, so leaving it at `auto` let main's positioned stickies paint over it.
534
+
535
+ - Stop a parked off-canvas `Sidebar` from making the page horizontally scrollable. The rail hides itself with `translate: ±100%`, which parks the panel outside the shell visually but leaves its box in the scrollable overflow. That was harmless while the panel anchored to the viewport — viewport-anchored `fixed` boxes contribute nothing to document overflow — but `headerSpan="full"` promotes the rail slots to containing blocks for their `fixed` children, which moves the parked panel into the document's overflow chain: the page became scrollable right by the rail's own width, revealing the content-less panel as an empty slab. Under that template, a rail's own slot now clips its inline axis while the rail is collapsed. Scoped that tightly on purpose: on the slot rather than the shell, so a page-sized box is not left reporting cut-off content (axe flags that); gated on the collapsed state, so a filled rail — which widens past the slot's width — is never cropped; and gated on `headerSpan="full"`, the only template where the promotion, and therefore the overflow, exists. The rule sits in `@layer utilities` because the slot's own `overflow-visible` utility would otherwise outrank it. Note for a future revert: `apps/boilerplate` dropped its own `overflow-x-clip` class in reliance on this rule, so reverting the rule alone reintroduces the scroll there — revert `AppChrome.tsx` with it.
536
+
537
+ - `CodeEditor`'s copy toolbar floats inside the editor instead of above it, and `Button size="icon-xs"` now has a size under argo.
538
+
539
+ The toolbar used to sit out of flow above the editor's top edge, which meant the component reserved 32px of its own (`tw:mt-8`) so it would not collide with whatever was above. On a labelled field that reads as a gap between the label and the code, and the chip itself — a 32px button in a 40px chip — was the largest thing on the row. It is now a 24px button in a 28px chip pinned to the editor's top-right corner, so a copy button costs no vertical space at all.
540
+
541
+ `icon-xs` was declared but empty in `argoVariants`, so under argo the button fell back to its content box (18px, icon touching the border). It is `tw:size-6` now, matching the legacy scale — visible anywhere `size="icon-xs"` already renders under argo, such as the AI-kit markdown code-block copy button.
542
+
543
+ - `CopyField` renders correctly under the dark colour scheme.
544
+
545
+ Every part of the block was painted with hex literals (`bg-[#fafafa]`, `border-[#e4e4e4]`, `text-[#3f3f3f]`, `text-[#8b8b8b]`, `text-[#6b6b6b]`) — the last block in `src/blocks/` still doing so. Dark mode swaps `--*` CSS variables under `[data-theme='dark']`, and a literal cannot participate, so the box stayed a near-white slab with dark grey text on the dark page: unreadable value text, invisible prefix/suffix dividers, and copy/reveal icons that darkened on hover instead of lightening.
546
+
547
+ The parts now read semantic tokens: `bg-muted` for the box with `hover:bg-accent`, `border-border` for the box and addon dividers, `text-foreground` for the value, and `text-muted-foreground` → `hover:text-foreground` for the addons and the copy/reveal icons.
548
+
549
+ Light mode is visually near-identical (`#fafafa` → `#f1f5f9` fill). Three side effects worth knowing:
550
+ - The hover fill is the shared `accent` tint rather than a neutral grey step, matching every other interactive surface in the package.
551
+ - Because the box reads tokens, a customer brand finally reskins it — under `example-customer` the fill resolves to `#f4f2f8`, where the literal previously pinned it to `#fafafa` regardless of brand.
552
+ - `CopyField.Prefix` / `CopyField.Suffix` text moves from 5.11:1 to 4.34:1 against the box fill in the light scheme. That is `muted-foreground on muted`, the package's standard secondary-text-on-muted-panel pairing and an already-documented sub-AA entry in `CONTRAST-AUDIT.md` — retuning it is a token-level fix for every muted panel at once, not a reason to give this one block a bespoke colour.
553
+
554
+ Covered by a pair of browser-mode regression stories (`Regression — follows the {dark,light} scheme`) that assert the _rendered_ colours: the box fill has to land on the same side of the lightness midpoint as the page surface behind it, and the value text has to clear AA against that fill. Verified to fail on the pre-fix hex under the dark scheme while still passing under light.
555
+
556
+ - CopyField reads as a hairline box again: `bg-card` + `hover:bg-muted` instead of `bg-muted` + `hover:bg-accent`. A white fill and its shadow lift the box off the page; `accent` painted a large blue area on hover, which on a box holding a token or a command reads as a text selection.
557
+
558
+ - `Button variant="danger"` tints its fill on hover, so a destructive action is no longer inert under the cursor.
559
+
560
+ Under argo the variant declared its background, both hover/focus text colours and its focus ring, but no hover background at all. The computed fill on hover came back byte-identical to resting — the button gave no feedback in either colour scheme, and `danger` was the only argo variant with that gap (`primary`, `secondary`, `outline` and `invisible` all tint; `inline` underlines).
561
+
562
+ The hover fill is `bg-destructive/90`, the same 10% tint `primary` uses, so it follows the brand's `destructive` token and works in both schemes: it lightens against a light page and darkens against a dark one. Every surface that routes a destructive action through the variant picks this up — `AlertModal`'s confirm button, `Popconfirm`, `SplitButton`, `MultiActionButton`.
563
+
564
+ - Make `DiffView` and `MergeEditor` legible under the dark colour scheme.
565
+
566
+ Both painted their added/removed/conflict rows with opaque light pastels
567
+ (`green-100`, `error-100`, `warning-100`, …) and let the text colour inherit, so
568
+ under `[data-theme='dark']` the near-white `foreground` landed on a light slab.
569
+ Each fill is now a light/dark pair — the pastel wash in light, a low-alpha tint
570
+ of the same hue over the pane surface in dark — and every changed row, word-level
571
+ `mark`, gutter line number, and the `MergeEditor` deleted-pane wash pins its own
572
+ text colour.
573
+
574
+ Also raises three light-scheme line-number contrasts that were below AA:
575
+ `DiffView`'s added gutter (2.30:1 → 6.13:1) and removed gutter (3.30:1 →
576
+ 5.38:1), and `MergeEditor`'s highlighted gutters (3.96:1 → ≥6:1).
577
+
578
+ - `DropdownMenu.Item` sizes an icon passed as `children` at 16px, so a menu row's glyphs no longer disagree with each other.
579
+
580
+ The item already lays itself out for the icon+label shape (`flex items-center gap-2`), but sized no svg descendant — the guard lived only in `ItemIcon`, which is reached exclusively through the `icon` prop. An icon handed to the item as a child therefore fell back to Lucide's 24px default and stood 1.5× taller than its explicitly-sized siblings, pushing its label out of the row's shared gutter. `DropdownMenu.Item` was the last item-like primitive in the package without the default: `SelectItem`, `ComboboxItem`, `CommandItem`, `ItemIndicator`, `SidebarMenuIcon` and `Button` all carry it, as do the sibling overlay containers `PopoverContent`, `HoverCardContent` and `ModalBody`.
581
+
582
+ The default is `:not([class*="tw:size-"])`-guarded like everywhere else in the package, so it only reaches icons with no size of their own and an explicit `tw:size-*` at the call site still wins. That bounds it to the icons already rendering oversized — it cannot shrink anything deliberately sized. Surfaced in kbc-ui's Workspaces row-actions menu, where "Enable/Disable sharing" and "Delete workspace" rendered at 24px (UT-5028); the ~28 places across apps that render `DropdownMenu.Item` directly are covered by the same change.
583
+
584
+ - Fix `FullScreenModal` on the dark colour scheme. The shell filled a fixed `neutral-50` (only the argo branch reached `background`), the default header title pinned `neutral-900`, and the shell set no foreground at all — so on dark the modal stayed light under legacy chrome, its title went invisible under argo, and content that inherits its text colour sat dark-on-dark (the modal portals to `document.body`, so it inherits no app-level foreground). The shell now pairs `bg-background` with `text-foreground` for both chromes and the title reads `text-foreground`. Light-mode values are unchanged apart from the legacy shell moving `#f2f4f7` → `#f9fafb`.
585
+
586
+ - Fix `Markdown` rendering dark-on-dark under the dark colour scheme. Tailwind Typography pins its own palette (`--tw-prose-bold`, `--tw-prose-headings`, …) to light-mode values and has no awareness of `[data-theme]`, so a bold run or heading inside a rendered answer stayed near-black (`gray-900`) while the surrounding body text followed the semantic tokens to near-white. `[data-theme='dark'] .tw:prose` now applies `prose-invert`.
587
+
588
+ Declared in the `utilities` layer, not `components`: `.tw:prose` is itself a utility, and a Tailwind v4 cascade layer beats specificity outright — the same rule in `components` still loses to the plain utility. Same layer plus higher specificity is what actually wins.
589
+
590
+ This affects any surface rendering Markdown on a dark background, including kbc-ui's Kai transcript.
591
+
592
+ - Lay `Navbar.Footer` out as a centred row. It was a bare block, so its children were inline-level boxes aligned on a text baseline — a button carrying an icon and a text-only button sat at different heights, visibly off-centre against the bar (measured 2px below vs 2px above centre, a 4px mismatch between neighbours). The slot is documented to hold several items (search, notifications, avatar), so it now centres them and gaps them.
593
+
594
+ No effect on the common composition of a single `Navbar.Menu` child: centring a lone child is what the baseline was approximating, and `gap` only applies between siblings.
595
+
596
+ - Let the Storybook dev server take a port from `PORT`, so the Claude Code preview pane's autoPort can place it on a free port. Defaults to 6006 as before; no runtime or published-output change.
597
+
598
+ - `PageLayout.Content nav={…}` no longer crops the focus ring of a control sitting at the top of the page body.
599
+
600
+ The `nav` layout makes the body its own scroll region, and `overflow-y: auto` clips at the padding box — which had no padding. Every form control in the system paints its focus affordance _outside_ its border box (`ring-4`, plus a 1px outline on `Search`), and the first child of a page is flush with that region. Focusing it therefore lost the top, left and right of the ring, leaving only the stubs where the rounded corners curve back inside — most visible on Project Settings, where `Search` is `autoFocus` and the ring is cropped before anyone touches the page.
601
+
602
+ The body now carries 6px of clip slack (`p-1.5`), given straight back to the layout as a negative margin, so the content box lands exactly where it did before — same position, same width, at every column width. Below the two-column breakpoint the body does not scroll, so neither applies.
603
+
604
+ - `PageLayout.Content nav`'s columns now run to the bottom of the scroll viewport, and their edges fade while there is content past them.
605
+
606
+ A shell that pads the bottom of its scroll container means end-of-scroll slack for a page that scrolls as one — kbc-ui carries 8em of it, ported from legacy `.main-container`. A region that scrolls internally never reaches that padding, so subtracting it from the columns' height left them ending up to 128px above the bottom of the window: a band of dead space under a body that was already clipping content. The padding is now measured as `--section-nav-bottom-inset` and cancelled with a matching negative bottom margin, so the columns fill the viewport without the padding adding scrollable height below them. Cancelled only when `Content` is the last child — a following `PageLayout.Footer` keeps the dead space rather than being pulled up over the columns.
607
+
608
+ Both columns also carry `scroll-fade-y`, the edge treatment `Modal.Body` uses, so a card cut off at the window's bottom edge reads as "scroll for more" rather than a broken layout. It sits behind `@supports (animation-timeline: scroll())`, because the utility's fallback is a static fade at both edges — across a whole page body that would wash out the first and last card on every non-Chromium browser. Note that a masked element is the containing block for `position: fixed` descendants and clips them: hand-rolled `fixed` markup placed directly in the body has to portal out (every design-system overlay already does).
609
+
610
+ The body's focus-ring clip slack goes from 6px to 8px, which clears the widest affordance in the system (`ring-4` + `ring-offset-2` + a 1px outline) with room for fractional rounding under browser zoom. Content lands where it did before — the slack is given back with a negative margin.
611
+
612
+ - `PromptInput` lets modifier-held keystrokes reach document-level hotkeys while the textarea is focused.
613
+
614
+ With `commands` set, the textarea's keydown handler called `stopPropagation()` for every key the always-mounted cmdk `Command` root hijacks (`Enter`, `ArrowUp`, `ArrowDown`, `Home`, `End`). That guard is what keeps cmdk from `preventDefault`-ing plain `Enter` and breaking multiline editing, but it did not distinguish modified keystrokes — and React dispatches from the container root, above which `document` listeners sit. So `Cmd+Enter` was swallowed too: an app hotkey registered on `document` (via `react-hotkeys-hook`, even with `enableOnFormTags: true`) never fired while the prompt input had focus, which is its default state. In kbc-ui this is the Kai "approve tool call" shortcut, which only worked after clicking away from the composer.
615
+
616
+ The guard now runs only for keystrokes cmdk actually acts on. Reading cmdk's root handler, `Enter` is the sole hijacked key whose branch reads no modifier at all — it `preventDefault`s unconditionally. Every other one does branch on modifiers: the arrows on `metaKey` (jump to first/last) and `altKey` (jump to the adjacent group), and `Home`/`End` `preventDefault` unconditionally. So a modified `Enter` is the only keystroke the Command root would ignore, and it is the only one now allowed to bubble; modified arrows and `Home`/`End` keep the shield, or cmdk would `preventDefault` them and break caret movement in the textarea.
617
+
618
+ Behaviour that is deliberately unchanged: plain `Enter` still submits, `Shift+Enter` still inserts a newline, and the slash-command menu still opens and navigates — `Shift` is not a cmdk modifier, so `Shift+Enter` stays guarded. `Alt+Enter` now bubbles where it previously did not; that is the point, since `mod+alt+enter` is a live hotkey elsewhere in kbc-ui.
619
+
620
+ Covered by regression tests asserting that `Cmd`/`Ctrl`/`Alt`+`Enter` reach a `document` listener, while plain `Enter`, `Shift+Enter`, `Home`/`End` and every modified arrow do not.
621
+
622
+ - `RjsfSchemaEditor` handles its own reset, so hosts no longer need a changing React `key` to remount it.
623
+
624
+ Two behaviours that previously only a full remount provided:
625
+
626
+ A pending debounced write is now cancelled when an external `value` arrives. Reverting the form within the 100 ms debounce window used to leave the host store holding the value the user had just discarded, while the form showed the restored one.
627
+
628
+ Errors raised through `onRegisterValidate` now clear as the fields are fixed, instead of staying on screen until something remounted the form. Validation is still quiet until the host asks for it — a configuration saved before its schema gained a constraint should not open covered in red — and from the first ask on the form revalidates on blur.
629
+
630
+ - `PageLayout.Content nav` behaves the same at both widths, and no longer eats a sticky table header.
631
+
632
+ Three things diverged as the nav collapsed. The dropdown trigger stayed pinned, so it floated over the content it was meant to label — it is only pinned now where it sits beside a scrolling body. The body scrolled itself only at the wide width, so the edge fades were absent when collapsed and a page's tables changed scroll container under them; the body is now the scroll region at both widths, measured from its own top since stacked it sits below the trigger. And the fade dissolved a `position: sticky` table header exactly while it was pinned at the region's top, so the body's leading edge is now tunable and a page that pins a header flattens just that edge.
633
+
634
+ Both edges stay masked, and stacked the region's edge now starts at the dropdown: the row took a 24px gap under the trigger, so content dissolved at a line floating below it. `tailwind-config` gains `--tw-scroll-fade-size-start`, which lets a region keep a shallow or flat leading edge — a mask dims a `position: sticky` table header pinned at the top along with the content it is fading, so kbc-ui flattens it on pages that pin one (`:has(.is-sticky)`) and every other page keeps the full fade.
635
+
636
+ - `SectionNav` and `PageLayout.Content nav` now survive a host app that emits the same Tailwind utilities they do.
637
+
638
+ Every property that differs between the two widths — `display`, `flex-direction`, `gap`, `width` — was written as a plain utility plus a `@[640px]` variant. An app whose own source contains `tw:hidden` / `tw:flex` / `tw:flex-col` / `tw:gap-6` / `tw:w-full` / `tw:m-0` ships an identical rule; at equal specificity in the same `utilities` layer the later stylesheet wins, and design's compiled CSS is deliberately imported first. So the plain utility outranked the variant meant to override it and the nav was stuck in one state — in kbc-ui, permanently invisible.
639
+
640
+ Those pairs are now two mutually exclusive container variants (`@[640px]` and `@max-[640px]`) with no plain fallback, and the sub-section list uses `my-0 mr-0` instead of `m-0` so no shorthand can flatten its `ml-5` indent. Selectors only this block emits cannot be outranked by a copy of a class the app also uses. No API or visual change.
641
+
642
+ Two more fixes in the same area. The collapsed dropdown's group is now a real menu item rather than a label — it is a destination in the tree (it goes to the group's first section), so a menu that made it unclickable was a different navigation rather than a smaller one. And `PageLayout.Header sticky` fades the background into the content just below itself: content passing under a sticky header emerges at a hard edge, and a card's own `0 2px 6px` top shadow lands exactly there, reading as a shadow cast by the header.
643
+
644
+ Adds `useOptionalSidebar(side)`, which returns the sidebar if there is one and `null` otherwise — for code that _enhances_ a page when the app shell is around it (borrowing width, collapsing a panel) and must still render without one, in a test, a story, or an embedded surface. Anything that genuinely requires a sidebar keeps `useSidebar`.
645
+
646
+ `PageLayout.Content nav` also gives the nav column and the content body **independent scroll regions** instead of one page scroll. It publishes `--section-nav-available` — the height actually left below the header, measured from the app shell's scroll container rather than assumed from the viewport, because a `100dvh` calc overshoots by the shell's navbar and bottom padding and makes the outer page scroll instead of the columns.
647
+
648
+ - `Tabs` triggers now paint their own focus ring, inset, instead of leaving the focus affordance to the browser or the host app.
649
+
650
+ `TabsList` wraps the row in a horizontal `ScrollArea`, whose viewport is `overflow-y: hidden`, and a trigger is exactly as tall as that viewport. Anything painted outside a trigger's border box was therefore cropped: keyboard-focusing a tab drew a ring that lost its top and bottom edges, and the first tab lost its left edge too. On Project Settings — eleven tabs in one scrolling row — every tab showed the clipped ring.
651
+
652
+ The trigger now sets `focus-visible:outline-hidden` plus an inset `ring-2 ring-ring`, painted within the border box where no ancestor can crop it. This is the same treatment `SectionNav` rows already use for the same reason. Consumers with a global `a, button { :focus-visible { outline: … } }` rule get the component's ring instead, at every column width.
653
+
654
+ - `Tooltip` with `triggerAsChild` no longer strips its child's selected state. Radix stamps the tooltip's own open/closed `data-state` on the trigger, and primitives that keep a `data-state` of their own — `ToggleGroup.Item`, `Switch`, `Tabs` triggers — spread incoming props after their own attribute, so the tooltip's value won and the child silently lost every `data-[state=…]` style it had.
655
+
656
+ - Markdown content inside a legacy (dark) tooltip is readable again — body text, bold, links and inline code all invert for the near-black tooltip fill.
657
+
658
+ The hook was already there: `[data-tooltip-content='true'] .tw\:prose { @apply tw:prose-invert }`. It was dead code. `prose-invert` works by redeclaring the same `--tw-prose-*` custom properties that `.tw\:prose` declares, and `.tw\:prose` is emitted into `@layer utilities` while the override sat in `@layer components`. The layer order at the top of `styles.css` is `theme, base, components, utilities`, so the utility's own declarations won on layer precedence no matter how specific the override was — cascade layers outrank specificity. Body copy and bold therefore stayed at the light-theme slate-blue (`#334155` / `#1e293b`) on `bg-neutral-800`, around 1.2–1.5:1: technically painted, effectively invisible. The rule moves into `@layer utilities`, after the block that defines `.tw\:prose`.
659
+
660
+ Two colours leak independently of the layer order and are now pinned explicitly:
661
+ - **Links.** kbc-ui's `Markdown` maps `<a>` to design's `Link`, which forces `tw:text-primary` (specificity 0,1,0). Tailwind Typography's own link rule is `:where()`-wrapped at specificity 0, so `--tw-prose-invert-links` could never reach it. Inverted or not, the link stayed the dark brand blue. It is now `text-blue-300` with a `hover:text-blue-200`.
662
+ - **Inline code.** Typography emits no background for inline `code`; the light `#f9f2f4` chip came from kbc-ui's vendored Bootstrap in `@layer legacy`. Inverting does not help either, because `--tw-prose-invert-code` is set to the same red `#c7254e` as the light token in `packages/tailwind-config/typography.ts`. The chip is now `bg-white/10` with `text-neutral-100`, scoped `:not(pre) > code` so a fenced block's `pre > code` keeps its own `--tw-prose-invert-pre-bg` instead of getting the chip painted over it.
663
+
664
+ Blockquotes needed the opposite correction. `.tw\:prose blockquote` lives in `@layer components` with a light `rgb(var(--muted))` fill, and inverting the prose turns the quote text light — so a blockquote inside a dark tooltip rendered light-on-light. Inside a legacy tooltip it is now re-filled with the same translucent `bg-white/10` wash, which composites over the fixed `bg-neutral-800` to a lighter dark. Quote text measures 10.19:1 against it, with a nested link at 6.64:1 and a nested inline-code chip at 7.18:1.
665
+
666
+ Argo tooltips are untouched — `data-tooltip-content` is only set when `useArgoDesign()` is false.
667
+
668
+ Covered by a `LegacyMarkdownContent` story in `Tooltip.stories.tsx`: an explanatory tooltip forced open under `ArgoDesignContext value={false}`, rendering a `tw:prose` subtree with `<strong>`, a `Link`, an inline `<code>`, a fenced `<pre><code>` block and a `<blockquote>` (itself containing bold, inline code and a link), so the visual baseline pins the inverted result.
669
+
670
+ - Updated dependencies:
671
+ - @keboola/brand-registry@1.8.0
672
+ - @keboola/tailwind-config@1.0.2
673
+ - @keboola/codemirror-lang-sql@0.6.0
674
+
675
+ ## 10.0.0
676
+
677
+ ### Major Changes
678
+
679
+ - Give `Card` the header chrome its consumers were hand-rolling, so a card's spacing comes from props rather than per-call-site classes.
680
+
681
+ **Breaking:** `Card.Header` previously rendered an unstyled `<div>` and now lays itself out. Any consumer that supplied only _part_ of the layout — flex and a gap but no `justify-*` — now also gets `justify-between` and will see its children pushed to opposite ends. Pass `layout="inline"` to keep them together.
682
+
683
+ `Card.Header` defaults to a **row** — `flex items-center justify-between gap-4`, the title-left / actions-right band that 30 call sites were reproducing by hand. The other two shapes are explicit:
684
+ - `layout="inline"` (`items-center gap-3`) — a title with an adornment: icon + title, title + badge.
685
+ - `layout="stack"` (`flex-col gap-1.5`) — title over description.
686
+
687
+ `Card.Header divider` adds a bottom rule plus the header's own padding and a `min-h-16` floor, so the rule spans the full card width and the band keeps a consistent 64px height whether it holds just a title or a title and an action button. (With padding alone the height tracks the tallest child, which made a header with a button visibly taller than one without.) `Card.Content padded` takes the standard body padding (`px-6 py-5`). Both are opt-in: without them the slots add no padding, so a card that pads its root is never doubled.
688
+
689
+ `Card padding="none"` now also drops the root's row gap. The slots own their padding in that mode, so a gap would space a `divider` header away from the content it is already ruled off from. The padded roots (`small` / `medium` / `large`) keep `gap-4`.
690
+
691
+ Together these replace the `Card bordered padding="none"` + `Card.Header className="…p-4"` + `Card.Content className="tw:p-0"` recipe:
692
+
693
+ ```tsx
694
+ <Card bordered padding="none">
695
+ <Card.Header divider>
696
+ <Card.Title>Tokens</Card.Title>
697
+ <Button size="small">New token</Button>
698
+ </Card.Header>
699
+ <Card.Content>{/* table bleeds to the card edge */}</Card.Content>
700
+ </Card>
701
+ ```
702
+
703
+ Migration, by what the header holds:
704
+ - title + actions → nothing to do, that is the default (and you can drop the hand-rolled `tw:flex tw:items-center tw:justify-between`).
705
+ - icon/badge + title → `layout="inline"`, and drop the hand-rolled `tw:flex tw:items-center tw:gap-*`.
706
+ - title + description → `layout="stack"`.
707
+ - **a single child that relied on filling the header** → `layout="stack"`. This one is easy to miss because the header looks untouched: the child used to be a block box at the header's full width, and as a flex item it shrinks to its content. A `Card.Title className="tw:text-center"` alone in a header goes from centred across the card to sized-to-its-text, so the centring silently stops doing anything.
708
+
709
+ A `Card.Header` that already sets `justify-*` itself is unaffected. Note that `cva` here concatenates rather than tailwind-merges, so adding `tw:justify-start` in `className` will **not** reliably beat the default — use `layout="inline"`.
710
+
711
+ - **Breaking: the package ships ESM only.** The CommonJS build is gone — no `*.cjs`, no `*.d.cts`,
712
+ no `main` field, and no `require` condition in the `exports` map.
713
+
714
+ Consume the package from an ESM-capable bundler (Vite, webpack, Rollup, esbuild) or `import` it
715
+ from Node. The ESM entries are plain-Node resolvable, so a Vitest/Node test suite needs no
716
+ special configuration — that was already true before this change.
717
+
718
+ `require()` now resolves to the ESM entry instead of a CJS one. On Node ≥ 22.12 that still
719
+ works, because Node can require an ES module; on older Node it throws `ERR_REQUIRE_ESM`, and a
720
+ CJS-only bundler will fail to load it. TypeScript resolves the `.d.ts` under `bundler` and
721
+ `node16`-from-ESM; a `node16`-from-CJS or `node10` consumer no longer gets types.
722
+
723
+ Nothing else about the import surface moves: the same eight entries, the same subpath
724
+ specifiers, the same type declarations.
725
+
726
+ The CJS half existed for no consumer we could find, and code-splitting made it actively worse:
727
+ esbuild's CJS re-export facade emits one `Object.defineProperty` getter per export, so for the
728
+ 5887 icon names in `@keboola/design/icons` the facade grew as large as the payload it wraps and
729
+ `require('@keboola/design/icons')` went from parsing 777 kB to 1574 kB. Dropping it takes 3.5 MB
730
+ across 34 files out of the published tarball (6.0 MB with sourcemaps) and removes the
731
+ dual-package hazard — one module graph, so one copy of every context and every module-level
732
+ identity.
733
+
734
+ ### Minor Changes
735
+
736
+ - New editor mode `text/x-bqsql`, backed by the BigQuery dialect of
737
+ `@keboola/codemirror-lang-sql`. A backtick quotes an identifier, so `` `dataset`.`table` ``
738
+ lexes as one qualified name — both for highlighting and for anything reading the parse
739
+ tree, which is what schema completion does — and BigQuery's own keywords, types and
740
+ functions are recognised.
741
+
742
+ `text/x-plsql` keeps the stock PL/SQL dialect for Oracle.
743
+
744
+ - `CodeEditor` takes a `variant`. `'field'` draws the chrome an `InputGroup` draws —
745
+ surface, border, radius, focus ring, read-only wash — for an editor that is one control
746
+ among others in a form, matching a `TextInput` in whichever design language the subtree
747
+ renders. It sizes to its content, so don't pin its height. `'plain'` is the default and
748
+ unchanged.
749
+
750
+ `SqlEditor` builds its chrome from the same presets. Its appearance is unchanged.
751
+
752
+ - The build is now code-split, so every subpath entry shares one copy of the code it has in
753
+ common with the others instead of inlining its own.
754
+
755
+ Two consequences for consumers:
756
+
757
+ **A provider now reaches every entry.** Module-level identities used to be per-entry, so the
758
+ `ArgoDesignContext` a component read depended on which bundle it shipped in. An app's
759
+ `<ArgoDesignContext value={false}>` was invisible to anything from `@keboola/design/rjsf`,
760
+ `/markdown-editor`, `/mermaid` or `/editors`, and `useArgoDesign()` inside those answered the
761
+ context default — argo. Those surfaces now follow the value the app provides, which means
762
+ **they render legacy chrome on a legacy-brand page where they previously rendered argo**.
763
+ `ThemeContext` was duplicated the same way; nothing outside the root barrel read it, so that
764
+ half was a latent trap rather than a visible bug. `ExportMenu` no longer pins argo
765
+ unconditionally — it follows the surrounding context like every other component.
766
+
767
+ **`@keboola/design/markdown-editor` keeps its lazy mermaid boundary.** `MarkdownEditor`
768
+ reaches `MermaidDiagram` through `React.lazy`, but an un-split build resolves an intra-bundle
769
+ dynamic import by inlining it and hoisting its external deps, so `dist/markdown-editor.js`
770
+ carried a static top-level `import 'beautiful-mermaid'`. That made the mermaid payload part of
771
+ the entry rather than a chunk behind it: bundlers that re-split their own graph ended up with
772
+ several copies of it (in kbc-ui, three chunks carried mermaid; now one), and
773
+ `require('@keboola/design/markdown-editor')` failed outright under plain Node, because
774
+ `beautiful-mermaid` publishes no CJS `exports`. It is a real chunk again.
775
+
776
+ `dist/` gains `chunk-*.js` files that the entries import; the `exports` map,
777
+ the entry filenames and the type declarations are unchanged. Total shipped ESM drops from
778
+ 282.9 kB to 227.9 kB brotli.
779
+
780
+ - `FileUpload` gains the escape hatches an app needs to make it the only drop zone it ships:
781
+ - **`useFileDropTarget`** — headless drop target for when the affordance is an existing subtree you style yourself, not a dashed box. Returns `getRootProps()` / `getInputProps()` / `isDragActive` / `rejections`, and accepts `noClick` / `noKeyboard` so wrapping an already-interactive subtree (a message composer, a form) doesn't open the file dialog on every click. `FileUpload` is now built on it, so there is one drop-target implementation.
782
+ - **`useFileUploadRejections`** — read the rejections of the most recent drop from anywhere inside `FileUpload`, to render your own "why was this refused" copy. `FileUpload` still ships none, and the hook is reset per drop, so the message can't go stale.
783
+ - **`FileUpload.FileList` `invalid`** — tints the selected-file rows with the destructive token, for a selection that fails validation after it was accepted. Visual only; pair it with the reason in an announcing element.
784
+ - **`FileUpload.DropZone` `inputId`** — puts an `id` on the underlying file input so an external `<Label htmlFor>` can address it. The zone also carries `data-slot="file-upload-dropzone"`, so end-to-end tests can address it without depending on a consumer's own class names.
785
+
786
+ `accept` and `onFilesRejected` are now typed with the package's own `FileUploadAccept` / `FileUploadRejection` (structurally identical to what they replace, so existing call sites are unaffected) instead of re-exporting `react-dropzone`'s types.
787
+
788
+ `FileUpload` is documented as brand-neutral rather than argo-only — it has no argo runtime gate, its chrome is semantic tokens, so it renders under legacy chrome too.
789
+
790
+ - `SqlEditor` takes a `completionSource`, the prop `CodeEditor` already has: completions the
791
+ caller knows and the schema does not — a table written to before it exists, or one whose
792
+ usable name is a path rather than the label it is matched on. `installExtensions` takes the
793
+ same source for callers wiring the language up themselves.
794
+
795
+ ### Patch Changes
796
+
797
+ - Correct the shipped `AGENTS.md` usage contract: Tailwind classes now use the v4 `tw:` variant prefix (was the v3 `tw-` prefix, which does not compile), and the primitives are described accurately as Radix for most components with Base UI for newer ones such as `Combobox`.
798
+
799
+ `AGENTS.md` ships inside the tarball, so external consumers reading `node_modules/@keboola/design/AGENTS.md` were being handed class names that silently produce no styles.
800
+
801
+ - Align the `FieldBox` control (Switch / Checkbox / `RadioGroup.Item`) to the first line of its content instead of the content block's centre. Items that render their own multi-line block — rather than passing `description` — no longer leave the control floating next to the middle line. Single-line items are unchanged.
802
+
803
+ `RadioGroup.Item`'s `className` now styles the item box for the `radio` / `radio-block` variants, where it previously landed on the small radio control inside it.
804
+
805
+ - `InputGroup`, the `Toggle` `ghost` variant and the legacy `Button` `invisible` variant follow the active color scheme instead of staying pinned to light-scheme literals. Adds an `input-background` token for the form-control fill.
806
+
807
+ All three hard-coded the values that happen to be right on a white page, so under `[data-theme='dark']` an input kept a white fill, a light border and near-black text, a pressed `ghost` toggle rendered as a bright pale-blue block, and an `invisible` button's label vanished into the panel.
808
+
809
+ **New token — `input-background`.** A form control's fill had no token of its own, so the only scheme-following surfaces available were container tokens. `card` is raised container chrome (Card / Modal / Sheet) and a brand tints it independently of what it wants inside a field, so the field now gets its own slot: `--input-background` / Tailwind `bg-input-background`, brand-overridable through `colors.inputBackground`. It pairs with `input`, which stays the control's _border_. Defaults are `#ffffff` light and `#2f3037` dark — the dark value clears `background`, `card` and `popover` so a field reads as its own surface in all three.
810
+ - `InputGroup` takes its border from `input`, and the `primary` variant its fill from `input-background` and its text from `foreground`. `primary` also re-pins its readonly fill to `muted`/`input`, the pair `argo` already uses — the shared readonly fill is a fixed light grey that scheme-following text would sit on invisibly. The `argo` variant keeps its `muted` fill: a grey inset is deliberate there, and converging the two is a brand-value decision.
811
+ - `Toggle` `ghost` takes its hover fill from `muted` and its pressed fill from a translucent blue tint that composites over whatever surface it sits on.
812
+ - `Button` `invisible` (legacy) takes its label from `foreground` and its hover / active fills from `muted` / `secondary`. A transparent button inherits whatever surface it is dropped on, so it is the one legacy variant that cannot assume a light page; pinning it forced dark-scheme call sites to re-declare the colour by hand. The argo `invisible` variant already read `foreground`.
813
+
814
+ Light-scheme deltas are limited to the `invisible` button, whose label moves from `neutral-800` (`#222529`) to `foreground` (`#1e293b`) — converging it with the ~90 surfaces already on that token — and whose hover / active fills move from `neutral-100` / `neutral-150` to `muted` / `secondary` (`#edf0f5` → `#f1f5f9`, `#d9dde5` → `#e2e8f0`). Everything else is byte-identical: `input-background` is white in the light scheme, and `input` / `foreground` / `muted` resolve to the greys the literals named (a readonly `primary` input's fill lightens by one step).
815
+
816
+ `InputGroup`'s `secondary` variant and the error scales are still light-only.
817
+
818
+ - `SuggestedPrompt`: paint hover and the label from scheme-aware semantic tokens (`hover:bg-muted`, `text-foreground`) instead of fixed `neutral-*` values, so the row no longer lights up as a near-white pill under `colorScheme="dark"`. The default icon also keeps the AI gradient on hover — it previously swapped to a flat `secondary-600` stroke, which read as the glyph being replaced.
819
+
820
+ - `Sidebar.MenuButton` no longer puts button-only attributes on a slotted link. It emitted `type="button"` and `aria-selected` (defaulting to `false`) unconditionally, including under `asChild` — where the slot is normally an `<a>`, the canonical nav item. `aria-selected` is not supported on a link, so axe reported a **critical** violation on every sidebar built the documented way.
821
+
822
+ `type` is now applied only when rendering a real `<button>`, and `aria-selected` only when a caller passes it explicitly. Mark an active link with `aria-current="page"`, which `Sidebar.MenuButton` already styles identically.
823
+
824
+ - Updated dependencies:
825
+ - @keboola/brand-registry@1.7.1
826
+ - @keboola/tailwind-config@1.0.1
827
+ - @keboola/codemirror-lang-sql@0.5.0
828
+
829
+ ## 9.1.0
830
+
831
+ ### Minor Changes
832
+
833
+ - `Combobox` can now window long option lists with `@tanstack/react-virtual`, so large
834
+ enumerations (1,000+ items) stay fast to open and scroll. It is **opt-in** via the new
835
+ `virtualized` prop (default `false`, so existing call sites are unchanged): pass
836
+ `'auto'` to virtualize only ungrouped lists longer than 30 items, or `true` to force
837
+ it. Filtering/search and keyboard navigation run against the full item set, so search
838
+ behavior is unchanged. Grouped lists are not virtualized.
839
+
840
+ `Combobox` also now shows the full option list when opened with a value already selected,
841
+ instead of filtering down to just the selected item's label — so you can browse and pick a
842
+ different option without clearing first. Typing still filters as before.
843
+
844
+ - `CodeEditor` gains `inputLocked`, for callers that hold the editor for the length of an in-flight save so a keystroke arriving mid-request isn't overwritten by the response.
845
+
846
+ `readOnly` was what those callers reached for, and it is a viewer switch: it greys the surface, tears down the JSON lint marks and drops autocompletion. `inputLocked` flips `EditorState.readOnly` alone — input, paste, drop and the doc-changing commands are refused, while the caret, the surface, the lint marks and the completions stay as they were. `readOnly` keeps its meaning, a permission the user does not have, and the two compose.
847
+
848
+ **Fixed — flipping `readOnly` on a live editor reset its undo history.** The viewer's `basicSetup` turned `history` off, and because a `basicSetup` change reconfigures the view, the reconfigure discarded the state field along with the extension. A viewer can never add a history entry, so leaving it installed saves nothing; it now stays. This also covers editors that reach `readOnly` through a `disabled` prop upstream and so cannot use the new one.
849
+
850
+ - The `text/x-sfsql` editor mode now uses the Snowflake dialect from `@keboola/codemirror-lang-sql`, and `@keboola/codemirror-lang-sfsql` is gone.
851
+
852
+ That package was a copy of CodeMirror 5's **MySQL** mode, so Snowflake code was highlighted by a tokenizer that had never heard of it: `MERGE`, `QUALIFY`, `IFF`, `FLATTEN`, `OBJECT_CONSTRUCT`, `LATERAL`, `ROW_NUMBER`, `TO_DATE`, `STRING` and `NUMBER` were not recognised, `"quoted_identifier"` was read as a string literal, a `$$ … $$` body was read as SQL rather than as one string, and MySQL-only words such as `auto_increment` and `innodb` were highlighted as keywords. Being a stream parser it carried no comment tokens and no completions either, so the design system hand-patched both in — that wrapper is deleted, since `SQLDialect` supplies them. Keyword completion additionally stops firing inside comments and string literals, which the hand-wired list did not guard.
853
+
854
+ Visible beyond that: plain identifiers and built-in functions are now coloured, data types change colour, and the document gains statement nodes, so folding and indentation work.
855
+
856
+ `getSqlDialect(name)` is a new export, for an editor that wants the language without the rest of the SQL editing stack — `installExtensions` brings its own completions, keymaps and popup theme, and reads `document.body`, so it cannot stand in.
857
+
858
+ One vocabulary fix goes with it, measured against the tokenizer: `over`, `partition` and `within` were missing from the keyword list, and the first two were highlighted by the old MySQL table — so leaving them out would have been the one place this swap made highlighting worse.
859
+
860
+ - Neither editor rebuilds its extension tree any more when a caller re-renders with a freshly-built `variables` list or handler.
861
+
862
+ `variables` keyed both editors' extension memo by array identity, and callers build that list inline — mapping a configuration row to `{ name, value }` inside a store selector, so a fresh array arrived on every store push. Rebuilding the tree re-runs the SQL schema walk, milliseconds for a real catalogue. Both now share `useVariableCompletion`, whose identity never moves: CodeMirror asks the completion source for options rather than reading them up front, so a request always reaches the latest render's variables and the editor never has to be reconfigured for them at all.
863
+
864
+ `SqlEditor` additionally passed `onChange`, `onSelectionChange`, `queryParser`, `onFocusEditor` and `onBlurEditor` straight through, all of which CodeMirror holds by identity. They are stable now and read the latest render when they fire — the same treatment `CodeEditor`'s `onChange` already had.
865
+
866
+ `onDestroyEditor` now reports once, when the editor actually goes away. It was keyed on the handler's identity, so a caller passing an inline arrow was told the editor had been destroyed on every render.
867
+
868
+ `SqlEditor` drops `extraKeys`. Nothing passed it, and unlike `CodeEditor`'s it took `KeyBinding[]` — an array whose identity would have reconfigured the editor on every render.
869
+
870
+ `schema` on `SqlEditor`, and `autocompletions` / `completionSource` on `CodeEditor`, are still compared by identity, and now say so in their types. A schema is too large to compare by value — hand the editor a stable object.
871
+
872
+ - `useStableCallback` is now exported from the package root.
873
+
874
+ The hook already shipped inside the package for `CodeEditor` and `MergePane`, while kbc-ui's chat carried its own copy for the callbacks it puts into a context value. Exporting it leaves one definition of "a callback whose identity never changes but which always sees the latest render".
875
+
876
+ The two copies differed in when they swap the ref: chat's used an insertion effect, this one uses a layout effect. Both land before the browser can paint, and the layout effect is the on-label hook — React scopes `useInsertionEffect` to CSS-in-JS. The difference is observable only to a consumer that calls the returned function from its own layout effect, which the hook's docblock now names.
877
+
878
+ ### Patch Changes
879
+
880
+ - `CodeEditor` hands over its `autocompletions` list only when it holds something, and the prop now says that its labels have to be single words.
881
+
882
+ CodeMirror wraps a list in `completeFromList`, which answers an empty result rather than none — so an editor left with the default empty list still contributed a source the completion engine ran on every keystroke, unable to match anything. With the default JSON mode, which supplies no completions of its own, that was the only source there was. The registration is now skipped where there is nothing to offer, and joins the variable source in one entry list.
883
+
884
+ The same derivation is what makes a label with a space in it a trap: the pattern for the word being completed comes from the characters of the labels, so one space stretches it back over the words already on the line and the editor goes quiet past the first one. Nothing in the prop said so. A phrase — like anything the cursor's position decides — belongs in `completionSource`, which owns that pattern itself.
885
+
886
+ - Fix `DateRangeInput` shifting typed ISO dates by a day. `parseDateRange` built its dates with
887
+ `new Date('2024-01-15')`, which the spec treats as **UTC midnight**, and the component reads them
888
+ back through local-time accessors (`getDate`, `toLocaleDateString`). West of UTC that rendered the
889
+ previous day — a range typed as `2024-01-15 - 2024-01-20` displayed as `1/14/2024 - 1/19/2024` for
890
+ every US user; east of UTC the dates landed at 09:00 rather than local midnight.
891
+
892
+ The date-only ISO form is now parsed as local time. The other accepted formats (`01/15/2024`,
893
+ `Jan 15, 2024`, `2024/01/15`) were already local and are untouched, and `new Date`'s validation is
894
+ preserved — an out-of-range month or day (`2024-13-45`) is still rejected rather than silently
895
+ rolling over, which is why this appends a time instead of building parts via `new Date(y, m - 1, d)`.
896
+
897
+ The emitted `Date` objects are now local midnight, so format them with local getters
898
+ (`toLocaleDateString`) rather than `toISOString().slice(0, 10)`. This is not a new constraint: the
899
+ calendar-picker path already produced local-midnight dates, so typed input was the odd one out. The
900
+ two input paths now agree.
901
+
902
+ - Updated dependencies:
903
+ - @keboola/codemirror-lang-sql@0.4.0
904
+
905
+ ## 9.0.0
906
+
907
+ ### Major Changes
908
+
909
+ - There is now one definition of a Keboola configuration-variable completion, and `CodeEditor` owns it.
910
+
911
+ Two existed. `SqlEditor` got one from `@keboola/codemirror-lang-sql`, which inserted `{{ name }}` with spaces and showed the literal word "variable" as the detail; every transformation editor got another from kbc-ui, which inserted `{{name}}` and showed the variable's value. A user who touched both editors in one session met two spellings of the same thing.
912
+
913
+ **`CodeEditor` gains `variables`.** Pass `{ name, value? }[]` and it completes them into `{{name}}` — the form transformation authors have been writing since long before this editor rendered them, and the one the rest of the platform matches on. It is independent of `completionSource`: both are offered, so a caller keeps its own completions and still gets variables.
914
+
915
+ The completion's `label` is the bare name, with the braces in `displayLabel`. That is not cosmetic: `SqlEditor` filters prefix-only (`filterStrict`) while `CodeEditor` filters fuzzily, and a bare label is the only spelling both accept whether or not the caret has already typed `{{`. The popup is unchanged — it renders `displayLabel`.
916
+
917
+ **`@keboola/codemirror-lang-sql`:** `installExtensions` no longer takes `variables: string[]`. It takes `variableCompletion?: CompletionSource` and has no variable implementation of its own — the package knows about SQL, not about Keboola's placeholder syntax. `SqlEditor` supplies the source; any other caller supplies its own.
918
+
919
+ **Breaking, `@keboola/design`:** `SqlEditor`'s `variables` prop widens from `string[]` to `{ name, value? }[]`. Every call site already held both fields and was discarding the value; the SQL editor now shows it, as the transformation editors always have.
920
+
921
+ ### Minor Changes
922
+
923
+ - `CodeEditor` gains two props for editors that complete against project data:
924
+ - `completionSource` takes a CodeMirror `CompletionSource`, so completions can depend on where the caret is — a table's columns offered only once that table has been named. `autocompletions` stays the right prop for a list that is the same everywhere in the document; the source wins over it and over `completeAny`. Both feed CodeMirror's `autocomplete` language-data slot, which already accepted either shape.
925
+ - `withAutocompleteHint` renders the keyboard hint for triggering completion, the counterpart to `withToggleCommentHint`.
926
+
927
+ - `CodeEditor` gains `searchQuery` and `searchMatchIndex` — the props `SqlEditor` already had, driving the same `search.ts` engine: every occurrence is highlighted and the selected one is scrolled into view.
928
+
929
+ The find UI stays with the caller. That is deliberate: the editor cannot know whether the input belongs above it, in a modal header, or in a page toolbar, and the CodeMirror 5 editors this replaces put it in all three places by portalling their dialog out of the editor.
930
+
931
+ The extension installs only when a caller passes the prop — the gate is presence, not truthiness — so an editor that never searches carries none of it, and typing a query does not swap the extension set. A view created while a query is already set (a remount, a tab switch) picks the search up as it is built.
932
+
933
+ - `CodeEditor` drops `withAutocompleteHint` and the `Alt-Space` binding that came with it. Completions now open while you type, which is the only trigger that works everywhere: on macOS `Ctrl-Space` switches input source and `Alt-Space` either types a space or is claimed by a launcher, so the hint advertised a shortcut that did nothing. `Ctrl-Space` still works where the platform allows it — it just is not promised any more.
934
+
935
+ ### Patch Changes
936
+
937
+ - `CodeEditor`'s completion popup is no longer cropped when the editor is short and sits inside a modal — the dbt Execution Step is a one-line editor, and it showed an unreadable sliver of the list.
938
+
939
+ CodeMirror hangs tooltips inside the editor and lets `position: fixed` carry them past its edges, which holds only while the viewport is their containing block. A modal centred with a `translate` takes that job, and from there both the editor's own scroll wrapper and `Modal.Body` crop the popup; `Modal.Body`'s argo fade mask crops it independently of either. A tall editor hides all of this, which is why it surfaced only once an editor with no height existed.
940
+
941
+ The tooltip layer is now hosted in that containing block, which clears the scrollers and the mask in one step while keeping the popup inside the modal's stacking, focus and pointer-events scope — `document.body` would keep none of those, and the popup would land behind the modal.
942
+
943
+ - Four fixes to `CodeEditor`, all of which made an editor quietly do less than it looked like it did:
944
+ - **Toggling a comment did nothing on `text/x-sfsql`, `text/jsonnet` and `text/x-dbt`.** `Mod-/` was bound, but the command reads the comment tokens off language data and no-ops without them — and the modes we author ourselves declared none. The `lang-sql` dialects and the upstream legacy modes were unaffected.
945
+ - **`text/x-sfsql` completed no keywords.** Every other SQL dialect gets them from `@codemirror/lang-sql`; Snowflake is a legacy stream parser and carries none. They now come from the table the parser already tokenizes with.
946
+ - **Autocomplete could not be triggered on macOS.** CodeMirror binds `Ctrl-Space`, which macOS takes for switching input source before the browser sees it; its Mac alternatives are not discoverable. `Alt-Space` is now bound too, and both keyboard hints name the shortcut that works on the platform reading them rather than listing every variant.
947
+ - **The editor reconfigured on every keystroke** when a caller passed a freshly-built `extraKeys` object, which is how callers write it. That dispatched a full `StateEffect.reconfigure` per keypress, and for the stream-parser modes a reconfigure rebuilds the `Language`, discarding the syntax tree and re-parsing the document. Only the set of bound keys now keys the extensions; the handlers are read when the key is pressed.
948
+
949
+ - The dbt mode completes past the first word again, and now offers only what the cursor's position can hold.
950
+
951
+ `completeFromList` derives the pattern for "the word being completed" from the characters its labels are made of. Two dbt commands are two words — `source freshness` and `docs generate` — so a space landed in that character class and the pattern ran back to the start of the line: on `dbt r` the text to complete was `dbt r`, which matches no label, so the popup was empty everywhere except the first word. The mode now names the pattern itself.
952
+
953
+ With the boundary fixed, the vocabulary is offered per slot rather than all at once: `dbt` as the first word, commands after it, `generate` / `freshness` after the command that takes them, arguments and flags after a command, and selector methods only where a `--select` / `--exclude` / `-s` gives them something to select. Each group carries the completion `type` matching the colour the mode paints it in, so the popup icons line up with the highlighting.
954
+
955
+ - `text/x-dbt` now completes the dbt vocabulary — commands, arguments, methods and flags. It is the same list the mode already highlights with, so the two cannot drift.
956
+
957
+ - `SqlEditor` hosts its completion popup the same way `CodeEditor` does, so a SQL editor in a modal is no longer cropped either.
958
+
959
+ `installExtensions` used to end with `tooltips({ parent: document.body })`, paid for by a `z-index: 1051` override on the popup — a language package deciding where an overlay lives. Body hosting also puts the popup outside the modal's pointer-events scope, so its items cannot be clicked while a dialog is open. Both the parent and the z-index override are gone; `SqlEditor` installs the design system's tooltip extension instead, which hosts the popup in the containing block for `position: fixed`.
960
+
961
+ The two mechanisms did not compose — CodeMirror's tooltip config takes the first contributor that names a parent — so there is now exactly one, and both editors' stories assert the same promise.
962
+
963
+ - Updated dependencies:
964
+ - @keboola/codemirror-lang-sql@0.3.0
965
+
966
+ ## 8.0.0
967
+
968
+ ### Major Changes
969
+
970
+ - **Breaking:** `CodeEditor`'s `withCopyButton` now defaults to `false`. An editor that was showing a copy button without asking for one will stop; pass `withCopyButton` to keep it.
971
+
972
+ It defaulted to `true`, and consumers opted out 12 times against 4 intentional uses in 3 files — every opt-out on a surface that already had its own copy control or wanted none. It also matches the CodeMirror 5 editor that kbc-ui is migrating away from, which renders no copy button at all, so migrated call sites keep their appearance rather than each gaining a chip.
973
+
974
+ **The copy toolbar reserves its own space.** It is positioned out of flow and translated above the editor's top edge, overhanging it by 32px, but space was reserved only via `{ 'tw:mt-5': !!renderAdditionalButtons }` — keyed to the wrong prop, so a bare `withCopyButton` reserved nothing, and 20px short even where it applied. Both sites using the toolbar overlapped the content above them. The reservation now follows whether the toolbar actually renders, which includes `ButtonToolbar`'s own rule that it draws nothing without content — so an empty editor no longer leaves a gap.
975
+
976
+ **A read-only editor no longer installs the editing stack.** Measured on a read-only editor: 5 fewer StateFields, 5 fewer ViewPlugins, 27 fewer extension slots and 21 fewer keybindings — undo history that can never receive an entry, bracket closing, autocompletion, and a fold gutter the theme then hides. Search stays, since `Mod-f` is useful on a viewer. This follows the gate `shouldLintJson` already applies to the JSON linter.
977
+
978
+ `SqlEditor` and `MergeEditor`'s panes now share that setup instead of each hand-writing their own, so a read-only SQL viewer and two of a merge view's three panes stop installing it too.
979
+
980
+ Mount cost falls by 0.07–0.27 ms per read-only editor, and `dist/editors.js` grows by 106 B gzip — the flags are branched on at runtime inside `basicSetup`, so nothing tree-shakes out.
981
+
982
+ **`readOnly` accepts a variable, not just a literal.** The props discriminated on `readOnly: true | false`, which rejected `readOnly={canEdit ? false : true}` and every other computed value — a permission-gated field is editable or not depending on the user, and had no way to say so. The editable branch now reads `readOnly?: boolean`, so an editor with an `onChange` may toggle. Declaring `readOnly` with no `onChange` still type-checks, and omitting both still does not.
983
+
984
+ ### Minor Changes
985
+
986
+ - `Clipboard` accepts `tooltip={null}` for a trigger that has nothing to say until the copy resolves:
987
+
988
+ ```tsx
989
+ <Clipboard asChild tooltip={null} text={snippet}>
990
+ <Button>Copy</Button>
991
+ </Clipboard>
992
+ ```
993
+
994
+ `Tooltip.Content` is no longer rendered when `tooltip` is `null` or `undefined`, so hovering opens the
995
+ tooltip with nothing in it rather than an empty bubble, and the Copied / Failed outcome still appears
996
+ once the copy completes. Passing tooltip content behaves exactly as before.
997
+
998
+ - `CodeEditor` gains three language modes: `text/jsonnet`, `text/x-dbt` and `text/x-toml`.
999
+
1000
+ TOML is `@codemirror/legacy-modes` wired straight in, like `text/x-rsrc` and `text/x-sh` before it. dbt goes through `simpleMode` from the same package — CodeMirror 5's `defineSimpleMode` in CodeMirror 6 form — so its rule table carries over as data.
1001
+
1002
+ jsonnet has no CodeMirror 6 grammar anywhere: the language's authors published one for CodeMirror 5 as `google/codemirror-mode-jsonnet` and never ported it. This is that mode as a `StreamParser`, ported from upstream rather than from the copy this repo has been carrying, which had drifted from it. Verified by tokenizing the same 36 documents through both engines: identical everywhere except where the local copy had drifted, in two places.
1003
+ - A `|||` text block ends at the first line indented less than its own first line. The local copy assigned `textBlockCol` where every read was of `textBlockIndent`, so the indent was never cleared between blocks and a second block was measured against the first one's — painting every shallower line as an error. It also latched the indent before looking for the closing `|||`, so a block terminated at column zero never ended at all.
1004
+ - `$` — the root-object reference — was matched as `/\$\b/`, so in `$.x` the word boundary failed and it fell through to the operator pattern. Upstream styles it as a keyword.
1005
+
1006
+ Every pattern in the port is anchored and hoisted, which the original's were not. `StringStream.match` slices the rest of the line and, without `^`, scans all of it for a match it discards unless it started at the cursor — and this tokenizer makes up to twenty probes per token. Measured: 1.8× faster on a 40-line document, 14.8× on a single 2,540-character line.
1007
+
1008
+ - Add plain-text and three SQL dialect modes to `CodeEditor`, an `ariaLabel` prop, and derive the mode
1009
+ list from a single const.
1010
+
1011
+ `text/plain` installs no language, so a plain-text document — a `requirements.txt`, a shell transcript —
1012
+ is no longer coloured as if it were JSON. `text/x-mysql`, `text/x-pgsql` and `text/x-mssql` complete the
1013
+ `@codemirror/lang-sql` dialect family already reachable through `mode`; each is pinned by a test over a
1014
+ construct only that dialect accepts (backtick identifiers and hash comments, dollar-quoted strings,
1015
+ bracket identifiers).
1016
+
1017
+ `ariaLabel` names the editable region for screen readers. The editor renders no visible label of its own,
1018
+ so an editor that is the only control in its form section had no accessible name at all.
1019
+
1020
+ `LANGUAGE_MODES` is now a `const` that `LANGUAGE_MODE` derives from, and both it and an `isLanguageMode`
1021
+ guard are exported from `@keboola/design/editors`. Two things this fixes: the RJSF `CodeEditorWidget` kept
1022
+ a hand-copied `LANGUAGE_MODE[]` whitelist and silently coerced any mode missing from it back to
1023
+ `application/json` — an annotated copy the type checker could not keep in sync — and consumers had no way
1024
+ to name the mode type, so they reconstructed it from `ComponentProps`. `getLanguageMode` also no longer
1025
+ defaults unrecognised modes to `json()`; they now render unstyled, so a mode added to the list without a
1026
+ parser degrades instead of mis-colouring.
1027
+
1028
+ All modes and the prop are additive; existing call sites are unaffected.
1029
+
1030
+ - Flip `Button` chrome casing to standard case by default; only legacy brands
1031
+ (`keboola-legacy-*`) uppercase the label. Previously the shared legacy-chrome base
1032
+ unconditionally uppercased every button label; now the base renders standard case and a
1033
+ `[data-brand^=keboola-legacy]` override re-applies `uppercase` (+ chrome letter-spacing)
1034
+ for legacy brands only. So modern brands (incl. Keboola V2) read naturally — including on
1035
+ not-yet-migrated kbc-ui surfaces that still render legacy chrome (argo is decoupled from the
1036
+ brand) — while `keboola-legacy-*` is unchanged. Mirrors the V2 standard-case treatment shipped
1037
+ for Tooltip/Badge; unblocks the UT-4130 Track C button sweep. No API changes.
1038
+
1039
+ - Keep `beautiful-mermaid` (and elkjs, ~442 kB gzip) out of consumers' entry chunks. Two static paths
1040
+ reached it, and both had to go:
1041
+ - The root barrel reached it: `index.ts` -> `blocks/AIKit` -> `Markdown` -> `MermaidDiagram` ->
1042
+ `beautiful-mermaid`. AIKit's `Markdown` now loads the mermaid entry on demand, showing the same
1043
+ skeleton `MermaidDiagram` already renders while its own runtime starts, and falling back to the
1044
+ source text (with a logged error) if the chunk cannot load. Mermaid code fences render as before,
1045
+ one tick later.
1046
+ - `svgToPngBlob` and `toSvgFileContent` were reachable only through `@keboola/design/mermaid`, so a
1047
+ caller wanting those two helpers dragged the whole subpath in. They are exported from the root
1048
+ barrel now.
1049
+
1050
+ **Breaking, on undocumented surface — hence `minor`, not `patch`:** `@keboola/design/mermaid` no
1051
+ longer re-exports `svgToPngBlob` / `toSvgFileContent`. Import them from `@keboola/design` instead.
1052
+ That subpath is documented as exporting `MermaidDiagram` only, and nothing in this repo used the
1053
+ helpers from there.
1054
+
1055
+ - Two new region marks on `@keboola/design/icons` — `Eu` (the European flag) and `Us` (the flag of the
1056
+ United States):
1057
+
1058
+ ```tsx
1059
+ import { Eu, Us } from '@keboola/design/icons';
1060
+
1061
+ <Eu className="tw:size-4" />
1062
+ <Us className="tw:size-5" />
1063
+ ```
1064
+
1065
+ They follow the same contract as the existing `Aws` / `Azure` / `Gcp` marks: typed `LucideIcon`, so
1066
+ they drop into any `icon`-shaped API next to a Lucide glyph, and sized on Lucide's 24-unit grid via
1067
+ `size` / `tw:size-*`. Being flags, they are **decorative and fixed** — their paths carry their own
1068
+ fills, ignore `color`, and do not re-skin under a customer brand.
1069
+
1070
+ Both are drawn as a **square 20 × 20 tile** with a 2-unit corner rather than at their official flag
1071
+ ratios (2:3 and 10:19), the way icon sets ship "squared" flag variants. At the real ratios the
1072
+ artwork is 13.3 units tall, which puts the EU's star ring and the US's 13 bands under a pixel at
1073
+ `tw:size-4` — the EU mark in particular renders as a solid blue block. Squaring makes both 1.5×
1074
+ larger, and the pair shares one silhouette so they line up in a list.
1075
+
1076
+ Two further calls, both about icon scale rather than fidelity:
1077
+ - `Eu` keeps all 12 stars and the exact colors, but draws them larger than spec — circumscribed
1078
+ radius hoist/10 on a ring of hoist/3.4, against the official hoist/18 and hoist/3. The official
1079
+ ratios are sized for a flagpole; at 16px they disappear. These read as a gold ring at 16px and
1080
+ resolve into twelve distinct stars by 48px.
1081
+ - `Us` keeps all 13 bands but omits the 50 stars: each would be well under a grid unit, which only
1082
+ muddies the canton. Solid blue plus the bands reads unmistakably at this size.
1083
+
1084
+ - **Breaking:** `field.SelectField` no longer accepts the deprecated `options` prop — pass `items`
1085
+ instead, matching the base `Select` and the other data-driven pickers (`Combobox`,
1086
+ `MultiCombobox`). `items` is now a plain required prop, so the discriminated union that made
1087
+ "one of `items` / `options`" expressible is gone and the prop types are a single object.
1088
+
1089
+ ```diff
1090
+ -<field.SelectField label="Region" options={REGION_OPTIONS} />
1091
+ +<field.SelectField label="Region" items={REGION_OPTIONS} />
1092
+ ```
1093
+
1094
+ - `field.SelectField` now takes the same `items` as the base `Select` — flat items **or** grouped
1095
+ items with optional headings — instead of a private flat-only copy of the shape:
1096
+
1097
+ ```tsx
1098
+ <field.SelectField
1099
+ label="Fruit"
1100
+ items={[
1101
+ { label: "Citrus", items: [{ value: "orange", label: "Orange" }] },
1102
+ {
1103
+ label: "Berries",
1104
+ items: [{ value: "strawberry", label: "Strawberry" }],
1105
+ },
1106
+ ]}
1107
+ />
1108
+ ```
1109
+
1110
+ The union (`SelectItem | SelectItemGroup`, both already exported) is discriminated by the presence
1111
+ of `items`, so a group needs `items` and a leaf needs `value` — mixing them is a type error. Item
1112
+ `icon`s now render too; the connector's own item type silently dropped them.
1113
+
1114
+ This comes from `SelectField` delegating to `Select` rather than re-composing `Select.Root` /
1115
+ `.Trigger` / `.Content` itself, which is how the sibling connectors (`TextField` → `TextInput`)
1116
+ already work. To make that possible, `Select` forwards a few more props to its trigger and root:
1117
+ `onBlur`, `aria-describedby`, `aria-invalid`, `aria-required`, `dir`, `autoComplete`, `form`.
1118
+
1119
+ Two `SelectField` prop changes fall out of the delegation:
1120
+ - `defaultValue` is no longer accepted — the form field owns the value, and passing both fought.
1121
+ - `name` is no longer accepted; it is derived from the field, and now reaches the Radix root (which
1122
+ renders its hidden native select for form integration) instead of the trigger button.
1123
+
1124
+ - `Toggle` takes an `orientation` prop — `'horizontal'` (default, unchanged) or `'vertical'`, which
1125
+ stacks the label under the icon:
1126
+
1127
+ ```tsx
1128
+ <Toggle orientation="vertical" icon={<Keboola />} size="large">
1129
+ ChevronDown
1130
+ </Toggle>
1131
+ ```
1132
+
1133
+ Vertical also lets the label wrap (`whitespace-normal` + `break-words`, centred), so a tile no longer
1134
+ has to be as wide as its longest label. Existing call sites are unaffected — the default keeps the
1135
+ current single-line row.
1136
+
1137
+ ### Patch Changes
1138
+
1139
+ - Refresh `remark-gfm` from 4.0.0 to 4.0.1, the newest version already allowed by the declared
1140
+ range. The release is types and docs only — exposed types moved to `interface`, declaration maps
1141
+ were added — with no runtime change to GFM parsing.
1142
+
1143
+ - Fix `MessageBubble` never applying `text-foreground` to nested AIKit `Markdown`. The rule was written
1144
+ as the arbitrary variant `tw:[&_.tw\:prose]:text-foreground`, which produced dead CSS: Tailwind's
1145
+ content scanner reads raw file bytes, so the `\:` escape in the TS string reached the generator
1146
+ doubled. The emitted selector both carried a malformed descendant (`.tw\\:prose` — an escaped
1147
+ backslash plus a bogus `:prose` pseudo-class, which is what lightningcss warned about during minify)
1148
+ and no longer matched the class the component actually renders.
1149
+
1150
+ `Markdown` now marks its container with `data-prose`, and `MessageBubble` hooks on that attribute
1151
+ instead. The attribute selector has no colon to escape and outranks the typography plugin's own
1152
+ `color` declaration on `.prose`.
1153
+
1154
+ Visible effect: assistant markdown in AIKit message bubbles picked up the typography plugin's default
1155
+ body colour (a hardcoded `gray-700`) rather than the theme's `--foreground`. Consumers rendering
1156
+ `Markdown` inside `MessageBubble` will see its text colour follow the active theme — most noticeably
1157
+ in dark mode, where the old colour was near-invisible. The `data-prose` attribute is additive; no API
1158
+ changed.
1159
+
1160
+ - The editors no longer leak a document stylesheet rule per render, and reconfigure the live view far less often.
1161
+
1162
+ `@uiw/react-codemirror` built a fresh `EditorView.theme({ height, … })` on every render and spliced it into the extensions it reconfigures the view with — and `StyleModule` never removes rules, so each reconfigure mounted one more into a shared document `<style>`, permanently. Measured before the fix: 200 parent re-renders of one editable editor took the document from 210 to 410 rules, never reclaimed; 76 mounts left 253 rules behind after unmount. Every insertion invalidates style for the whole document.
1163
+
1164
+ **That is fixed upstream in `@uiw/react-codemirror` 4.25.11**, which hoists the static rule to a module-level theme and caches the dimension theme by key. This release bumps to it, which fixes `CodeEditor`, `SqlEditor` and `MergeEditor` at once and for every prop that triggers a reconfigure, not just the handler.
1165
+
1166
+ Two things the bump does not fix, because they are ours:
1167
+ - **`MergeEditor`'s panes reconfigured on every render regardless of their props**, because `basicSetup` was an inline object literal and CodeMirror compares it by identity. `MergeEditor` mounts three panes and its conflict modal re-renders per keystroke, so that was three full extension-tree rebuilds per character typed. The object is now a module constant.
1168
+ - **`CodeEditor` and `MergeEditor` reconfigured whenever the caller's `onChange` identity moved**, which for an inline arrow is every render. Both now reach the handler through a stable wrapper, so a re-render costs nothing.
1169
+
1170
+ A reconfigure rebuilds and re-resolves the whole extension tree — roughly 25 extensions, their state fields and view plugins, then a re-measure. The tests assert the dispatch count directly rather than the stylesheet rules it used to leak, so they keep meaning something now that the library no longer leaks them.
1171
+
1172
+ - `@keboola/design/icons` is now tree-shakeable. The barrel was `export * from 'lucide-react'`, and that
1173
+ star form does not survive the trip through the package's built `dist/icons.js` — bundlers materialize
1174
+ the whole namespace instead of resolving each binding, so a consumer importing a handful of icons
1175
+ shipped the entire icon set (~177 kB gzip in kbc-ui).
1176
+
1177
+ No API change: the same names are exported, `Google` / `Keboola` / `Microsoft` and the `LucideIcon` /
1178
+ `LucideProps` types are unaffected, and no import site needs editing. `dist/icons.js` grows to
1179
+ ~17.6 kB brotli and `dist/icons.d.ts` to ~90 kB — re-export statements only, and consumers get far
1180
+ more than that back.
1181
+
1182
+ - Render `Tooltip` (action variant) and `Badge` (default variant) labels in standard case under the
1183
+ Keboola V2 brand (`data-brand="keboola"`), instead of the legacy uppercased chrome. The uppercase base
1184
+ stays, so `keboola-legacy-*` and other brands are unchanged; the V2 brand adds a
1185
+ `[data-brand=keboola]` override (`normal-case` + `tracking-normal`) that wins by selector specificity.
1186
+ No API changes.
1187
+
1188
+ - Ship `src/theme.css` in the published tarball. The `./theme` entrypoint
1189
+ (`@keboola/design/theme`, the portable CSS-first `@theme` that v4 apps import to
1190
+ generate their own themed utilities) pointed at `src/theme.css`, but that file was
1191
+ missing from the package `files` allow-list — so an external `@import
1192
+ '@keboola/design/theme'` resolved to nothing. Added `src/theme.css` to `files`
1193
+ alongside `src/styles.css`.
1194
+
1195
+ Also excluded `./theme` from `attw` in `build:check`, matching the sibling
1196
+ `./styles` CSS entrypoint: `arethetypeswrong` resolves a CSS export to neither type
1197
+ declarations nor JavaScript and reports it as a hard failure, which was red-lighting
1198
+ the publish pipeline's export-validation gate.
1199
+
1200
+ ## 7.0.0
1201
+
1202
+ ### Major Changes
1203
+
1204
+ - Migrate `@keboola/design` to Tailwind CSS v4 (CSS-first config via `@config`, scoped preflight via the v4 `@plugin` API, `tw-` → `tw:` class prefix).
1205
+
1206
+ **Breaking:** the raw stylesheet (`@keboola/design/styles`) now uses Tailwind v4 syntax and can only be processed by a v4 Tailwind pipeline. Consumers still on Tailwind 3 must either consume the pre-compiled `@keboola/design/styles/compiled` (if they run their own Tailwind pipeline) or migrate to Tailwind 4. Class names shipped by the design system move from the `tw-` prefix to `tw:`.
1207
+
1208
+ ### Minor Changes
1209
+
1210
+ - Add `ExportMenu` and open up `ZoomableSvg`'s toolbar
1211
+
1212
+ New `ExportMenu` block — a single icon-only download button that opens a menu of formats. Sized `icon-sm` to sit flush with neighbouring toolbar controls (copy, fullscreen) rather than widening the row with per-format labels.
1213
+
1214
+ `ZoomableSvg` and `MermaidDiagram` gain `renderActions`, a render prop for extra toolbar controls that receives the sanitized SVG so an exporter writes exactly what is on screen. This keeps the export behaviour — filenames, rasterization, error reporting — with the consumer, so `@keboola/design/mermaid` stays lazily loadable without download or menu dependencies of its own. Omit the prop and the toolbar is unchanged.
1215
+
1216
+ The mermaid subpath also re-exports `toSvgFileContent` and `svgToPngBlob` (2x PNG, painted on the themed `--color-background`) so a consumer can serialize or rasterize the rendered diagram itself.
1217
+
1218
+ `ZoomableSvg`'s toolbar picks up `focus-within` so keyboard users can see the controls they tab into — it was hover-only, leaving focus invisible.
1219
+
1220
+ - Add `allowCreate` to `MultiCombobox`, mirroring the single-value `Combobox`. When enabled, typing a query with no exact match surfaces a "Create …" option; selecting it adds the free-form value as a chip. Supports the optional `queryToCreateLabel` and `queryToCreateValue` accessors for custom labels and item shapes. This unblocks free-form multi-select entry (e.g. typing primary-key column names by hand).
1221
+
1222
+ ### Patch Changes
1223
+
1224
+ - Fix two `CollapsibleStepHeader` layout issues that made a wrapped header look inconsistent with a short one:
1225
+ - Keep the status icon at 16×16 when the label is long enough to overflow. It was a flex item without `flex-shrink: 0`, so `tw-size-4` acted as a shrinkable basis and the glyph squashed horizontally (to ~10px), rendering circles as ovals.
1226
+ - Keep the chevron next to the end of the label instead of pinned to the header's right edge. It was a sibling of the label block, so once the label wrapped and stretched to full width the chevron sat far from the text it belongs to. It now renders inline after the label, following the last word on the final line, bound to that word with a word joiner so it cannot orphan onto a line of its own.
1227
+
1228
+ - Fix the `DiffView` header (config-changes / version diff) under Tailwind v4: the sticky
1229
+ header showed a dark divider and a harsher drop-shadow instead of the original light-grey
1230
+ rule + subtle shadow. Two v4 breaking changes were behind it:
1231
+ - **Default border colour** changed from `gray-200` to `currentColor`, so the header cells'
1232
+ bare `border-b` (and the view's outer `border`) drew a dark line (the text colour). Pinned
1233
+ to `border-border`.
1234
+ - **Shadow scale renamed** (`shadow-sm` → `shadow-xs`), so the codemod's `shadow-sm` now
1235
+ renders the larger v3-`shadow`. Restored the subtle rule with `shadow-xs`.
1236
+
1237
+ Verified: header border `rgb(229,231,235)` and shadow `0 1px 2px rgb(0 0 0 / .05)` — identical to v3.
1238
+
1239
+ - Let a component schema suppress the `#`-prefix encryption hint under a JSON code editor field with
1240
+ `options.encryption_hint: false`. The hint was appended to **every** `format: editor` field rendered in
1241
+ `application/json` mode, with no way for the author to turn it off. It is accurate — the widget parses
1242
+ the JSON it stores, so a `#`-prefixed key inside such a field really is encrypted on save, whatever the
1243
+ field's declared type — but for a field whose JSON is not Keboola configuration (a vendor filter tree,
1244
+ a request payload) it advertises an action that hands the component ciphertext it cannot use. Default
1245
+ behaviour is unchanged; only an explicit opt-out hides the hint.
1246
+
1247
+ - Fix `Modal` / `AlertModal` enter/exit animation flying in from the top-left instead of popping
1248
+ from the centre under Tailwind v4. The dialog centres with `-translate-x-1/2` / `-translate-y-1/2`,
1249
+ which in v4 set the CSS **`translate` property**, while `tw-animate-css` animates the **`transform`**
1250
+ property. The `slide-in-from-left-1/2` / `slide-in-from-top-1/2` classes used to _re-pin_ the -50%
1251
+ centring during the animation under v3 (where centring and animation shared `transform`, so the
1252
+ animation overrode it). Under v4 they instead **compose** with the persistent `translate` centring,
1253
+ stacking -50% + -50% = -100% at the animation's start — the dialog flew in from the corner. Dropped
1254
+ those slide utilities; centring now persists on the `translate` property and the animation is
1255
+ transform-only (zoom + fade), so the dialog stays centred and just scales in. Legacy's intentional
1256
+ slide-down (`slide-in-from-top-[48%]`, top-anchored, nothing to double) is unchanged.
1257
+
1258
+ - Reset the `NavbarMenu` `<ul>` margin (`tw:m-0`). Browsers give `<ul>` a default block
1259
+ margin; a plain app's scoped preflight resets it, but under a host with legacy Bootstrap
1260
+ CSS layered above the preflight (kbc-ui), `ul { margin-bottom: 10px }` re-applied and
1261
+ inflated the menu's height, pushing the whole navbar row ~5px off-center. Resetting the
1262
+ margin explicitly (utilities beat the legacy layer) keeps the row vertically centered.
1263
+
1264
+ - Fix the `Skeleton` shimmer animation under Tailwind v4. The sweep keyframe animated the
1265
+ `transform` property, but v4's `-translate-x-*` utilities (the skeleton's `before:-translate-x-full`
1266
+ start position) set the CSS `translate` property, not `transform`. The two compose as separate
1267
+ properties, so the static `translate: -100%` pinned the shimmer highlight off-screen while the
1268
+ `transform` animation ran invisibly. The `shimmer` keyframe now animates `translate`, matching the
1269
+ start-position utility, so it overrides it and sweeps across as before.
1270
+
1271
+ - Fix two v4 regressions in `Truncated` and `ButtonGroup`:
1272
+ - `Truncated` clamped via a hand-rolled `[display:-webkit-box!important]` arbitrary property.
1273
+ Tailwind v4 does not support `!important` **inside** arbitrary-property brackets (v3 did),
1274
+ so that class produced no rule at all — the clamp silently stopped and long text wrapped
1275
+ instead of truncating. Switched to v4's native `line-clamp-1` / `line-clamp-2`, which emit
1276
+ the full `-webkit-box` recipe.
1277
+ - `ButtonGroup` merged its base classes with the caller's `className` via cva's raw
1278
+ concatenation (no tailwind-merge), so a passed `tw:hidden` and the base `tw:inline-flex`
1279
+ both landed on the element and CSS source order decided the winner. v4 flipped that order
1280
+ (`inline-flex` now sorts after `hidden`), leaving hover-reveal button groups permanently
1281
+ visible. It now composes with `cn` (tailwind-merge), so a caller's display utility wins.
1282
+
1283
+ - Fix v4 variant-order regressions where a descendant-subject variant (`[&_svg]`, `[&_*]`) was stacked
1284
+ _before_ a state variant describing the parent (`data-[state]`, `aria-pressed`, `hover`, `focus`,
1285
+ `active`, `disabled`). Tailwind v4 applies stacked variants left-to-right, so those compiled to
1286
+ `… svg[data-state=unchecked]` / `… svg:hover` — selectors that target the icon's own (never-present)
1287
+ state instead of the component's, leaving the rule dead or firing only when the icon itself is hovered.
1288
+ - `DropdownMenu.RadioItem` / checkbox item: the indicator dot/check is `forceMount`ed and hidden when
1289
+ unchecked via `[&_svg]:data-[state=unchecked]:hidden`. The dead selector meant the dot showed on
1290
+ **every** item, so all radios/checkboxes looked selected. Now `data-[state=unchecked]:[&_svg]:hidden`.
1291
+ - `Button` (invisible/outline/inline), `ButtonInline`, `Toggle`: icon hover/focus/active/disabled and
1292
+ `aria-pressed` recoloring now key off the button/toggle state again, not the `<svg>`.
1293
+
1294
+ Class strings only; no API, prop, or behavior contract change.
1295
+
1296
+ - `DropdownMenu.Item` now declares `text-inherit` when used with `asChild`, so items rendered as `<a>`
1297
+ follow the menu foreground instead of a host app's global anchor color. Under Tailwind v4's real
1298
+ cascade layers, the scoped preflight's `a { color: inherit }` reset (in `base`) loses to an app's
1299
+ element-level `a { color: … }` when that CSS sits in a higher layer — kbc-ui's `legacy` (Bootstrap)
1300
+ layer does. A utility class settles it regardless of layer order, and stays overridable via
1301
+ `className`. Plain (non-`asChild`) items are unchanged.
1302
+
1303
+ - Fix `PromptInput` (AI Kit) to render correctly in dark mode by swapping hardcoded colors for semantic tokens:
1304
+ - The inner surface fill (previously a hardcoded `inset ... white` shadow) now reads `rgb(var(--color-card))`, so the field is a raised dark surface in dark mode instead of a blinding white box. Light mode is unchanged (the `card` token is `#fff`).
1305
+ - The disabled fill/border now use `muted` / `border` tokens instead of `neutral-100` / `neutral-200`.
1306
+ - The textarea text now uses `foreground` / `muted-foreground` instead of `neutral-800` / `neutral-400`.
1307
+ - The slash-command dropdown now uses `popover` / `popover-foreground` / `border` instead of `bg-white` / `border-neutral-100`, and its helper text uses `muted-foreground`.
1308
+
1309
+ The decorative AI gradient border and the send-icon accent are left as-is — they read correctly in both schemes.
1310
+
1311
+ - Share CodeMirror theme StyleModules across editor instances. `useThemedExtension` now takes a `cacheKey` and memoizes in a module-level cache keyed by that key plus the brand / light-dark signal, so `CodeEditor`, `SqlEditor`, and `MergePane` no longer build a fresh theme per instance. Per-instance themes appended their rules to a document-level stylesheet on every mount, which invalidates style for the whole document — making N editors on one page cost O(N²) style recalculation.
1312
+
1313
+ - Fix the `Tabs` bottom border (the separator under the tab strip, and the active-tab
1314
+ underline) silently disappearing under Tailwind v4 in a consumer app.
1315
+
1316
+ The wrapper and trigger used `border-0` plus a single-side border (`border-b` on the
1317
+ wrapper, `border-b-[3px]` on the trigger) — an all-sides width reset plus a bottom
1318
+ border. Under v4, design's compiled utilities and the consumer app's own
1319
+ `tailwindcss/utilities.css` share one `utilities` cascade layer, so a plain `border-0`
1320
+ from the app (imported after design's compiled CSS) can win the bottom width on
1321
+ source order alone (same layer + specificity) and zero it — the border renders
1322
+ `border-bottom-width: 0`, invisible, even though `border-solid`/`border-border` still
1323
+ apply. This is the same class of layer-ordering regression as the grid/filter collapse.
1324
+
1325
+ Replaced `border-0` with directional resets (`border-t-0 border-x-0`) on both the tab
1326
+ list wrapper and the trigger, so only `border-b`/`border-b-[3px]` ever touches the
1327
+ bottom — nothing can zero it. Visually identical where it already worked.
1328
+
1329
+ Verified in a browser against the composed + lightningcss-minified production bundle:
1330
+ old class computed `border-bottom: 0px` under the two-block race; new class computes
1331
+ `1px solid rgb(229,231,235)` with the other three sides at `0`.
1332
+
1333
+ - Document why `useThemedExtension` fills its module-level cache during render. No functional change — the comment was lost while tightening the API and is the one part a reader cannot derive from the code, since a render-phase write to module state normally is a bug.
1334
+
1335
+ - Updated dependencies:
1336
+ - @keboola/tailwind-config@1.0.0
1337
+
1338
+ ## 6.5.0
1339
+
1340
+ ### Minor Changes
1341
+
1342
+ - `Breadcrumb`: slash (`/`) separators by default and a chevron-button `Breadcrumb.Dropdown` picker. `Breadcrumb.Separator` now renders a `/` instead of a chevron (pass `children` to override). The `Breadcrumb.Dropdown` trigger is now always a small borderless chevron button grouped tight next to the label — it lifts its background/colour on hover and while open, rather than wrapping the whole crumb in an outlined box. The label is a link only with `splitTrigger`; otherwise it's plain text. Only the chevron opens the panel.
1343
+
1344
+ - `CopyField`: the copy box's "Copy" hover tooltip now waits ~2s before appearing, so it no longer flashes the instant the pointer crosses the box (the "Copied" confirmation still shows immediately on click). New `hoverHint` prop on `CopyField.Root` (default `true`) — set it `false` to suppress the hover hint entirely, so the tooltip only appears as the "Copied" confirmation after a copy. `Tooltip` gains an optional `delayDuration` prop to tune the hover-open delay.
1345
+
1346
+ - Add an `action` slot to the form field connectors. It renders an interactive node at the trailing edge of the label row (opposite the label) — e.g. a "Forgot password?" link beside a Password label. It is a sibling of `<Label>`, so it is not part of the field's accessible name. Threaded through `TextField`, `NumberField`, `TextareaField`, `SelectField`, `PasswordField`, and `OtpInputField`.
1347
+
1348
+ Also pin `GoogleButton`/`MicrosoftButton` font weight to literal values so they stay brand-independent — the `tw-font-*` scale is now brand-overridable via `--font-weight-*`, which was leaking into these third-party marks.
1349
+
1350
+ - Add a form-level submit error to the `Form` block, plus an inline `Select` variant and disabled select options.
1351
+ - `setSubmitError(form, message)` and the `form.SubmitErrorAlert` component surface a failed submission that isn't tied to a single field (bad credentials, 5xx, network) as an error `Alert`, complementing the field-level `setServerFieldErrors`. Pair it with `canSubmitWhenInvalid: true` so the submit button stays retryable — any error entry otherwise marks the form invalid.
1352
+ - `SelectTrigger` gains an `inline` variant, selectable independently of the Argo theme override.
1353
+ - `SelectField` forwards a per-option `disabled` flag to `Select.Item`. The base `Select` already supported it; the form connector was dropping it, so a field couldn't offer visible-but-unselectable options.
1354
+
1355
+ - Add an optional `passwordToggleLabel` prop to `PasswordInput`. When set, it suffixes the reveal button's accessible name — `Show password: <label>` / `Hide password: <label>` — so multiple password fields on one form get distinct, screen-reader-friendly labels. Omitting it keeps the original `Show password` / `Hide password` name, so existing usage is unchanged.
1356
+
1357
+ - `PlanApproval` now offers a full-screen view. When a plan body is present, a maximize control in the card header opens the plan in a full-screen overlay so long plans can be read without the cramped inline scroll area. The full-screen view reuses the same plan body and the Approve / Request Changes / Dismiss actions, and closes on Esc or the minimize control.
1358
+
1359
+ - Remove the `Tagger` block (free-form chip/tag entry). It diverged from the
1360
+ current design language and had no product usage; the `Tagger` export is gone
1361
+ from the package root. There is no drop-in replacement — for creatable/free-form
1362
+ selection use `Combobox` with `allowCreate`, or `MultiCombobox` for the
1363
+ enumerated multi-select case. It can be reimplemented as a block if the need
1364
+ returns.
1365
+
1366
+ - Add `Table.SortHeader` — a reusable sortable column-header label (border-less
1367
+ button + direction chevron) for use inside `Table.Head`, plus the
1368
+ `tableAriaSort` helper and `TableSortDirection` type. Works with a manual
1369
+ sort state or `@tanstack/react-table`'s sorting model.
1370
+
1371
+ ### Patch Changes
1372
+
1373
+ - Fix `PlanApproval` and `ToolCallApproval` footer action buttons wrapping to a second line in a narrow container. The action row is now horizontally scrollable with non-shrinking, no-wrap buttons.
1374
+
1375
+ - `CodeEditor`: only attach the JSON parse linter when the editor is editable. Read-only JSON editors no longer run `jsonParseLinter()` or show error underlines — the user can't act on them, so the lint work and markers were wasted overhead/noise (e.g. non-strict JSON rendered read-only in Kai chat).
1376
+
1377
+ - Move `FormGroup`, `Command`, and `MarkdownEditor` from `components/` (Layer 0) into `blocks/`
1378
+ (Layer 1). Relocating `TextInput` into `blocks/` left these three Layer-0 composites importing a
1379
+ Layer-1 block, violating the Layer-0/Layer-1 directory contract; they are themselves compositions
1380
+ (labelled field group, cmdk command palette, Lexical markdown editor), so `blocks/` is their correct
1381
+ home and they may import blocks freely. Public entry points are unchanged — `FormGroup` and
1382
+ `Command` still export from the `@keboola/design` barrel and `MarkdownEditor` still ships from the
1383
+ `@keboola/design/markdown-editor` subpath — so this is an internal relocation with no API change.
1384
+
1385
+ - `DateInput` moved from a Layer-0 `Input`-based component into a Layer-1 block. It
1386
+ now composes `InputGroup` directly (dropping the dependency on the low-level
1387
+ `Input` component) and renders the calendar-picker trigger and clear button as
1388
+ `InputGroup.Button`, mirroring `TimeInput`. It inherits the `InputGroup` chrome
1389
+ props (`variant`, `focusVariant`, `sizing`, `state`) and the underlying
1390
+ `InputGroup.Input` props (e.g. `aria-invalid`). The public import path
1391
+ (`@keboola/design` → `DateInput`) is unchanged. The `Input`-only slots (`prefix`,
1392
+ `leftSuffix`, `rightSuffix`, `rightSuffixVariant`, `testId`, `onClear`) are no
1393
+ longer accepted.
1394
+
1395
+ - `DateRangeInput` moved from a Layer-0 `Input`-based component into a Layer-1
1396
+ block. It now composes `InputGroup` directly (dropping the dependency on the
1397
+ low-level `Input` component) and renders the calendar-picker trigger and clear
1398
+ button as `InputGroup.Button`, mirroring `TimeInput`. It inherits the
1399
+ `InputGroup` chrome props (`variant`, `focusVariant`, `sizing`, `state`) and the
1400
+ underlying `InputGroup.Input` props (e.g. `aria-invalid`). The public import path
1401
+ (`@keboola/design` → `DateRangeInput`) is unchanged. The `Input`-only slots
1402
+ (`prefix`, `leftSuffix`, `rightSuffix`, `rightSuffixVariant`, `testId`, `onClear`)
1403
+ are no longer accepted.
1404
+
1405
+ - Optically center the control in `FieldBox` (Switch / Checkbox / RadioGroupItem) when a description is present. The control was geometrically centered on the title's line box, but text sits low within its line box, so the control read as too high. Nudge the control down 2px only in the has-description (multi-line) case; single-line controls are unchanged.
1406
+
1407
+ - Replace the deprecated React `ElementRef` type with React 19 idioms throughout the package. Where the props base already carries a correctly-typed `ref` (`ComponentProps<T>`), the redundant `& { ref?: Ref<ElementRef<X>> }` clause is dropped; `ComponentPropsWithoutRef<T> & { ref?: … }` collapses to `ComponentProps<T>`; and remaining ref-type usages are renamed to `ComponentRef`. Types-only change — no runtime or public-API behavior change.
1408
+
1409
+ - Fix the `Keboola` icon clipping the top of its own octopus mark. The artwork
1410
+ fills the viewBox edge-to-edge vertically, so under the SVG default
1411
+ `overflow: hidden` sub-pixel rounding at certain viewport sizes / zoom levels
1412
+ shaved the head flat. Adding `overflow="visible"` lets the rounded top edge
1413
+ paint fully wherever the mark renders (navbar, loading states, modals, …).
1414
+
1415
+ - Render icons inside Mermaid diagrams. `MermaidDiagram` now registers a `lucide`
1416
+ Iconify pack with mermaid, so the flowchart icon shape
1417
+ (`Node@{ icon: "lucide:database" }`) and `architecture-beta` service icons render
1418
+ instead of showing nothing — the regression left over from the FontAwesome →
1419
+ Lucide migration. Diagrams that use icon syntax skip the icon-less synchronous
1420
+ fast renderer and take the async mermaid path so the pack can load.
1421
+
1422
+ - Fix the Modal-form recipe in the consumer docs: `Modal.Header` renders its title from `children`, not from a `title` prop. Since the component's props extend `ComponentProps<'div'>`, `title="…"` type-checks as the native HTML attribute and renders no visible title.
1423
+
1424
+ - RjsfSchemaEditor: array multi-selects and checkbox arrays show item-level enum_titles as option labels (forwarded via the items uiSchema enumNames channel) instead of raw enum values
1425
+
1426
+ - `TextInput` moved from a Layer-0 `Input`-based component into a Layer-1 block. It
1427
+ now composes `InputGroup` directly, collapsing the previous split between the
1428
+ private low-level `Input` primitive and its string-typed `TextInput` wrapper into a
1429
+ single component (the low-level `Input`, which was never exported, has been
1430
+ removed). The public import path (`@keboola/design` → `TextInput`) and the full
1431
+ public API are unchanged — string `value` / `defaultValue`, `onValueChange` /
1432
+ `onValueBlur`, the `prefix` / `leftSuffix` / `rightSuffix` / `rightSuffixVariant`
1433
+ slots, `allowClear` / `onClear`, `testId`, and the `InputGroup` chrome props
1434
+ (`variant`, `focusVariant`, `sizing`, `state`) — so this is a drop-in relocation
1435
+ with no behavior change.
1436
+
1437
+ - `Timeline`: constrain the content column with `minmax(0,1fr)` instead of `1fr` (which defaults to `minmax(auto,1fr)`). A wide, non-wrapping item — e.g. a long monospace command — no longer blows the content track past the container; it wraps or scrolls within its own column instead.
1438
+
1439
+ - Extract a shared internal `useControllableState` hook (Radix-style: `{ prop, defaultProp, onChange }`
1440
+ → `[value, setValue]` with a `SetStateAction` functional updater) and adopt it across the
1441
+ InputGroup-based inputs — `PasswordInput`, `TimeInput`, `ColorInput`, `DateInput`,
1442
+ `DateRangeInput`, `TextInput`, and `Textarea`. This deduplicates the `isControlled` /
1443
+ internal-state / `setValue` boilerplate and unifies controlled/uncontrolled handling. The hook is
1444
+ internal (not exported from the package); no public API change.
1445
+
1446
+ - Portal the `ZoomableSvg` fullscreen overlay to `document.body` so it escapes ancestor stacking contexts (e.g. a `z-50` sidebar) and its `z-[1060]` reliably covers the whole viewport. Previously, when a diagram was rendered inside a positioned/z-indexed container such as the chat sidebar, the fullscreen view was trapped in that stacking context and the app navigation bled on top with a broken layout.
1447
+
1448
+ ## 6.4.0
1449
+
1450
+ ### Minor Changes
1451
+
1452
+ - Add a `MicrosoftButton` block — a "Sign in with Microsoft" button following
1453
+ Microsoft's official brand guidelines (fixed vendor colors that do not re-skin
1454
+ under a customer brand). Supports the `label` wordings (`signin` / `signup` /
1455
+ `continue`). The light treatment is the default and the dark treatment
1456
+ auto-applies under the design-system dark scheme
1457
+ (`html[data-theme=dark]`), so it follows `ThemeProvider` with no prop. Also
1458
+ exports a `LucideIcon`-compatible `Microsoft` four-square mark from
1459
+ `@keboola/design/icons`.
1460
+
1461
+ ### Patch Changes
1462
+
1463
+ - Fix read-only inputs being illegible in dark mode under the argo chrome. The
1464
+ base `InputGroup` layer pins the read-only surface to a fixed light grey
1465
+ (`neutral-150`) — correct for the legacy light chrome, but it collided with
1466
+ argo's themed `foreground` text, rendering light-on-light (invisible) under a
1467
+ dark brand. The argo variant now re-pins the read-only background and border to
1468
+ the same themed `muted` / `input` tokens the editable argo input already uses,
1469
+ so a read-only `TextInput` (e.g. a copyable URL field) stays AA-legible in every
1470
+ color scheme. Legacy chrome is unchanged.
1471
+
1472
+ - Improve `Badge` `gray-inverse` legibility in dark mode. The dark chip
1473
+ background was `neutral-800` (#222529), which sits only ~15 levels above a
1474
+ deep app background and read as bare floating text rather than a chip —
1475
+ especially on darker brand themes. Bump the dark chip background (and its
1476
+ underline decoration) to `neutral-700` (#33373d) so the neutral tag reads
1477
+ as a distinct pill; the text stays `neutral-300` (≈5.1:1 on the new
1478
+ background). Light mode is unchanged.
1479
+
1480
+ - Fix `Popover.Content` (and everything built on it — `DatePicker`,
1481
+ `DateRangePicker`, `TimePicker`, `Popconfirm`, `Tagger`, `BreadcrumbDropdown`)
1482
+ being unclickable inside a `Modal`. An open Radix Dialog locks
1483
+ `pointer-events: none` on `<body>`; the popover content is portalled to `<body>`
1484
+ as a sibling of the dialog and inherited the lock, so clicks fell through to the
1485
+ content behind it (e.g. picking a calendar day did nothing). The content now
1486
+ sets `pointer-events: auto`; dismiss-on-outside-click is unaffected.
1487
+
1488
+ ## 6.3.0
1489
+
1490
+ ### Minor Changes
1491
+
1492
+ - Add a `GoogleButton` block — a "Sign in with Google" button following Google's
1493
+ official Sign-In branding guidelines (fixed vendor colors that do not re-skin
1494
+ under a customer brand). Props: `variant` (`default` / `neutral` light-scheme
1495
+ treatments), `shape` (`rectangular` / `circle`), `size` (`default` / `icon`,
1496
+ icon-only), and `label` wordings (`signin` / `signup` / `continue` /
1497
+ `signin-short`). The dark treatment auto-applies under the design-system dark
1498
+ scheme (`html[data-theme=dark]`), so it follows `ThemeProvider` with no prop.
1499
+ Also exports a `LucideIcon`-compatible `Google` mark from
1500
+ `@keboola/design/icons`.
1501
+
1502
+ ## 6.2.1
1503
+
1504
+ ### Patch Changes
1505
+
1506
+ - Improve DS contrast. The `Table` header now uses a distinct `table-header`
1507
+ chrome token instead of `muted` (which sat ~1.05:1 against the surfaces, making
1508
+ the header row near-invisible), and the gray-inverse `Badge` text meets WCAG-AA.
1509
+ Adds a non-gating `audit:contrast` script + `CONTRAST-AUDIT.md` reporting sub-AA
1510
+ token pairings in the default palette as a backlog.
1511
+ - Updated dependencies:
1512
+ - @keboola/tailwind-config@0.9.0
1513
+ - @keboola/brand-registry@1.7.0
1514
+
1515
+ ## 6.2.0
1516
+
1517
+ ### Minor Changes
1518
+
1519
+ - Add a brand-driven type scale to `typography`. `theme.typography.typeScale` now carries `display` / `heading` / `body` steps, each bundling `size` / `weight` / `leading` (line-height) / `tracking` (letter-spacing) — all optional. `ThemeProvider` emits them as `--type-<step>-<axis>` CSS vars, and `@keboola/tailwind-config` reads them through new `tw-text-display` / `tw-text-heading` / `tw-text-body` utilities (one utility sets size + line-height + weight + letter-spacing together). This lets a brand express a real display/body hierarchy — pair with `typography.fontDisplay` (e.g. a serif display face over a sans body) for an editorial/executive feel. The utilities' inline fallbacks equal the stylesheet defaults, so brands that omit `typeScale` render unchanged.
1520
+
1521
+ - `BrandEditor`: expose the full `Brand` theme schema. Adds controls for the categorical chart palette (`chart1`–`chart5`, per light/dark), the rest of `typography` (display + monospace faces, the chrome font-weight scale, and chrome letter-spacing), and the advanced token groups — elevation (`shadows`), density (`spacing`), motion, and the brand `gradient`. Each writes only the tokens a brand sets, so unset slots still inherit the `@keboola/design/styles` defaults.
1522
+
1523
+ - Add `BrandEditor` — a rich, controlled brand-editing block (colors/scheme/sidebar/a11y/radii/fonts/logo) for live theming.
1524
+
1525
+ - Add signature block templates — opinionated higher-order compositions for building product-grade dashboard pages. `DashboardHero` (eyebrow / title / description / actions + optional aside) for the top-of-page hero, `StatCard` + `StatCardGrid` for a KPI/metric strip (with a semantic `success`/`destructive`-tinted `StatCard.Trend` delta), and `ChartPanel` — a `Card` shell (title, description, `toolbar`, `footer`) that hosts a `@keboola/design/charts` chart as children. All three read semantic tokens only, so they re-skin with the active brand. See the `Blocks/DashboardHero`, `Blocks/StatCard`, and `Blocks/ChartPanel` stories (the composed "Signature dashboard" story shows them assembled together).
1526
+
1527
+ ### Patch Changes
1528
+
1529
+ - Make decorative `Badge` "inverse" tints legible in dark mode. The categorical
1530
+ inverse tints are fixed light primitives (`<color>-100` fill), so on dark
1531
+ surfaces they rendered as bright chips (a light-bleed glare). Each inverse
1532
+ colour now carries a `[data-theme='dark']` tweak inline in `badgeColors` —
1533
+ a deep same-hue fill (`<color>-900` / `neutral-800`) + light same-hue text
1534
+ (`<color>-200` / `neutral-300`) from the existing palette, applying the fill
1535
+ as both background and underline decoration so the underline stays hidden.
1536
+ Light rendering is unchanged; no new tokens or global CSS. Tints stay fixed
1537
+ across brands (categorical, per the Layer-0 contract) — scheme-aware only.
1538
+
1539
+ - Fix dark-mode light-bleed in chrome components that hardcoded light colors instead of semantic tokens. `CollapsiblePanel` surfaces now use `tw-bg-card`, the `Search` clear button and the `FullScreenModal` "go back" link now use `tw-text-muted-foreground`/`tw-text-foreground`, and the `MarkdownEditor` floating toolbar, its buttons, block/alignment dropdowns, link panel, and divider now use `tw-bg-popover`/`tw-bg-accent`/`tw-text-muted-foreground`/`tw-border-border`. These surfaces now flip correctly under `[data-theme='dark']`.
1540
+
1541
+ - RjsfSchemaEditor: DynamicSelectWidget keeps showing the stored value while the watched enumSource data is not loaded yet (e.g. before a sync action populates it)
1542
+
1543
+ - Updated dependencies:
1544
+ - @keboola/brand-registry@1.6.0
1545
+ - @keboola/tailwind-config@0.8.0
1546
+
1547
+ ## 6.1.0
1548
+
1549
+ ### Minor Changes
1550
+
1551
+ - Add the `Copy.*` compound block — a readonly value/command box with inline click-to-copy. Supports a monospace command shape, masked values (`Copy.Hidden` masks with asterisks, plus an optional `Copy.Reveal` eye toggle), muted prefix/suffix addons, a label row (`Copy.Label` with `leading`/`trailing` slots), a helper `Copy.Footer`, and multi-line boxes (`minLines`/`maxLines`). Clicking anywhere in `Copy.Box` copies the real value with Copy → Copied feedback, while text stays drag-selectable.
1552
+
1553
+ ### Patch Changes
1554
+
1555
+ - Fix accessibility gaps found by axe: `ClipboardButton` now defaults to an accessible `aria-label` ("Copy", or the string `tooltip`) when rendered icon-only with no text children, so the diagram-copy control inside `MermaidDiagram`/`ZoomableSvg` gets discernible text too. `CardTitle` gains an optional `as` prop (`'h1'`–`'h6'`, defaulting to `'h3'`) so a `Card.Title` can match its position in the heading hierarchy instead of always emitting an `h3`. `PromptInputSubmit`'s icon-only submit button now gets a status-appropriate `aria-label` ("Send message" / "Stop" / "Continue" / "Sending…") instead of relying solely on a visual tooltip.
1556
+
1557
+ - Guarantee AA-safe interactive brand tokens instead of leaving contrast to the consumer.
1558
+ - `@keboola/brand-registry` exports `checkInteractiveContrast(brand)` (plus `contrastRatio`, `AA_CONTRAST_MIN`, the `ContrastWarning` type, and a `brands` array): a non-fatal check that reports interactive foreground↔background pairings — button/link labels on branded fills, and `primary`-on-`background` — that fall below WCAG-AA (4.5:1), across both the light and dark palettes.
1559
+ - `@keboola/validate-ui`'s visual-brand axis now asserts the alternate brand's interactive-token contrast (non-fatal `moderate` findings), not just that chrome re-skins.
1560
+ - The `keboola` V2 brand's light `primary` is AA-tuned to blue-600 (`37 99 235`, 5.17:1 white-on-fill), kept in lock-step with `@keboola/design/styles.css`; the dark palette and both `keboola-legacy` brands are unchanged.
1561
+
1562
+ - Updated dependencies:
1563
+ - @keboola/brand-registry@1.5.0
1564
+
1565
+ ## 6.0.0
1566
+
1567
+ ### Major Changes
1568
+
1569
+ - Accessible form primitives. Standardizes field-level error semantics and error chrome across the form connectors.
1570
+ - **`FormGroup.Error`** — a `role="alert"` field-error primitive (accepts `children` or an `errors` array). Adopt it wherever `HelpBlock` was misused for errors (wrong ARIA).
1571
+ - **Error chrome via `aria-invalid`** — `InputGroup`, `SelectTrigger`, and `OtpInput` surface error styling driven solely by `aria-invalid` on the control, no separate `state` prop.
1572
+ - **Shared `FieldItem`** — one render-prop frame for the form connectors that owns the label, required marker, tooltip, help, error, and `aria-describedby` wiring. The required marker is gated on the `required` flag (asterisk only when required, `(optional)` only when not).
1573
+ - **`Label.Optional` / `Label.Required`** are decorative-only (both `aria-hidden`); requiredness is conveyed by `aria-required` on the control.
1574
+
1575
+ **Breaking:**
1576
+ - Renamed `InputOtp` → `OtpInput` (and its parts) and the `TextAreaField` connector → `TextareaField`. Update imports and `field.TextAreaField` usages accordingly.
1577
+ - Removed the public `useInputOtpContext` hook — read `input-otp`'s `OTPInputContext` directly for custom slot rendering.
1578
+ - `OtpInput` no longer takes a `state` prop; drive its error state with `aria-invalid`.
1579
+ - `field.SwitchField` / `field.CheckboxField` no longer accept `label` / `help` props — they own their label and description via `children`.
1580
+
1581
+ **New:** `field.OtpInputField` connector (fixed-length verification code); `FormGroup.Error`; `aria-invalid` error chrome on `InputGroup` / `Select` / `OtpInput`.
1582
+
1583
+ ### Minor Changes
1584
+
1585
+ - Wire the brand `radii` and body-font tokens into their Tailwind utilities. `tw-rounded-sm/md/lg` now read `--radius-sm/md/lg` and `tw-font-sans` now reads `--font-sans` (both emitted by `ThemeProvider` from a brand's `theme.radii` / `theme.typography.fontFamily`), each with an inline fallback equal to the previous value so unset brands are unaffected. Until now these vars were injected but no utility consumed them, so a brand's radii and body font only reached the handful of components using `var(--radius-*)` directly and elements inheriting `html`'s font — a brand override now re-skins corner radii and typography across all chrome. Mirrors the existing `--shadow-*` / `--space-*` / `--font-display` wiring.
1586
+
1587
+ One behavioral change for consumers importing `@keboola/design/styles`: `tw-rounded-sm` shifts `0.125rem` → `0.25rem`, because the stylesheet's `:root` already declares `--radius-sm: 0.25rem` (which the new `var(--radius-sm, 0.125rem)` resolves to). `tw-rounded-md/lg` and `tw-font-sans` are unchanged, since their `:root`/brand values already equal the previous stock values.
1588
+
1589
+ ### Patch Changes
1590
+
1591
+ - Make chrome tracking + font weights on `Button` and `Badge` token-driven so a brand can re-skin them via `@keboola/brand-registry`. The hardcoded `tw-tracking-[1px]` becomes the `tw-tracking-chrome` token (reading `--letter-spacing-chrome`), and the `fontWeight` scale reads the brand's `--font-weight-*` overrides. `ThemeProvider` emits the new variables. Casing stays a plain `tw-uppercase`. Defaults are unchanged, so brands that omit the slots render identically to today.
1592
+
1593
+ `@keboola/tailwind-config` gains the `tracking-chrome` utility and a var-driven `fontWeight` scale — both falling back to today's values.
1594
+
1595
+ - Updated dependencies:
1596
+ - @keboola/tailwind-config@0.7.0
1597
+ - @keboola/brand-registry@1.4.0
1598
+
1599
+ ## 5.1.0
1600
+
1601
+ ### Minor Changes
1602
+
1603
+ - `Breadcrumb.Dropdown.Group`: non-empty `collapsible` groups now animate their expand / collapse
1604
+ (a smooth height transition) instead of toggling instantly. The items stay mounted while collapsed
1605
+ (so the filter registry keeps tracking them) but are taken out of the accessibility tree and tab
1606
+ order, and the animation is disabled under `prefers-reduced-motion`.
1607
+
1608
+ - `Breadcrumb.Dropdown.Group`: a `collapsible` group with no items now renders a non-interactive,
1609
+ greyed header (a `<span aria-disabled>` with a faded, static chevron) instead of a toggle button
1610
+ that opens onto nothing. `Breadcrumb.Dropdown`'s `splitTrigger` chevron also gets a larger,
1611
+ more generous hit target (colour-only hover, no background box).
1612
+
1613
+ ### Patch Changes
1614
+
1615
+ - Make the `Skeleton` loading placeholder visible in light mode. The base used
1616
+ `tw-bg-muted`, which sits ~1.05–1.10:1 against white cards / off-white
1617
+ background — effectively invisible — and the `white/40` shimmer was likewise
1618
+ invisible over that near-white base. The base now uses a dedicated,
1619
+ scheme-aware `tw-bg-skeleton` token (`--color-skeleton`): a touch darker than
1620
+ `muted` in light and lighter than card/background in dark, so the block is
1621
+ perceptible and the shimmer reads in both schemes.
1622
+ - Updated dependencies:
1623
+ - @keboola/tailwind-config@0.6.0
1624
+
1625
+ ## 5.0.0
1626
+
1627
+ ### Major Changes
1628
+
1629
+ - Upgrade react-resizable-panels to v4. Breaking changes to the Resizable wrappers:
1630
+ `ResizablePanelGroup` takes `orientation` (was `direction`); `ResizablePanel` exposes its
1631
+ imperative handle via the `panelRef` prop typed `PanelImperativeHandle` (was `ref` /
1632
+ `ImperativePanelHandle`); and layout persistence uses the re-exported `useDefaultLayout`
1633
+ hook (was the `autoSaveId` prop).
1634
+
1635
+ ### Minor Changes
1636
+
1637
+ - Add advanced brand theme tokens beyond colour/radii. `brandSchema.theme` now accepts optional `shadows` (elevation scale `sm`/`md`/`lg`/`lift`), `spacing` (`gutter`/`inset` density), `motion` (`duration`/`easing`), `gradient` (`brand`), and `typography.fontDisplay` / `typography.fontMono` for display/mono font pairing. `ThemeProvider` emits each as a CSS variable only when the brand sets it (`--shadow-*`, `--font-display`, `--font-mono`, `--space-*`, `--motion-*`, `--gradient-brand`), so unset brands are unaffected. Tailwind utilities read these vars with inline fallbacks equal to the current defaults — `tw-shadow-sm/md/lg` are unchanged, plus new `tw-shadow-lift`, `tw-font-display`, `tw-bg-brand`, `tw-duration-brand` / `tw-ease-brand`, and `tw-p-inset` / `tw-gap-gutter`.
1638
+
1639
+ ### Patch Changes
1640
+
1641
+ - Make Card / Search elevation dark-aware. Both components hardcoded a pale
1642
+ blue-gray shadow (`0 2px 6px rgba(100 135 153 / 0.13)`) tuned for white
1643
+ backgrounds, which reads as a light halo on dark surfaces. The shadow now comes
1644
+ from a new `tw-shadow-raised` utility backed by the `--shadow-raised` token,
1645
+ which flips to a black shadow under `[data-theme='dark']`. Light rendering is
1646
+ unchanged (the token default equals the previous value).
1647
+ - Updated dependencies:
1648
+ - @keboola/brand-registry@1.3.0
1649
+ - @keboola/tailwind-config@0.5.0
1650
+
1651
+ ## 4.5.0
1652
+
1653
+ ### Minor Changes
1654
+
1655
+ - Add `Breadcrumb.Dropdown` — a breadcrumb segment that opens a standardized switcher panel (typed `Group`/`Item` sub-parts, built-in filter on by default, collapsible groups, `small`/`medium`/`large` size presets with viewport-collision clamping, split trigger keeping the crumb label a working link).
1656
+
1657
+ - Redesign `Modal` — grid-based `Modal.Header` with a prop-driven API, and split the header icon into `Modal.Icon` (status glyph) and `Modal.Image` (remote logo).
1658
+
1659
+ **Breaking — `Modal.Header`:** the title is now the `children`; `icon` / `description` / `actions` are props. Migrate `<Modal.Header title="X"><Modal.Icon … /></Modal.Header>` → `<Modal.Header icon={<Modal.Icon … />}>X</Modal.Header>`.
1660
+
1661
+ **Breaking — `Modal.Icon`:** the `iconUrl` / `iconAlt` image variant is removed — use the new `Modal.Image` (`src` / `alt`) for remote/logo images.
1662
+ - `Modal.Icon` / `Modal.Image` each render as a single element, styled per design language via CSS.
1663
+ - New `actions` slot on `Modal.Header` (top-right controls, before the close icon).
1664
+ - `Modal.Header` descriptions: `description` renders a visible subtitle (and fills `aria-describedby`); the new `aria-description` prop provides a screen-reader-only accessible description — it fills `aria-describedby` (via a visually-hidden `Dialog.Description`) without rendering a visible subtitle, for modals that need an accessible description but no visible one.
1665
+ - argo: viewport-centered position + pop animation, unified 16px padding, `rounded-3xl`; per-section horizontal padding so the body runs full-bleed and its scrollbar sits in the gutter; the legacy header separator is an inset `Separator` (respects padding). Vertical edge padding lives on `Modal.Content`, so an omitted header or footer still spaces correctly.
1666
+ - `Modal.Footer` layout is fully caller-owned.
1667
+ - `AlertModal` argo aligned with `Modal` (`rounded-3xl` + `p-4`).
1668
+ - `DropdownMenu` content now layers above `Modal` / `Sheet` (z-index `1055`).
1669
+
1670
+ ## 4.4.0
1671
+
1672
+ ### Minor Changes
1673
+
1674
+ - feat(design): make status colours themeable with AA-legible defaults (argo only)
1675
+
1676
+ Under the argo design language (the V2 Keboola brand and any customer brand), Alert, HelpBlock, ErrorState, and Stepper now read the brand-overridable `success` / `warning` / `info` / `destructive` tokens (a tint background plus the full-strength, role-coloured icon/text) instead of fixed `green-*` / `warning-*` / `error-*` / `blue-*` scales, so a customer brand recolours every status surface at once. The argo default token values are retuned in light mode to meet WCAG AA (≥4.5:1) as the icon/text on their own tint — fixing existing contrast failures (e.g. Alert success/warning/info icons were ~2:1). Dark-mode values already passed and are unchanged.
1677
+
1678
+ The legacy Keboola brands (`keboola-legacy-topnav` and `keboola-legacy-sidebar`) are unchanged: Alert/ErrorState keep their fixed categorical scales via a `data-argo` gate, and the legacy brand retains its original status token hues — so legacy chrome looks exactly as before.
1679
+
1680
+ - Add per-tool-call AIKit primitives to the root barrel: `ToolCallApproval` (an approve/reject frame that generalizes `PlanApproval` to a single tool call — `toolName`, `title`, `status: 'pending' | 'approved' | 'rejected' | 'expired'`, `onApprove` / `onReject`, `children` for app-rendered detail, `actions` override, opt-in `Enter`/`Escape` shortcuts) and `ToolCallChip` (a presentational icon + label + trailing-action chip with a `processing` / `done` / `error` state and no URL knowledge — wrap in a `Link` or pass `onClick`). No SDK, permissions, or deep-link coupling. `PlanApproval` stays as the whole-plan variant.
1681
+
1682
+ - Add a categorical `amber` (and `amber-inverse`) caution color to `Badge`. Previously the only warning-adjacent color was `orange` (the `warning-500` orange ramp), collapsing "warning" and "caution" onto one hue. `amber` draws from the same `warning` ramp — a paler `warning-300` (`#ffd699`) fill with dark `warning-800` text — so it reads as a soft caution chip that is tonally on-brand with `orange` yet clearly distinct from it. `amber-inverse` follows the same-hue `-inverse` convention (`warning-100` fill, `warning-700` text). Badge colors stay categorical, not brand tokens.
1683
+
1684
+ - Add `<BrandLogo>` — renders the active brand's logo (from `@keboola/brand-registry`) read via the nearest `<ThemeProvider>`. Picks the right `mark` / `wordmark` and light / dark variant, and renders inline SVG (a `currentColor` mark inherits text colour) or an `<img>` depending on whether the source is markup or a URL / `data:` URI; renders nothing when the brand ships no matching logo. `Sidebar.Header` and `Navbar.Header` now render `<BrandLogo>` by default when given no children (collapsed sidebar → mark, otherwise wordmark), so the app shell is branded for free — call sites that pass their own header content are unaffected.
1685
+
1686
+ - Consolidate theming into a single gated `ThemeProvider` and decouple the argo design language from brand internals.
1687
+ - **design**: `ThemeProvider` now resolves the active brand, injects only the brand's overrides for the resolved color scheme (missing slots — including dark palettes — fall through to the stylesheet defaults), and exposes the argo flag via a dedicated `ArgoDesignContext` + `useArgoDesign` hook instead of `Theme.__isArgo`. Drops the `brands` / `getBrand` re-exports.
1688
+ - **brand-registry**: group brand tokens under `Brand.theme` and make color slots fully optional, so a brand can override as little as a single token.
1689
+
1690
+ - Add a themed charts / data-viz wrapper at the new `@keboola/design/charts` subpath (recharts under the hood — kept out of the root barrel so it stays tree-shakeable). Ships `LineChart`, `BarChart`, `AreaChart`, `PieChart`, a `ChartContainer` responsive frame, and a semantic-token `ChartTooltip`. Series colors resolve from the `--color-chart-1..5` palette only (no inline hex / primitive scales), so charts follow the active color scheme (light/dark) and the active brand's `colors.chart` when it defines one (else the shared palette).
1691
+
1692
+ - Add a read-only `Markdown` component to AIKit. Renders GFM (tables, lists, task lists, code, links) with design-token `prose` typography and safe defaults (external links get `target="_blank"` + `rel="noopener noreferrer"`). GFM tables render through the design `Table`, fenced ` ```mermaid ` blocks render through the existing `MermaidDiagram`, and other fenced code blocks get a hover copy button (mount a `TooltipProvider` at the app root). Pair it with `MessageBubble` to render assistant message bodies — previously every consumer had to bring its own markdown renderer.
1693
+
1694
+ - Add `@keboola/design/rjsf` subpath export — the RJSF (react-jsonschema-form) schema editor migrated from kbc-ui. `RjsfSchemaEditor` ships with design-system widgets/fields/templates, a plain-JS public API, and a host-injection seam (`widgets`/`fields` props merged over the built-in registry, `renderMarkdown`, `hasProtectedDefaultBranch`). Select and DynamicSelect widgets are rebuilt on `Combobox`; CodeEditorWidget on the design `CodeEditor` (CodeMirror 6).
1695
+
1696
+ - Add the `SpotlightBackdrop` primitive and the `useTargetRect` hook, plus a "Guided tour (recipe)" Storybook story.
1697
+
1698
+ `SpotlightBackdrop` is a dimmed full-viewport layer with an SVG-mask cut-out around a target rect (the spotlight highlight); `rect={null}` dims the whole viewport with no cut-out. The dim tone uses the semantic `foreground` token, so it re-skins with the active brand. `useTargetRect(target, enabled)` resolves a CSS selector or React ref to a live viewport rect and keeps it in sync on scroll/resize (coalesced to one measure per animation frame), re-resolving lazily-mounted targets.
1699
+
1700
+ Tour / coachmark orchestration (step list, next/prev, "seen" persistence) is intentionally **app-owned**, in line with the shadcn/Radix "primitives, not patterns" stance — the design system ships the primitives, and apps compose the flow from `SpotlightBackdrop` + `Popover` + `useTargetRect`. The canonical composition is the "Guided tour (recipe)" story.
1701
+
1702
+ ### Patch Changes
1703
+
1704
+ - Refresh `COMPONENT-API.md` to reflect the new optional `logo` field on the re-exported `Brand` type — it widens the inline type of `ThemeProvider`'s `brand` / `setBrand` props. Generated-doc refresh only; no runtime change.
1705
+
1706
+ - Update the CodeMirror suite (`@codemirror/view` 6.43.6, `@codemirror/state` 6.7.1, `@uiw/react-codemirror` 4.25.10) to pick up the upstream tile-tree corruption fixes causing disappearing lines and wrong selections in the editor
1707
+
1708
+ - Update react-day-picker to v10. No API or CSS pipeline change — v10 removes only APIs already deprecated in v9, none of which were used. (Upstream now also publishes the renamed `@daypicker/react` wrapper, but the original package name remains the real implementation.)
1709
+
1710
+ - Rename the categorical editor / diff / badge / diagram colors off the legacy `primary`/`secondary` ramp aliases onto the fixed `green`/`blue` palettes (UT-4361 categorical tail). Badge color variants, CodeEditor chrome (selection, search, syntax, active-query, merge highlights), DiffView "added", MergeEditor decorations, and MermaidDiagram theme are categorical — their color encodes fixed meaning/decoration and must not follow a customer brand — so `primary-{N}`→`green-{N}`, `secondary-{N}`→`blue-{N}`, and `colors.primary[N]`/`colors.secondary[N]`→`colors.green[N]`/`colors.blue[N]`. Pure rename, visually identical.
1711
+
1712
+ - Resolve the CodeMirror editor chrome (editor/gutter surface, selection, read-only wash) from live `--color-*` semantic tokens instead of literal `@keboola/tailwind-config/colors` values, so `CodeEditor`, `SqlEditor`, and the non-deleted `MergeEditor` pane re-skin with brand + dark mode like the surrounding Tailwind chrome. A small resolution layer (`CodeEditor/tokenTheme.ts`) reads the channels off `<html>` at theme-build time — mirroring `MermaidDiagram.resolvePrimary()` — and rebuilds the theme on `data-theme` / `data-brand` changes. Syntax-highlight and diff add/remove/conflict colors stay categorical (fixed across brands).
1713
+
1714
+ - Point interactive selection affordances at the semantic `primary` token (UT-4361 / brand-neutrality). The Checkbox checked fill, Command / DropdownMenu check indicators, and the default Navbar menu button now read `primary` instead of the fixed `secondary` blue ramp — following shadcn's single-brand-hue model where interactive affordances derive from `primary` (no separate interactive hue). Visually identical today (`--color-primary` is `#1f8fff`, the same blue these used); when a brand changes its primary, selection and primary actions move together. Also unifies the DropdownMenu default/argo indicator variants, which now share `primary`.
1715
+
1716
+ - Fix: a bare `Checkbox` with a `tooltip` (no label) could not be toggled. The tooltip trigger was merged onto the Radix Checkbox `Root` via `asChild`, stacking two stateful primitives on one element that collide on `data-state` — the tooltip's `open`/`closed` overwrote the checkbox's `checked`/`unchecked`, so clicks never toggled (and the checked styling never applied). Now `triggerAsChild` is only used when the checkbox has a label (the `FieldBox` wrapper is safe to merge onto); a bare checkbox uses the span-wrapping trigger so it toggles independently. Added a story interaction test as a regression guard.
1717
+
1718
+ - Make outline/invisible Button and DropdownMenu item hover follow the brand primary (modern/argo path). Their hover/focus state used the fixed `accent` token (a light blue independent of `primary`), so changing a brand's primary left the hover blue. They now use a translucent `primary` tint (`bg-primary/10` + `text-primary`), matching the `secondary` button. Visually near-identical in the Keboola brands today; now re-skins with the brand. Legacy (`default`) path unchanged.
1719
+
1720
+ - Fix dark-mode text contrast in AI chat messages. `MessageBubble` and `tw-prose` headings hardcoded fixed neutral primitives (`neutral-800`/`neutral-900`) that stayed dark on the dark surface, rendering assistant body copy and markdown headings nearly invisible. They now use the semantic `foreground` token (and `muted` for the user bubble) so they flip with `[data-theme='dark']`. All prose heading levels (`h1`–`h6`) now resolve to `foreground`, so none stay dark-on-dark in dark mode. In light mode the values are near-identical (headings `#101114`→`#1e293b`, body `#222529`→`#1e293b`, user bubble `#f2f4f7`→`#f1f5f9`), aligning chat text with the app's other headings.
1721
+
1722
+ - Fix `Navbar` dark-mode surface — the top nav hardcoded `tw-bg-white`, so it stayed white under `colorScheme="dark"` while the paired `Sidebar` (`tw-bg-sidebar`) went dark. Point it at the dark-aware `tw-bg-sidebar` token it already relies on for text/accent/ring. Also drop a dead `tw-text-neutral-400` from the search-variant `Navbar.MenuButton` (already overridden by the dark-aware `tw-text-muted-foreground`).
1723
+
1724
+ - Add CSS-var-backed `ring-offset` semantic token (`--color-ring-offset`) alongside the existing `ring` / `card` tokens, and migrate focus rings (`ring-primary`, `ring-primary/50`) and the Card surface (`bg-card` → `bg-card text-card-foreground`) onto the semantic tokens. Resizable's focus ring now resolves through `ring-ring` + `ring-offset-ring-offset`. Defaults reference the existing primary/white values, so the default `keboola` brand does not drift.
1725
+
1726
+ - RjsfSchemaEditor: render the `options.tooltip` help icon of `type: boolean` fields beside the checkbox label instead of on its own row above the checkbox.
1727
+
1728
+ - RJSF SelectWidget and DynamicSelectWidget render the Combobox with the `secondary` variant, matching the other form widgets (gray input background).
1729
+
1730
+ - Align the `Select` trigger corner radius with sibling inputs. The argo `Select.Trigger` used a fixed `rounded-lg` (`0.5rem`) instead of the token-driven `--radius-lg`, so it did not track brand radii like `InputGroup` (and therefore `Combobox`), `Button`, and `Alert` do. It now uses `var(--radius-lg)`. Visually identical under the default Keboola brand (both resolve to `0.5rem`); under `keboola-legacy` or any custom brand the trigger now re-skins with the rest of the input chrome instead of staying stuck at `0.5rem`.
1731
+
1732
+ - Dim the `Skeleton` shimmer highlight in dark mode. The moving sweep used a hardcoded near-white primitive (`neutral-50`) that glared over the dark `bg-muted` base; it now uses a white-alpha overlay (`white/40`) so the highlight stays a subtle lighter sweep over the muted surface in both light and dark color schemes.
1733
+
1734
+ - Make selection check/radio indicators brand-theme-aware on the modern (argo) path. Combobox, DropdownMenu, and Command check/checkbox/radio indicators read semantic tokens (`bg-background`, `border-input`, `bg-primary` / `text-primary-foreground`) under the argo brand, so they follow a customer brand and dark mode. The legacy (`default`) chrome path keeps its original `white` / `neutral-200` / `secondary-500` values (gated by brand variant), and categorical colors are unchanged. Visually identical in the Keboola brands today.
1735
+
1736
+ - Make focus rings follow the active brand's `primary` color instead of a fixed light blue. The `--color-ring`, `--color-sidebar-primary`, and `--color-sidebar-ring` defaults were hardcoded to Keboola blue (`#1f8fff`), so focus rings stayed blue under a custom brand (e.g. Acme purple). These three tokens now alias `var(--color-primary)` — a no-op under the default `keboola` and `keboola-legacy` brands (their primary is already that blue), and a brand can still override each token explicitly. This is a token-default change only; component styling is untouched, so the `accent` / `sidebar-accent` tokens (both customizable via the theme generator) keep driving their selection/active/hover fills as before.
1737
+
1738
+ - Updated dependencies:
1739
+ - @keboola/brand-registry@1.2.0
1740
+ - @keboola/codemirror-lang-sql@0.2.2
1741
+ - @keboola/tailwind-config@0.4.0
1742
+
1743
+ ## 4.3.1
1744
+
1745
+ ### Patch Changes
1746
+
1747
+ - Rename the categorical status colors off the legacy `primary`/`secondary` aliases onto the semantic-correct fixed `green`/`blue` palettes (UT-4361). Alert, AlertModal, Toast, Modal icon, MergeEditor conflict status, and the AutoSaveTextarea "saved" indicator are categorical per the design brand-neutrality contract (like `error`/`warning`, their color encodes fixed meaning and does not follow a customer brand), so `primary-{N}` → `green-{N}` and `secondary-{N}` → `blue-{N}`. Pure rename, visually identical — and `ModalIcon`/`AlertModalIcon` now read with the class matching the `color`/`variant` prop.
1748
+
1749
+ - Fix dark-mode contrast on the checked `DropdownMenu.RadioItem` (argo). The checked item used a hardcoded `tw-bg-sky-100` tint that stayed light under `[data-theme='dark']`, leaving the item's text near-invisible (e.g. the active option in `ColorModeSwitch`). It now uses the semantic `tw-bg-primary/10`, which flips with the color scheme.
1750
+
1751
+ - Button, Alert and InputGroup now consume the brand radius token (`var(--radius-lg)`) for their corner radius instead of hardcoded `rounded`/`rounded-lg` utilities. A brand can re-skin their radii via `ThemeProvider` (the `radii` field on `@keboola/brand-registry`) with no `@keboola/design` source change. Keboola and legacy brands render identically to before; the fallback preserves rendering when no `ThemeProvider` is mounted.
1752
+
1753
+ - Move `TimeInput` from the Layer-0 components layer to a Layer-1 block. The public import (`import { TimeInput } from '@keboola/design'`) is unchanged.
1754
+
1755
+ ## 4.3.0
1756
+
1757
+ ### Minor Changes
1758
+
1759
+ - Add the **`ColorModeSwitch`** block — a controlled light / dark / system segmented control composed from `ButtonGroup` + icon-only `Button`s (Sun / Moon / Monitor). Pass `value` + `onValueChange` and feed the same state to `ThemeProvider`'s `colorScheme`. Each segment is an icon-only button with an `aria-label`; the group is a labelled `role="group"` and the active segment carries `aria-pressed` — no `TooltipProvider` required.
1760
+
1761
+ - DX ergonomics from dogfooding the design system as an external consumer:
1762
+ - **`Tabs` accepts a `readonly` `triggers` array** — widened `triggers` from `Trigger<T>[]` to `readonly Trigger<T>[]` so consumers can pass `triggers={[...] as const}` without TS4104.
1763
+ - **`Select` `small` height now matches `Button` `small` (32px)** — changed the `small` sizing from 34px (`tw-h-[34px]`) to 32px (`tw-h-8 tw-min-h-8`) so a `Select` and a small `Button` line up in a toolbar. `medium` / `default` are unchanged.
1764
+ - **`ThemeProvider` mounts a `TooltipProvider`** — any app already wrapped in `ThemeProvider` now gets `Tooltip` support without a separate provider, removing the "Tooltip must be used within TooltipProvider" crash. A consumer's own nested `TooltipProvider` still composes (Radix tolerates nesting).
1765
+ - **`COMPONENT-API.md` coverage for layout/data components** — added `AppShell`, `PageLayout`, `PageHeader.Index`, `PageHeader.Detail`, `Table`, `Tabs`, `Timeline`, `Progress`, `EmptyState`, `Popconfirm`, `Sidebar`, `Navbar` to the generated prop-signature reference.
1766
+
1767
+ ### Patch Changes
1768
+
1769
+ - Fix dark-mode text contrast on `Table` and outlined `Button`:
1770
+ - **`Table`** set no text color, so header + cell text inherited a fixed dark color and became near-invisible under `[data-theme='dark']`. It now sets the semantic `tw-text-foreground` on the table, so text flips correctly in dark mode (light-mode appearance unchanged).
1771
+ - **`Button` `variant="outline"`** (argo) set `tw-bg-background` but no base text color (unlike `invisible`), so its label/icon inherited a fixed dark color and disappeared in dark mode. It now sets `tw-text-foreground`.
1772
+
1773
+ - Move focus-ring and focus-emphasis indicators off the fixed `secondary` blue ramp onto the semantic `ring` token (UT-4361 / brand-neutrality). Focus rings, focus outlines, and focus/open-state emphasis borders across Button, Input, Select, Switch, Radio, Checkbox-adjacent, Avatar, Badge, Breadcrumb, Alert, Search, Toggle, Sidebar action, and AIKit controls now read `--color-ring`, so a customer brand can re-tint focus affordances. Crisp indicators are visually identical today (`ring` == the previous `secondary-500`, `#1f8fff`); translucent halos move from `secondary-200` to `ring/30` (near-identical). Selection/interactive blues are intentionally left for a follow-up (they need a dedicated interactive token, not `ring`).
1774
+
1775
+ ## 4.2.0
1776
+
1777
+ ### Minor Changes
1778
+
1779
+ - **AppShell.Main default page gutter.** `AppShell.Main` now applies an opinionated default page gutter (`tw-p-6`) so page content is spaced correctly out of the box. Because `cn` is tailwind-merge, override it from the call site when the app owns its own gutter (e.g. `className="tw-p-0"`). `PageLayout` still adds no gutter of its own, so its header and content align within the shell gutter.
1780
+
1781
+ **Navbar graduated out of WIP.** Removed the "do not use" / WIP disclaimer from `Navbar` and documented it in the AGENTS layout decision tree as the top navigation bar to host inside `AppShell.Header`.
1782
+
1783
+ ### Patch Changes
1784
+
1785
+ - Bundle react-day-picker into dist. Its ESM entry evaluates ~800 modules (own dist + date-fns) on every cold import (~1 s in plain Node), which consumer test suites pay per isolated test file. Inlining the tree-shaken part cuts the kbc-ui vitest wall time by ~44 % locally; production bundles are unaffected since consumers bundle the design dist anyway.
1786
+
1787
+ - PageHeader: add an opt-in `fullWidthDescription` prop to `PageHeader.Index` and `PageHeader.Detail`. When set, the description spans its own full-width row (capped at `max-w-[800px]`) instead of sharing the title column, and the action group wraps beneath the title on narrow widths (`flex-wrap` + `ml-auto`). It defaults to `false`, so existing consumers render exactly as before — the new layout is enabled per the `sidebar-navigation` feature in kbc-ui.
1788
+
1789
+ Storybook: reworked into a single `Blocks/PageHeader` component with a Controls-driven `Playground` (variant / actions / split-primary / inline-editable / full-width-description toggles) plus `Index` and `Detail` example groups that mirror real kbc-ui pages, with linux visual-regression baselines.
1790
+
1791
+ ## 4.1.1
1792
+
1793
+ ### Patch Changes
1794
+
1795
+ - Make the package barrel resolvable under plain Node / Vitest. Previously `import('@keboola/design')` threw `Unknown file extension ".css"` because the built ESM carried a raw `import 'react-day-picker/src/style.css'`, and `import { useDropzone } from 'react-dropzone'` (a CJS module) broke Node's ESM named-export resolution — forcing external consumers into undocumented `server.deps.inline` + `test.css: true` Vitest config to render any component.
1796
+ - react-day-picker's structural `.rdp-*` CSS is now inlined into the compiled `dist/styles.css` via `postcss-import` and imported from `src/styles.css`, instead of a JS-side side-effect `import`. Consumers still get it through `@keboola/design/styles`.
1797
+ - `react-dropzone` is now bundled into the design output (tsup `noExternal`) so esbuild applies proper CJS→ESM interop; the published ESM no longer emits a bare `react-dropzone` import.
1798
+
1799
+ Rendering a design component in a consumer's Vitest test now works with a default jsdom config — no special CSS/inline handling required.
1800
+
1801
+ - Fix `MarkdownEditor` `singleLine` mode persisting backslashes before markdown punctuation (e.g. renaming a config `support_test` saved `support\_test`).
1802
+
1803
+ `singleLine` mode edits plain-text entity names/titles, so it no longer parses inline markdown formatting (`_`, `*`, `` ` ``, `~` stay literal) and unescapes the punctuation Lexical escapes on export. Full markdown mode (descriptions) is unchanged.
1804
+
1805
+ - Align `@radix-ui/*` and `@codemirror/*` dependency versions onto a single release wave so their shared internal packages (`react-dismissable-layer`, `@codemirror/state`, …) resolve to one physical copy under the pnpm isolated linker. This removes the `pnpm` `overrides` that were pinning those internals; a guard in `pnpm deps:check` now fails on any future duplicate-singleton regression.
1806
+
1807
+ - Updated dependencies:
1808
+ - @keboola/codemirror-lang-sql@0.2.1
1809
+ - @keboola/codemirror-lang-sfsql@1.2.1
1810
+
1811
+ ## 4.1.0
1812
+
1813
+ ### Minor Changes
1814
+
1815
+ - Add `AlertModal` — a compound confirmation dialog built on `@radix-ui/react-alert-dialog`.
1816
+ - Public API: `AlertModal` + `AlertModal.Trigger` and `AlertModal.Content`. `Content` composes the icon/title/description/footer from props (`variant`, `icon`, `title`, `confirmLabel`, `cancelLabel`, `children`); those subcomponents are intentionally not public. Internally, `Content` owns positioning/animation/chrome and stacks an `AlertModalHeader` (icon + title + description grid) above the footer.
1817
+ - Status variants `info` / `warning` / `danger` / `success`; brand-aware `default` vs `argo` layout.
1818
+ - Controllable open state (uncontrolled via `Trigger`, or controlled `open` + `onOpenChange`, honoring `defaultOpen`).
1819
+ - Async-aware confirm: stays open while `onConfirm`'s thenable resolves, disables the action and locks Escape while loading, then closes via `onOpenChange`.
1820
+ - Contains its own clicks/keys so they don't leak to a host that rendered it (Escape still closes); `container` prop overrides the portal target.
1821
+
1822
+ - Add the `scroll-fade` CSS utility — fades the leading/trailing edges of a scroll container to hint at more content, purely in CSS (`mask-image` + CSS scroll-driven animations, no JS and no scroll listeners).
1823
+ - Ships from `@keboola/tailwind-config` (registered via the shared plugin), so the classes are available — and autocomplete — in every app that uses the config.
1824
+ - Classes: `tw-scroll-fade` / `tw-scroll-fade-y` (vertical) and `tw-scroll-fade-x` (horizontal). Apply directly to the scrollable element.
1825
+ - Fade thickness is tunable via the `--tw-scroll-fade-size` custom property (default `1.5rem`).
1826
+ - Static-fade fallback under `@supports not (animation-timeline: scroll())` for browsers without scroll-driven animations (non-Chromium).
1827
+ - First shadcn-derived utility in the design system. See the `Utility/Scroll Fade` Storybook story.
1828
+
1829
+ - Align semantic tokens with shadcn and add dark-mode-tunable status tokens.
1830
+ - Rename the raised-container-chrome token `surface` → `card` (shadcn naming) and add its `card-foreground` pair. `card` resolves to the same values `surface` did (white in light, `#222228` in dark) — `Card`/`Modal`/`Sheet` are visually unchanged.
1831
+ - Add a global `ring` focus-ring token (brand blue), matching `sidebar-ring`.
1832
+ - Promote `secondary` / `secondary-foreground` to CSS variables (the `secondary-100..900` primitive scale is unchanged).
1833
+ - Add semantic status tokens `success-foreground`, `warning` (+ `warning-foreground`), and `info` (+ `info-foreground`), and retune dark `destructive` from `#f87171` to `#e5484d`. These flip per color scheme so error/success/warning/info stay legible on dark surfaces.
1834
+ - Add a categorical `chart-1..5` palette (brand-derived).
1835
+
1836
+ - Remove the transitional `argo` escape-hatch from the public API (UT-4339 Phase 2). Chrome is now driven entirely by the active brand via `<ThemeProvider>` — `keboola-legacy` renders legacy chrome, any other brand renders argo (`renderArgo = !isLegacy`).
1837
+ - Dropped the `argo?: boolean` prop from Button, Alert, Card, Checkbox, Switch, Toast, HoverCard, FullScreenModal, Modal, Sheet, Tooltip, RadioGroup, DatePicker, TimePicker, MergeEditor (and its accept controls), and SplitButton.
1838
+ - Dropped `'argo'` from the public `variant` union of Select, Combobox, MultiCombobox, Command, DropdownMenu, Popover, InputGroup, and Toggle. Where `'argo'` was the only non-default value, the public `variant` prop was removed entirely (now fully brand-driven); Select trigger / InputGroup keep their remaining non-argo variants.
1839
+ - The `data-argo` attribute and its `data-[argo=true]:` styling utilities are retained unchanged; `data-argo` is now driven by the active brand (`renderArgo = !isLegacy`) instead of the removed prop. Per-subtree theming (nested `<ThemeProvider>`) keeps working because `useIsLegacyBrand()` reads React context per-subtree — this intentionally does NOT switch to a root `[data-brand]` CSS selector, which cannot scope to a subtree.
1840
+ - Removed the RadioGroup group-`argo` context flag; `ModalArgoContext` is retained but now carries the brand-driven `renderArgo` value. Sub-parts derive chrome from `useIsLegacyBrand()` (or the retained context) rather than a per-call-site prop.
1841
+
1842
+ These props were already `@internal` and stripped from the published typings (UT-4355), so this is not a breaking change for npm consumers. The internal `argoVariants` style function and internal `'default' | 'argo'` cva discriminators are unchanged. To force argo on a subtree, wrap it in a nested `<ThemeProvider brand={…}>`.
1843
+
1844
+ ### Patch Changes
1845
+
1846
+ - fix: the required-field asterisk in form fields now uses the brand-overridable `primary` token instead of a hardcoded `secondary-500` blue, so it follows a custom brand (no change under the Keboola brands, whose `primary` is the same blue)
1847
+
1848
+ - PageHeader.Detail action row now wraps on narrow widths so the primary/secondary CTAs no longer clip when space is constrained (e.g. with the Kai panel docked).
1849
+
1850
+ - Remove dead Heap-era tracking surface. GTM/Heap is no longer loaded on the platform, so the `redact` prop on `Truncated` (it only set the now-defunct `data-heap-redact-text` attribute) and the unused `extractExternalClasses` helper were dead — both removed.
1851
+
1852
+ - fix(ThemeProvider): emit the `aigradient-600` / `aigradient-700` SVG defs only from the root provider. A nested `<ThemeProvider>` (e.g. section-level brand re-skinning) previously re-rendered the gradient defs, producing duplicate — HTML-invalid — element IDs and making `fill="url(#aigradient-600)"` resolution browser-dependent. The provider already detects nesting (`isRoot = parent === null`), so the defs are now gated on it; brand CSS-var injection and cleanup are unchanged.
1853
+
1854
+ - Updated dependencies:
1855
+ - @keboola/tailwind-config@0.3.0
1856
+
1857
+ ## 4.0.0
1858
+
1859
+ ### Major Changes
1860
+
1861
+ - **BREAKING:** `CodeEditor`, `SqlEditor`, `SqlDialectName`, and the `MergeEditor` component now ship from the `@keboola/design/editors` subpath instead of the package root, so apps that don't use a code editor no longer bundle CodeMirror (~1.3 MB). Update imports: `import { SqlEditor } from '@keboola/design/editors'`. The lightweight merge helpers (`PaneHeader`, `ConflictStatusIndicator`, `useMergeDiff`, diff types) and `DiffView` remain on the package root.
1862
+
1863
+ ### Minor Changes
1864
+
1865
+ - PageHeader.Detail: add an opt-in `compact` title mode that renders a smaller title since the breadcrumb already shows the entity name.
1866
+
1867
+ - feat: `SelectField` (form connector) now accepts `items` to match the base `Select`; the `options` prop is kept as a deprecated alias
1868
+
1869
+ ### Patch Changes
1870
+
1871
+ - Alert dark-mode fix. Two coupled changes so alerts are readable in dark mode:
1872
+ - **Text**: `AlertRoot` text becomes `data-[argo=true]:tw-text-foreground` (was a hardcoded `tw-text-neutral-800`), so under argo brands it flips with the active palette. `keboola-legacy` keeps `neutral-800` — no change for kbc-ui.
1873
+ - **Background**: the categorical variant tints (`error-100` / `warning-100` / `primary-100` / `secondary-100`) are light fills that don't flip, so add `[data-theme='dark']` dark equivalents in `styles.css`, scoped to the alert group class. `[data-theme='dark']` never activates for `keboola-legacy` (light-only), so kbc-ui is unaffected. Categorical status-icon colors are unchanged.
1874
+
1875
+ - docs: document that the page gutter is consumer-owned (AppShell.Main/PageLayout impose none) and that Sheet titles use `Sheet.Header title=` (not the raw `Sheet.Title`)
1876
+
1877
+ - fix(MarkdownEditor): drop the bottom mask-image fade on clamped (`clampDisplay`) descriptions. On empty or single-line content the fade rendered as a broken-looking gradient. `line-clamp-2` is self-gating (inert below three lines), so short/empty content now renders clean while long text still truncates; a CSS fade cannot be gated on overflow without JS, so removing it is the minimal robust fix.
1878
+
1879
+ ## 3.2.3
1880
+
1881
+ ### Patch Changes
1882
+
1883
+ - Make accent, the sidebar token group, and the brand font family brand-overridable.
1884
+ - `colors` / `colorsDark` gain optional `accent` / `accentForeground` — accent is a general semantic colour (it drives both content and the sidebar), so it lives with the core palette rather than a sidebar-specific group.
1885
+ - `Brand` gains optional `sidebarColors` / `sidebarColorsDark` for the shadcn `sidebar` token group.
1886
+ - `typography.fontFamily` (non-empty, trimmed) is applied: `brandToCssVars` emits `--font-sans`, which the design stylesheet's `html` `font-family` reads (inherited text), falling back to the canonical Inter stack. Propagating it to elements that set `tw-font-sans` explicitly is a follow-up `tailwind-config` change.
1887
+
1888
+ All slots are additive and emitted only when a brand sets them, so brands that omit them — including `keboola` / `keboola-legacy` — render byte-identically. No default drift.
1889
+
1890
+ - chore: declare `react`/`react-dom` as peer dependencies (range `^19.0.0`) and move them out of runtime `dependencies` into `devDependencies`, so external consumers get an install-time signal for the required React without risking a nested duplicate React copy
1891
+
1892
+ - Updated dependencies:
1893
+ - @keboola/brand-registry@1.1.0
1894
+
1895
+ ## 3.2.2
1896
+
1897
+ ### Patch Changes
1898
+
1899
+ - Ship the consumer usage contract (`AGENTS.md`) and generated prop signatures (`COMPONENT-API.md`) inside the npm tarball so external consumers get version-pinned guidance. Contributor-only sections moved to a non-shipped `CONTRIBUTING.md`. Theming docs updated for `ThemeProvider colorScheme` and brand-driven argo.
1900
+
1901
+ ## 3.2.1
1902
+
1903
+ ### Patch Changes
1904
+
1905
+ - docs: correct `Badge` `color` JSDoc to match the type — `orange` (not `yellow`) for warning; remove `pink` (not in the union)
1906
+
1907
+ - Clarify the `useTheme` / `useColorScheme` missing-provider error message — it no longer implies the optional `brand` / `colorScheme` props are required to mount `<ThemeProvider>`.
1908
+
1909
+ ## 3.2.0
1910
+
1911
+ ### Minor Changes
1912
+
1913
+ - `PageHeader.Detail` now renders whatever is passed into its `badges` slot as pills, regardless of the `variant` the caller passes. The slot forces pill shape/case/tracking — and neutralises consumer-forced vertical padding so the pill keeps a uniform height — on any `Badge` inside it (including badges nested in wrapper components) by targeting the Badge root's new `data-slot="badge"` hook, so header status/category tags read consistently as pills across detail pages.
1914
+
1915
+ - ThemeProvider: add `colorScheme` ('light' | 'dark' | 'system', default light) with system `prefers-color-scheme` tracking and root-owned `data-theme`; inject the active mode's brand palette; add `useColorScheme`.
1916
+
1917
+ ### Patch Changes
1918
+
1919
+ - Storybook: add a **Mode** toolbar (Light / Dark / System) that drives `ThemeProvider`'s `colorScheme`, so the `[data-theme='dark']` palette (and a brand's `colorsDark`) can be previewed. `ThemeDecorator` gains an optional `colorScheme` prop and renders stories on a `bg-background` surface so the active mode is visible. Previously the only "Dark" control was the `backgrounds` addon, which repaints the canvas backdrop but never activates dark mode — leaving light-mode text on a dark backdrop.
1920
+
1921
+ ## 3.1.1
1922
+
1923
+ ### Patch Changes
1924
+
1925
+ - Add a selected-row state (`aria-selected:tw-bg-accent`) and a sortable-header focus ring (`focus-within:tw-ring-primary/50`) to `Table`, both on semantic tokens. (The container/border/header/hover token migration already landed separately.) No public API change.
1926
+
1927
+ ## 3.1.0
1928
+
1929
+ ### Minor Changes
1930
+
1931
+ - ThemeProvider's `brandToCssVars` now passes brand colour values straight to the CSS custom properties — the hex→RGB-triplet converter is gone because `@keboola/brand-registry`'s schema now stores colours as channel strings natively. `<MermaidDiagram>` reads `--color-primary` channels at runtime and formats `rgb(${channels})` for Mermaid's `activeTaskBorderColor`, replacing the previous `resolvePrimaryHex` round-trip. Colors palette docs omit `primary.DEFAULT` / `primary.foreground` from the swatch grid since they're now brand-driven CSS-var references rather than static hex.
1932
+
1933
+ - Remove the legacy FontAwesome icon shim. `setLegacyIconRenderer`, `LegacyFAIconValue`, and the internal `renderLegacyIcon` dispatcher are deleted; every component `icon` prop now takes a Lucide `ReactNode` directly. No consumer outside the icon migration referenced these exports.
1934
+
1935
+ - Tabs: split the wrapper-vs-trigger styling concerns into two props. `triggerClassName` now always lands on the actual `<TabsTrigger>` button so consumer-supplied padding/typography/focus styles affect the clickable element. The new `triggerGroupClassName` prop applies to the wrapper `<div>` that hosts the trigger + optional close button — use it for cell-level chrome (right-edge dividers, group-level opacity, flex sizing).
1936
+
1937
+ Closable tab hover styles now activate via `group-hover` on the wrapper, so the trigger keeps its hover treatment while the pointer moves between trigger and close button. Close button's `aria-label` is now derived from the tab title when the title is a string (`Close <title>`) so screen-reader users can distinguish multiple closable tabs.
1938
+
1939
+ ### Patch Changes
1940
+
1941
+ - Align data-display chrome to semantic tokens (shadcn convention) on the modern path, so it re-skins with the active brand and dark mode:
1942
+ - **Table** — header `bg-neutral-100`→`bg-muted`, header/body/container borders `neutral-150`→`border`, row hover `bg-neutral-100`→`bg-muted/50`.
1943
+ - **Tabs** — inactive/disabled trigger `text-neutral-400`→`text-muted-foreground`, active + hover accent `text-secondary-500`/`border-secondary-500`→`text-primary`/`border-primary`, list border `neutral-200`→`border`.
1944
+ - **Timeline** — connector line/L-corner `neutral-200`→`border`, label chip `bg-neutral-100`/`text-neutral-300`→`bg-muted`/`text-muted-foreground`, icon node `bg-white`→`bg-background`.
1945
+ - **Skeleton** — base fill `bg-neutral-150`→`bg-muted`.
1946
+ - **Command** — group heading `text-neutral-400`→`text-muted-foreground` + border `neutral-150`→`border`, item icon slot `text-neutral-400`→`text-muted-foreground`.
1947
+
1948
+ Legacy CVA branches and categorical colors unchanged.
1949
+
1950
+ - Align date/time-picker chrome to semantic tokens (shadcn convention) so it re-skins with the active brand and dark mode:
1951
+ - **Calendar** — selected day + range start/end `bg-secondary-500`/`text-white`→`bg-primary`/`text-primary-foreground`; range middle `bg-secondary-300`→`bg-accent`/`text-accent-foreground`; today + day hover `bg-secondary-100`/`bg-secondary-200`→`bg-accent`; nav chevrons `fill-secondary-500`→`fill-foreground`.
1952
+ - **TimePicker** — selected `bg-secondary-500`/`text-white`→`bg-primary`/`text-primary-foreground`; hover `bg-secondary-200`→`bg-accent`; focus outline `secondary-500`→`primary`.
1953
+ - **DateRangePicker** — preset-list divider `border-neutral-100`→`border-border`.
1954
+
1955
+ These were on the live modern path (no legacy gate) using the unwired `secondary-*` hex; they now track the brand. No prop/API changes.
1956
+
1957
+ - Align editor chrome to semantic tokens (shadcn convention) so the surrounding UI re-skins with the brand and dark mode:
1958
+ - **MarkdownEditor** — link `text-secondary-500`→`text-primary`; inline code `bg-neutral-100`→`bg-muted`; checklist checked box `bg/border-secondary-500`→`bg/border-primary` + check glyph `border-white`→`border-primary-foreground`; unchecked box `border-neutral-200`/`bg-white`→`border-input`/`bg-background`; editor hover `bg-neutral-50`→`bg-muted`, focus ring `secondary-500/40`→`primary/40`; placeholder `text-neutral-400`→`text-muted-foreground`.
1959
+ - **DiffView / MergeEditor / CodeEditor** — the Tailwind-class chrome (container/gutter/code backgrounds, pane borders, sticky header, pane-header text, fold/toolbar controls, scroller bg) moves from `neutral-*`/`white`/`secondary-*` to `muted`/`background`/`foreground`/`muted-foreground`/`border`/`accent`.
1960
+
1961
+ Categorical diff add/remove and syntax-highlighting colors are unchanged. The in-editor CodeMirror theming (configured via JS `colors.*` objects that can't consume CSS-var tokens) is **not** touched here — that needs a separate token-resolution refactor.
1962
+
1963
+ - Align form-control chrome to semantic tokens (shadcn convention). `Label` (+ `Label.Optional`), `HelpBlock`, and the `Select`/`Combobox` group labels and separators now use `text-foreground` / `text-muted-foreground` / `text-destructive` / `bg-border` instead of hardcoded `neutral-*` / `error-*` scales, so this chrome re-skins with the active brand and dark mode. No prop or API changes; the `HelpBlock` `warning` state (categorical, no semantic token) and legacy CVA branches are unchanged.
1964
+
1965
+ - Align miscellaneous chrome to semantic tokens so it re-skins with the brand and dark mode:
1966
+ - **EmptySearchContent** — message text `text-neutral-400`→`text-muted-foreground`.
1967
+ - **DotsLoader** — dots `bg-neutral-700`→`bg-muted-foreground`.
1968
+ - **ZoomableSvg** — fullscreen backdrop + container `bg-neutral-50`→`bg-muted`, border `neutral-150`→`border`.
1969
+ - **ScrollArea** — thumb `bg-neutral-200`/`hover:bg-neutral-400`→`bg-border`/`hover:bg-muted-foreground`, and removed a stray `[data-state="hidden"]:bg-pink-500` debug class.
1970
+
1971
+ - Align overlay surfaces to semantic tokens on the argo path so they re-skin with the brand and dark mode (mirroring `HoverCard`, which already did this):
1972
+ - **Popover** (and **Popconfirm**, which composes it) — the argo variant previously only changed border-radius; the surface stayed `bg-white`/`text-neutral-800`/`border-neutral-100`. Argo now uses `bg-popover`/`text-popover-foreground`/`border-border`. The legacy `default` variant keeps the fixed white panel.
1973
+ - **DropdownMenu** — argo content surface now `bg-popover`/`text-popover-foreground`/`border-border` (was unconditional `bg-white` + colorless border); argo item hover/focus moves from hardcoded `bg-sky-100`/`text-slate-800` to `bg-accent`/`text-accent-foreground`, and resting item text to `text-popover-foreground`.
1974
+
1975
+ Legacy (`data-[variant=default]`) branches are unchanged.
1976
+
1977
+ - Modernize `CollapsiblePanel` and `PanelWithDetails` chrome to semantic tokens. These components had no brand awareness at all — they hardcoded `neutral-*` surfaces/text and an unwired `secondary-500` hover, so they could never re-skin. Header surface `bg-neutral-100`→`bg-muted` (and the `white` variant → `bg-background`), text `neutral-400`/`neutral-800`→`muted-foreground`/`foreground`, container border `neutral-200`→`border`, hover `text-secondary-500`→`text-primary`. They now track the active brand and dark mode.
1978
+
1979
+ - Align text-link color to the semantic `primary` token (shadcn link convention). `Link` and `ButtonInline` (`color="link"`) used the unwired `secondary-500`/`secondary-600` hex; they now use `text-primary` / `hover:text-primary/90`, so links track the brand and dark mode. `ButtonInline`'s `dark` / `danger` / `muted` colors are unchanged.
1980
+
1981
+ - Repoint argo input-surface fills off the app-background token: `InputGroup`, `Select` trigger, `RadioGroup`, and `InputOtp` slot now use `tw-bg-muted` (the semantic equivalent of the legacy `secondary` gray) instead of `tw-bg-background`. `tw-bg-background` is the page surface; input fields get a muted gray fill that stays distinct from white containers (Modal/Popover/Card) and flips correctly in dark mode. Legacy (`keboola-legacy`) `primary`/`secondary` input chrome is unchanged.
1982
+
1983
+ - Finalize dark-mode (Graphite) semantic tokens and add a `--color-surface` token for raised container chrome (Modal/Card/Sheet). In light mode, argo `Card` and `Sheet` move from the off-white `background` (`#f9fafb`) to white `surface` (`#ffffff`) — `Modal` was already white, so it is visually unchanged; in dark mode these surfaces lift off the page background, fixing the surface/input collision.
1984
+
1985
+ - Resolve error-severity React state/effect findings: derive state during render instead of syncing it in effects (`useSHA256`, `SidebarProvider`, `PromptInput`, `MermaidDiagram`) and widen `Checkbox`'s overload return type to `ReactNode`. No behavior change.
1986
+
1987
+ - Fix three latent token bugs found during the shadcn token audit:
1988
+ - **`CardTitle`** used `tw-neutral-800` — missing the `tw-text-` prefix, so the class was inert and the title had no explicit color. Now `tw-text-foreground`.
1989
+ - **`ResizableHandle`** used `tw-ring-ring`, a token that doesn't exist in our config (so the focus ring fell back to Tailwind's default), and `tw-bg-neutral-50` for the divider. Now `tw-ring-primary` (our focus-ring substitute) and `tw-bg-border`.
1990
+
1991
+ - Modal: drop the `renderArgo ? 'tw-bg-background' : 'tw-bg-white'` conditional in `ModalContent` and pin the surface to `tw-bg-white`. In argo brand mode the previous `tw-bg-background` token collided with `InputGroup`'s identical token, so input fields visually merged into the modal surface (no perceptible input boundary). The hard-coded `tw-bg-white` restores the surface-vs-input distinction. `renderArgo` is still propagated via `data-argo` and `ModalArgoContext` for nested chrome that legitimately tracks the argo flag.
1992
+
1993
+ - Restore multiline editing in `PromptInput` when slash commands are enabled. The cmdk `Command` root backing the slash menu stays mounted while the menu is closed and preventDefaults ArrowUp/ArrowDown/Home/End/Enter, which broke vertical caret movement and Shift+Enter newlines in the textarea. Those keys are now stopped from reaching the cmdk root while the menu is closed; the menu still opens on `/` and navigates normally.
1994
+
1995
+ - Raise `Select.Content` z-index to 1055 so Select dropdowns layer above Modal instead of being hidden behind it (matches Combobox/Popover).
1996
+
1997
+ - Migrate unconditional Sidebar divider/label and Input suffix hardcoded colors to semantic tokens so they flip in dark mode.
1998
+
1999
+ - Drop Textarea's redundant hardcoded `variant` default and refresh TextInput/Textarea JSDoc to reflect brand-driven input chrome.
2000
+
2001
+ - Updated dependencies:
2002
+ - @keboola/brand-registry@1.0.0
2003
+ - @keboola/tailwind-config@0.2.0
2004
+
2005
+ ## 3.0.0
2006
+
2007
+ ### Major Changes
2008
+
2009
+ - Drop FontAwesome dependency.
2010
+
2011
+ **Breaking change:** the `Icon` component and its named exports (along with the `Icons.mdx` doc and FA-shape icon constants) are no longer exported from `@keboola/design`. Consumers should import icons directly from `@keboola/design/icons` (Lucide), or — for the small remaining FA-backed surface during migration — host their own thin wrapper (see `apps/kbc-ui/src/scripts/react/common/LegacyIcon.tsx`).
2012
+
2013
+ `@fortawesome/fontawesome-svg-core` and `@fortawesome/react-fontawesome` are removed from this package's runtime dependencies; `ThemeProvider` no longer eagerly loads the FA icon library.
2014
+
2015
+ ## 2.1.0
2016
+
2017
+ ### Minor Changes
2018
+
2019
+ - Ship compiled `dist/` outputs (CJS + ESM + DTS) instead of TS source. External consumers no longer need a TypeScript build pipeline to use `@keboola/design`. Resolves the footgun documented in UT-4090.
2020
+
2021
+ ### Patch Changes
2022
+
2023
+ - Add `@keboola/design/styles/compiled` sub-export — the same `dist/styles.css` `build:css` already produces, exposed for consumers that can't run Tailwind. `@keboola/design/styles` continues to point at the source CSS for the Tailwind-extending path used by Keboola apps.
2024
+
2025
+ ## 2.0.1
2026
+
2027
+ ### Patch Changes
2028
+
2029
+ - Size `NavbarMenuItemAdornment` `icon-button` variant to 16px (`tw-size-4`) and force `tw-p-0` over the base `tw-p-1`. Also defaults un-sized SVGs inside to `tw-size-4` instead of the global 12px fallback, so notification/search icons in the navbar render at their intended visual weight.
2030
+
2031
+ ## 2.0.0
2032
+
2033
+ ### Major Changes
2034
+
2035
+ - Remove `PageLayout.Toolbar` sub-component. The search / filter row is now its own block — `PageToolbar` — with structured slots (`Search`, `Right`, `Filters`, `Sort`, `Actions`, `View`, `ActiveFilters`, `Chip`, `AddFilter`, `Clear`).
2036
+
2037
+ Migration:
2038
+
2039
+ ```tsx
2040
+ // before
2041
+ <PageLayout.Toolbar>
2042
+ <SearchInput … />
2043
+ </PageLayout.Toolbar>
2044
+
2045
+ // after
2046
+ <PageToolbar>
2047
+ <PageToolbar.Search>
2048
+ <SearchInput … />
2049
+ </PageToolbar.Search>
2050
+ </PageToolbar>
2051
+ ```
2052
+
2053
+ For Figma-rebuilt multi-row toolbars (filter chips below the search row), reach for `PageToolbar.ActiveFilters` rather than overriding `PageLayout.Toolbar` with `tw-flex-col` utilities.
2054
+
2055
+ ## 1.3.1
2056
+
2057
+ ### Patch Changes
2058
+
2059
+ - Prevent the leading magnifying-glass icon in `Search`, `SearchInput`, and `SearchInputWithNavigation` from shrinking when the input wrapper is a constrained flex container — adds `tw-shrink-0` to the icon so it stays at its rendered `tw-size-4` instead of being squashed by long placeholders or sibling content.
2060
+
2061
+ ## 1.3.0
2062
+
2063
+ ### Minor Changes
2064
+
2065
+ - Add `argo` prop to `MergeEditor` that propagates to the editor's internal accept (chevron) and dismiss (xmark) buttons. Layout chrome (panes, pane headers, scrollers) is unchanged — only consumer-controlled interactive surfaces switch theme. Storybook ships an `Argo` story alongside the existing `WithConflicts` story so the variant is visually verifiable.
2066
+
2067
+ ## 1.2.1
2068
+
2069
+ ### Patch Changes
2070
+
2071
+ - Re-publish to scrub `workspace:^` from the npm tarball manifests.
2072
+
2073
+ `@keboola/design@1.2.0` shipped with literal `workspace:^` strings in
2074
+ `dependencies` (`codemirror-lang-sfsql`, `codemirror-lang-sql`,
2075
+ `tailwind-config`), making `npm install @keboola/design` fail with
2076
+ `EUNSUPPORTEDPROTOCOL` — npm has no way to resolve the yarn-only
2077
+ workspace protocol against a public registry.
2078
+ `@keboola/api-client-react@1.0.1` shipped with the same
2079
+ leak in `peerDependencies` (`@keboola/api-client`) — less fatal
2080
+ because peerDeps are warnings, not errors, but still wrong.
2081
+
2082
+ Root cause was the publish workflow invoking plain `npm publish`
2083
+ (which doesn't understand `workspace:^`) instead of
2084
+ `yarn npm publish` (which rewrites it just-in-time). This changeset
2085
+ rides alongside the workflow script fix, so the next release cycle
2086
+ republishes both packages cleanly through the corrected path.
2087
+
2088
+ The other recently-bumped packages (`api-client`, `brand-registry`,
2089
+ `brand-audit`, `codemirror-lang-*`, `agent-precheck`,
2090
+ `oxlint-config`) don't need patches — they happen to have no
2091
+ `@keboola/*` runtime or peer deps, so the bug couldn't bite them.
2092
+
2093
+ - Fix consumer-side `tsc --strict` compilation against `@keboola/design`'s
2094
+ published `src/`.
2095
+ - `@keboola/codemirror-lang-sfsql` now ships an `index.d.ts` declaration
2096
+ file (plus `types`/`exports` map and a `@codemirror/language` peer dep)
2097
+ so consumers without `allowJs` get real types for `sqlMode`,
2098
+ `modeSpec`, and `sfsql` instead of an implicit `any`. Previously
2099
+ `import { sfsql } from '@keboola/codemirror-lang-sfsql'` produced
2100
+ TS7016 "Could not find a declaration file" in any strict consumer.
2101
+ - Added missing `override` modifiers in design's source where TS strict's
2102
+ `noImplicitOverride` requires them: `HighlightMarker.toDOM`,
2103
+ `SafeGutterMarker.elementClass`, `ConflictGutterMarker.elementClass`,
2104
+ and every Lexical `DecoratorNode` lifecycle method on `ImageNode` and
2105
+ `MermaidNode`. Previously these tripped TS4114 when the consumer's
2106
+ `tsc` walked design's `src/` (the published main entry).
2107
+
2108
+ - Updated dependencies:
2109
+ - @keboola/codemirror-lang-sfsql@1.2.0
2110
+
2111
+ ## 1.2.0
2112
+
2113
+ ### Minor Changes
2114
+
2115
+ - Unprivatize `@keboola/design` so it ships to npm via the existing changesets release flow. The package was held back at `private: true` while Stage B (the tsup-built dist exports) was paused — but the in-flight publish work (Stage A + the inlined `@keboola/utils` symbols + all three workspace dependencies now public) means we can ship the source-export path today without finishing Stage B.
2116
+
2117
+ Concretely:
2118
+ - `exports."."` still points at `./src/index.ts`, so consumers compile TypeScript themselves (same model as the rest of the platform packages).
2119
+ - All workspace dependencies (`@keboola/tailwind-config`, `@keboola/codemirror-lang-sql`, `@keboola/codemirror-lang-sfsql`) are already on npm.
2120
+ - The dist-export work tracked in #5862 / UT-4009 Stage B can land on its own track without blocking experiments and external consumers any further.
2121
+
2122
+ ## 1.1.0
2123
+
2124
+ ### Minor Changes
2125
+
2126
+ - Add library build infrastructure for `@keboola/design` (Stage A of UT-4009). Preparatory work — the package remains `private: true` and the `exports` map still points at `src/`, so workspace consumers are unaffected and nothing is published yet.
2127
+ - New `tsup.config.ts` produces dual ESM/CJS bundles for three entry points (`index`, `mermaid`, `icon-names`) into `dist/`.
2128
+ - New `build:css` script invokes the Tailwind CLI to compile `src/styles.css` into a minified `dist/styles.css` (~173 KB). Once Stage B flips the `exports` map to `dist/`, consumers will be able to `import '@keboola/design/styles'` against the published package.
2129
+ - Publish metadata added (`description`, `license`, `repository`, `homepage`, `bugs`, `keywords`, `publishConfig`, `files`).
2130
+ - Stage B will fix a TypeScript type-leak from `@radix-ui/react-select` (private `SelectSharedProps` interface), enable DTS generation, flip the `exports` map to `dist/`, add `transit` + `prepublishOnly` + `build:check`, and flip `private: false`.
2131
+
2132
+ No runtime change for workspace consumers; design still resolves through `src/` exports as before.
2133
+
2134
+ ### Patch Changes
2135
+
2136
+ - Updated dependencies:
2137
+ - @keboola/codemirror-lang-sfsql@1.1.0
2138
+ - @keboola/codemirror-lang-sql@0.2.0