@keboola/design 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +25 -0
  2. package/dist/icons.cjs +731 -0
  3. package/dist/icons.cjs.map +1 -0
  4. package/dist/icons.js +29 -0
  5. package/dist/icons.js.map +1 -0
  6. package/dist/index.cjs +429 -333
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +426 -331
  9. package/dist/index.js.map +1 -1
  10. package/dist/markdown-editor.cjs +270 -135
  11. package/dist/markdown-editor.cjs.map +1 -1
  12. package/dist/markdown-editor.js +270 -135
  13. package/dist/markdown-editor.js.map +1 -1
  14. package/dist/mermaid.cjs +24 -15
  15. package/dist/mermaid.cjs.map +1 -1
  16. package/dist/mermaid.js +24 -15
  17. package/dist/mermaid.js.map +1 -1
  18. package/dist/styles.css +1 -1
  19. package/package.json +12 -5
  20. package/src/blocks/AIKit/CollapsibleStepHeader.tsx +7 -8
  21. package/src/blocks/AIKit/InlineStepLabel.tsx +7 -7
  22. package/src/blocks/AIKit/MessageBubble.stories.tsx +11 -3
  23. package/src/blocks/AIKit/MessageBubble.tsx +1 -1
  24. package/src/blocks/AIKit/PlanApproval.tsx +9 -8
  25. package/src/blocks/AIKit/PlanRewritingIndicator.tsx +2 -2
  26. package/src/blocks/AIKit/PromptInput.stories.tsx +10 -2
  27. package/src/blocks/AIKit/PromptInputSubmit.tsx +12 -8
  28. package/src/blocks/AIKit/Question.tsx +8 -9
  29. package/src/blocks/AIKit/SuggestedPrompt.tsx +12 -5
  30. package/src/blocks/AIKit/ThinkingIndicator.tsx +2 -2
  31. package/src/blocks/AutoSaveTextarea/AutoSaveTextarea.tsx +4 -4
  32. package/src/blocks/ClipboardButton/ClipboardButton.tsx +7 -5
  33. package/src/blocks/ClipboardButton/ClipboardInline.tsx +2 -3
  34. package/src/blocks/ColorInput/ColorInput.tsx +4 -2
  35. package/src/blocks/ColorInput/ColorPicker.tsx +5 -2
  36. package/src/blocks/EmptyState/EmptyState.tsx +16 -0
  37. package/src/blocks/ErrorState/ErrorState.tsx +15 -0
  38. package/src/blocks/Form/formHook.ts +15 -0
  39. package/src/blocks/MultiActionButtonGroup/MultiActionButton.stories.tsx +12 -11
  40. package/src/blocks/MultiActionButtonGroup/MultiActionButton.tsx +4 -4
  41. package/src/blocks/MultiCombobox/MultiCombobox.tsx +28 -12
  42. package/src/blocks/PageHeader/AGENTS.md +63 -3
  43. package/src/blocks/PageHeader/PageHeader.stories.tsx +124 -28
  44. package/src/blocks/PageHeader/PageHeader.tsx +15 -0
  45. package/src/blocks/PageHeader/PageHeaderDetail.tsx +23 -1
  46. package/src/blocks/PageHeader/PageHeaderIndex.tsx +20 -1
  47. package/src/blocks/PageLayout/PageLayout.tsx +26 -0
  48. package/src/blocks/PageToolbar/PageToolbar.stories.tsx +348 -0
  49. package/src/blocks/PageToolbar/PageToolbar.tsx +119 -0
  50. package/src/blocks/PageToolbar/index.ts +1 -0
  51. package/src/blocks/PasswordInput/PasswordInput.tsx +10 -10
  52. package/src/blocks/SearchInput/SearchInput.tsx +30 -4
  53. package/src/blocks/SearchInputWithNavigation/SearchInputWithNavigation.tsx +9 -6
  54. package/src/blocks/SplitButton/SplitButton.stories.tsx +7 -4
  55. package/src/blocks/SplitButton/SplitButton.tsx +17 -4
  56. package/src/blocks/Tagger/TagItem.tsx +4 -1
  57. package/src/blocks/Tagger/TagSelectPopover/TagSelectCreateItem.tsx +3 -1
  58. package/src/blocks/Tagger/TagSelectPopover/TagSelectList.tsx +2 -2
  59. package/src/blocks/Tagger/Tagger.tsx +2 -2
  60. package/src/components/Alert/Alert.stories.tsx +11 -3
  61. package/src/components/Alert/Alert.tsx +19 -1
  62. package/src/components/Alert/AlertCollapsibleIcon.tsx +3 -4
  63. package/src/components/Alert/AlertIcon.tsx +18 -21
  64. package/src/components/Badge/Badge.stories.tsx +22 -4
  65. package/src/components/Badge/Badge.tsx +16 -0
  66. package/src/components/Badge/BadgeButton.tsx +7 -6
  67. package/src/components/Breadcrumb/BreadcrumbEllipsis.tsx +2 -2
  68. package/src/components/Breadcrumb/BreadcrumbSeparator.tsx +2 -2
  69. package/src/components/Button/Button.stories.tsx +14 -2
  70. package/src/components/Button/Button.tsx +42 -5
  71. package/src/components/Button/styles.ts +4 -6
  72. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +32 -10
  73. package/src/components/Card/Card.tsx +17 -0
  74. package/src/components/Checkbox/Checkbox.tsx +3 -6
  75. package/src/components/CodeEditor/highlighting.ts +1 -1
  76. package/src/components/CollapsiblePanel.tsx +2 -5
  77. package/src/components/Combobox/Combobox.tsx +12 -14
  78. package/src/components/Combobox/ComboboxChips.tsx +6 -1
  79. package/src/components/Combobox/ComboboxItem.tsx +3 -4
  80. package/src/components/Combobox/ComboboxItemIndicator.tsx +6 -4
  81. package/src/components/Combobox/types.ts +2 -6
  82. package/src/components/Command/Command.stories.tsx +47 -56
  83. package/src/components/Command/CommandItem/CheckIndicator.tsx +26 -14
  84. package/src/components/Command/CommandItem/CommandItem.tsx +6 -3
  85. package/src/components/DropdownMenu/DropdownMenu.stories.tsx +8 -4
  86. package/src/components/DropdownMenu/DropdownMenuCheckboxItem.tsx +4 -1
  87. package/src/components/DropdownMenu/DropdownMenuItem.tsx +12 -6
  88. package/src/components/DropdownMenu/DropdownMenuItemIndicator.tsx +12 -6
  89. package/src/components/DropdownMenu/DropdownMenuRadioGroup/DropdownMenuRadioItem.tsx +8 -5
  90. package/src/components/DropdownMenu/DropdownMenuSub/DropdownMenuSubTrigger.tsx +2 -2
  91. package/src/components/DropdownMenu/ItemIcon.tsx +16 -8
  92. package/src/components/FormGroup/FormGroup.tsx +15 -0
  93. package/src/components/FullScreenModal/FullScreenModalHeader.tsx +4 -1
  94. package/src/components/Input/DateInput/DateInput.tsx +4 -1
  95. package/src/components/Input/DateRangeInput/DateRangeInput.tsx +4 -6
  96. package/src/components/Input/Input.tsx +18 -2
  97. package/src/components/Input/TextInput/TextInput.tsx +16 -0
  98. package/src/components/Input/TimeInput/TimeInput.tsx +4 -1
  99. package/src/components/InputGroup/InputGroup.stories.tsx +11 -3
  100. package/src/components/MarkdownEditor/MarkdownEditor.stories.tsx +63 -1
  101. package/src/components/MarkdownEditor/MarkdownEditor.tsx +105 -15
  102. package/src/components/MarkdownEditor/nodes/ImageNode.tsx +10 -10
  103. package/src/components/MarkdownEditor/nodes/MermaidNode.tsx +8 -8
  104. package/src/components/MarkdownEditor/plugins/KeyboardPlugin.tsx +47 -3
  105. package/src/components/MarkdownEditor/plugins/MarkdownSyncPlugin.tsx +12 -3
  106. package/src/components/MarkdownEditor/plugins/ToolbarPlugin.tsx +20 -10
  107. package/src/components/MarkdownEditor/plugins/toolbar/AlignmentDropdown.tsx +20 -14
  108. package/src/components/MarkdownEditor/plugins/toolbar/BlockTypeDropdown.tsx +25 -16
  109. package/src/components/MarkdownEditor/plugins/toolbar/LinkPanel.tsx +3 -3
  110. package/src/components/MarkdownEditor/transformers.ts +10 -0
  111. package/src/components/MergeEditor/AcceptControls/AcceptButton.tsx +9 -4
  112. package/src/components/MergeEditor/AcceptControls/AcceptControls.tsx +25 -14
  113. package/src/components/MergeEditor/ConflictStatus/ConflictStatusIndicator.tsx +2 -2
  114. package/src/components/MergeEditor/MergeEditor.stories.tsx +15 -0
  115. package/src/components/MergeEditor/MergeEditor.tsx +9 -0
  116. package/src/components/MergeEditor/MergePane.tsx +22 -4
  117. package/src/components/MergeEditor/decorations.ts +2 -2
  118. package/src/components/Modal/Modal.stories.tsx +6 -4
  119. package/src/components/Modal/Modal.tsx +26 -0
  120. package/src/components/Modal/ModalBody.tsx +5 -0
  121. package/src/components/Modal/ModalCloseIcon.tsx +2 -3
  122. package/src/components/Modal/ModalContent.tsx +8 -0
  123. package/src/components/Modal/ModalFooter.tsx +5 -0
  124. package/src/components/Modal/ModalHeader.tsx +11 -0
  125. package/src/components/Modal/ModalIcon.tsx +45 -19
  126. package/src/components/Navbar/Navbar.stories.tsx +26 -13
  127. package/src/components/Navbar/NavbarMenuButton.tsx +11 -10
  128. package/src/components/Pagination/PaginationEllipsis.tsx +2 -2
  129. package/src/components/Pagination/PaginationNext.tsx +2 -1
  130. package/src/components/Pagination/PaginationPrevious.tsx +4 -1
  131. package/src/components/PanelWithDetails.tsx +3 -4
  132. package/src/components/Popconfirm/Popconfirm.stories.tsx +13 -5
  133. package/src/components/RadioGroup/Radio.tsx +6 -12
  134. package/src/components/Search/ClearButton.tsx +2 -2
  135. package/src/components/Search/Search.tsx +3 -3
  136. package/src/components/Select/Select.tsx +16 -5
  137. package/src/components/Select/SelectContent.tsx +13 -4
  138. package/src/components/Select/SelectItem.tsx +6 -5
  139. package/src/components/Select/SelectTrigger.tsx +2 -2
  140. package/src/components/Sidebar/Sidebar.stories.tsx +55 -19
  141. package/src/components/Sidebar/SidebarGroup/SidebarGroupAction.tsx +8 -4
  142. package/src/components/Sidebar/SidebarMenu/SidebarMenuButton.tsx +4 -2
  143. package/src/components/Sidebar/SidebarMenu/SidebarMenuCard.tsx +3 -2
  144. package/src/components/Sidebar/SidebarMenu/SidebarMenuIcon.tsx +12 -12
  145. package/src/components/Sidebar/SidebarMenu/SidebarMenuSub.tsx +3 -7
  146. package/src/components/Sidebar/SidebarMenu/SidebarSearchInput.tsx +3 -5
  147. package/src/components/Skeleton/Skeleton.stories.tsx +52 -0
  148. package/src/components/Skeleton/Skeleton.tsx +24 -1
  149. package/src/components/Table/Table.stories.tsx +223 -1
  150. package/src/components/Table/Table.tsx +17 -0
  151. package/src/components/Tabs.tsx +25 -2
  152. package/src/components/Toast/ToastDismiss.tsx +3 -3
  153. package/src/components/Toast/ToastRoot.tsx +24 -19
  154. package/src/components/Toggle/Toggle.tsx +11 -6
  155. package/src/components/Tooltip/Tooltip.tsx +23 -0
  156. package/src/components/ZoomableSvg/ZoomableSvg.tsx +8 -6
  157. package/src/icons/Keboola.tsx +28 -0
  158. package/src/icons/icons.stories.tsx +174 -0
  159. package/src/icons/index.ts +180 -0
  160. package/src/index.ts +1 -0
  161. package/src/patterns/AlertWithAction.stories.tsx +30 -0
  162. package/src/patterns/CardWithEmptyState.stories.tsx +66 -0
  163. package/src/patterns/ConfirmDialog.stories.tsx +72 -0
  164. package/src/patterns/CreatableMultiCombobox.stories.tsx +33 -0
  165. package/src/patterns/DataCardWithSkeleton.stories.tsx +48 -0
  166. package/src/patterns/DisabledButtonTooltip.stories.tsx +30 -0
  167. package/src/patterns/FilterToolbar.stories.tsx +71 -0
  168. package/src/patterns/InfoTooltip.stories.tsx +56 -0
  169. package/src/patterns/ListDetail.stories.tsx +65 -0
  170. package/src/patterns/ModalForm.stories.tsx +59 -0
  171. package/src/patterns/PageHeaderTabs.stories.tsx +48 -0
  172. package/src/styles.css +16 -8
  173. package/src/utils/legacyIcon.tsx +28 -0
@@ -1,5 +1,13 @@
1
- import type { ComponentProps } from 'react';
1
+ import { type ComponentProps, type ReactNode, useMemo, useState } from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { action } from 'storybook/actions';
4
+
5
+ import { EmptyState } from '../../blocks/EmptyState';
6
+ import { ErrorState } from '../../blocks/ErrorState';
7
+ import { SearchInput } from '../../blocks/SearchInput';
8
+ import { Button } from '../../components/Button';
9
+ import { Icon } from '../../components/Icon';
10
+ import { Pagination } from '../../components/Pagination';
3
11
 
4
12
  import { Table } from './Table';
5
13
 
@@ -104,3 +112,217 @@ export const Overflow: Story = {
104
112
  export const Bordered: Story = {
105
113
  render: (props) => <Component {...props} bordered />,
106
114
  };
115
+
116
+ type Token = { id: string; name: string; createdAt: string; lastUsed: string };
117
+
118
+ const pad2 = (n: number) => String(n).padStart(2, '0');
119
+
120
+ const tokens: Token[] = Array.from({ length: 23 }, (_, i) => ({
121
+ id: `tok_${String(i + 1).padStart(3, '0')}`,
122
+ name: `Token ${i + 1}${i % 3 === 0 ? ' (production)' : i % 3 === 1 ? ' (staging)' : ''}`,
123
+ createdAt: `2026-01-${pad2((i % 28) + 1)}`,
124
+ lastUsed: `2026-05-${pad2((i % 28) + 1)}`,
125
+ }));
126
+
127
+ type SortKey = keyof Token;
128
+ type SortDir = 'asc' | 'desc';
129
+ const PAGE_SIZE = 5;
130
+
131
+ const sortAttr = (active: boolean, direction: SortDir) =>
132
+ !active ? 'none' : direction === 'asc' ? 'ascending' : 'descending';
133
+
134
+ const SortHeader = ({
135
+ active,
136
+ direction,
137
+ onClick,
138
+ children,
139
+ }: {
140
+ active: boolean;
141
+ direction: SortDir;
142
+ onClick: () => void;
143
+ children: ReactNode;
144
+ }) => (
145
+ <button
146
+ type="button"
147
+ onClick={onClick}
148
+ className="tw-group tw-flex tw-cursor-pointer tw-items-center tw-gap-1 tw-border-0 tw-bg-transparent tw-p-0 tw-font-medium tw-text-inherit"
149
+ >
150
+ {children}
151
+ <Icon
152
+ icon={!active ? 'sort' : direction === 'asc' ? 'angle-up' : 'angle-down'}
153
+ className="tw-size-3 tw-text-muted-foreground"
154
+ />
155
+ </button>
156
+ );
157
+
158
+ /**
159
+ * Canonical entity-list pattern: `Table` (presentational) composed with `SearchInput`, `Pagination`,
160
+ * and `EmptyState` / `ErrorState`. State (sort / name search / page) is plain React `useState` —
161
+ * scales up to TanStack Table's headless model (`@tanstack/react-table`,
162
+ * https://tanstack.com/table/latest) when the consumer needs server-side pagination, multi-column
163
+ * sort, row selection, or a true global filter that scans every column.
164
+ *
165
+ * Pattern reference: TanStack "headless UI" — the state library is separable from the presentational
166
+ * layer (https://tanstack.com/table/latest/docs/introduction#what-is-headless-ui).
167
+ */
168
+ export const AsEntityList: Story = {
169
+ name: 'As entity list',
170
+ parameters: { layout: 'padded' },
171
+ render: () => {
172
+ const [query, setQuery] = useState('');
173
+ const [sortKey, setSortKey] = useState<SortKey>('name');
174
+ const [sortDir, setSortDir] = useState<SortDir>('asc');
175
+ const [page, setPage] = useState(0);
176
+
177
+ const filtered = useMemo(() => {
178
+ const q = query.trim().toLowerCase();
179
+ const matched = q ? tokens.filter((t) => t.name.toLowerCase().includes(q)) : tokens;
180
+ return [...matched].sort((a, b) => {
181
+ const av = a[sortKey];
182
+ const bv = b[sortKey];
183
+ return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
184
+ });
185
+ }, [query, sortKey, sortDir]);
186
+
187
+ const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
188
+ const safePage = Math.min(page, pageCount - 1);
189
+ const pageRows = filtered.slice(safePage * PAGE_SIZE, safePage * PAGE_SIZE + PAGE_SIZE);
190
+
191
+ const toggleSort = (key: SortKey) => {
192
+ if (key === sortKey) {
193
+ setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
194
+ } else {
195
+ setSortKey(key);
196
+ setSortDir('asc');
197
+ }
198
+ setPage(0);
199
+ };
200
+
201
+ return (
202
+ <div className="tw-flex tw-w-[720px] tw-flex-col tw-gap-4">
203
+ <SearchInput
204
+ allowClear
205
+ placeholder="Search tokens…"
206
+ value={query}
207
+ onValueChange={(v) => {
208
+ setQuery(v);
209
+ setPage(0);
210
+ }}
211
+ className="tw-w-72"
212
+ />
213
+ {filtered.length === 0 ? (
214
+ <EmptyState>
215
+ <EmptyState.Icon>
216
+ <Icon icon="search" />
217
+ </EmptyState.Icon>
218
+ <EmptyState.Title>No tokens match your search</EmptyState.Title>
219
+ <EmptyState.Description>
220
+ Try a different query or clear the search to see all tokens.
221
+ </EmptyState.Description>
222
+ <EmptyState.Actions>
223
+ <Button variant="outline" onClick={() => setQuery('')}>
224
+ Clear search
225
+ </Button>
226
+ </EmptyState.Actions>
227
+ </EmptyState>
228
+ ) : (
229
+ <>
230
+ <Table bordered>
231
+ <Table.Header>
232
+ <Table.Row>
233
+ <Table.Head aria-sort={sortAttr(sortKey === 'name', sortDir)}>
234
+ <SortHeader
235
+ active={sortKey === 'name'}
236
+ direction={sortDir}
237
+ onClick={() => toggleSort('name')}
238
+ >
239
+ Name
240
+ </SortHeader>
241
+ </Table.Head>
242
+ <Table.Head aria-sort={sortAttr(sortKey === 'createdAt', sortDir)}>
243
+ <SortHeader
244
+ active={sortKey === 'createdAt'}
245
+ direction={sortDir}
246
+ onClick={() => toggleSort('createdAt')}
247
+ >
248
+ Created
249
+ </SortHeader>
250
+ </Table.Head>
251
+ <Table.Head aria-sort={sortAttr(sortKey === 'lastUsed', sortDir)}>
252
+ <SortHeader
253
+ active={sortKey === 'lastUsed'}
254
+ direction={sortDir}
255
+ onClick={() => toggleSort('lastUsed')}
256
+ >
257
+ Last used
258
+ </SortHeader>
259
+ </Table.Head>
260
+ </Table.Row>
261
+ </Table.Header>
262
+ <Table.Body>
263
+ {pageRows.map((token) => (
264
+ <Table.Row key={token.id}>
265
+ <Table.Cell>{token.name}</Table.Cell>
266
+ <Table.Cell>{token.createdAt}</Table.Cell>
267
+ <Table.Cell>{token.lastUsed}</Table.Cell>
268
+ </Table.Row>
269
+ ))}
270
+ </Table.Body>
271
+ </Table>
272
+ <Pagination>
273
+ <Pagination.Content>
274
+ <Pagination.Item>
275
+ <Pagination.Previous
276
+ onClick={() => setPage((p) => Math.max(0, p - 1))}
277
+ disabled={safePage === 0}
278
+ aria-disabled={safePage === 0}
279
+ />
280
+ </Pagination.Item>
281
+ {Array.from({ length: pageCount }, (_, i) => (
282
+ <Pagination.Item key={i}>
283
+ <Pagination.Button active={i === safePage} onClick={() => setPage(i)}>
284
+ {i + 1}
285
+ </Pagination.Button>
286
+ </Pagination.Item>
287
+ ))}
288
+ <Pagination.Item>
289
+ <Pagination.Next
290
+ onClick={() => setPage((p) => Math.min(pageCount - 1, p + 1))}
291
+ disabled={safePage === pageCount - 1}
292
+ aria-disabled={safePage === pageCount - 1}
293
+ />
294
+ </Pagination.Item>
295
+ </Pagination.Content>
296
+ </Pagination>
297
+ </>
298
+ )}
299
+ </div>
300
+ );
301
+ },
302
+ };
303
+
304
+ /**
305
+ * Failure surface for the entity-list pattern. Swap the table body for `ErrorState` when the
306
+ * underlying fetch errors. Mirrors the `AsEntityList` story shape so consumers can copy/paste either.
307
+ */
308
+ export const AsEntityListError: Story = {
309
+ name: 'As entity list — error',
310
+ parameters: { layout: 'padded' },
311
+ render: () => (
312
+ <div className="tw-flex tw-w-[720px] tw-flex-col tw-gap-4">
313
+ <SearchInput allowClear placeholder="Search tokens…" disabled className="tw-w-72" />
314
+ <ErrorState>
315
+ <ErrorState.Icon>
316
+ <Icon icon="circle-exclamation" />
317
+ </ErrorState.Icon>
318
+ <ErrorState.Title>Couldn&apos;t load tokens</ErrorState.Title>
319
+ <ErrorState.Description>
320
+ The request to load tokens failed. Check your connection and try again.
321
+ </ErrorState.Description>
322
+ <ErrorState.Actions>
323
+ <Button onClick={action('retry')}>Retry</Button>
324
+ </ErrorState.Actions>
325
+ </ErrorState>
326
+ </div>
327
+ ),
328
+ };
@@ -9,12 +9,29 @@ import { TableHead } from './TableHead';
9
9
  import { TableHeader } from './TableHeader';
10
10
  import { TableRow } from './TableRow';
11
11
 
12
+ /**
13
+ * Props for `Table`. `borderCollapsed` toggles standard collapsed borders vs separate row spacing;
14
+ * `bordered` wraps the table in a rounded bordered container with overflow scrolling.
15
+ */
12
16
  type Props = {
13
17
  borderCollapsed?: boolean;
14
18
  bordered?: boolean;
15
19
  containerClassName?: string;
16
20
  } & ComponentProps<'table'>;
17
21
 
22
+ /**
23
+ * Tabular data surface with header / body / row / cell compound parts.
24
+ *
25
+ * **When to use:** tabular data with named columns — entity lists with shared schema (jobs, buckets,
26
+ * tables), sortable rows, row-level actions. Compose `Table.Header` + `Table.Body` + `Table.Row` +
27
+ * `Table.Head` / `Table.Cell`.
28
+ * **When NOT to use:** for heterogeneous lists where each row has its own shape use a `Card` list;
29
+ * for tree / hierarchy layouts use a custom recursive list with `Table` per level.
30
+ * **Composition:** wrap in `PageLayout.Content`. For sortable / filterable behaviour bring your own
31
+ * state library (TanStack Table is a common pairing); this component is presentational.
32
+ *
33
+ * @see RECIPES.md#list-detail — for a list-table + row-click-to-detail pattern
34
+ */
18
35
  export const Table = ({
19
36
  borderCollapsed = true,
20
37
  bordered = false,
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import * as RadixTabs from '@radix-ui/react-tabs';
3
3
 
4
+ import { X } from '../icons';
4
5
  import { cn } from '../utils';
5
6
 
6
7
  import { Button } from './Button';
@@ -18,12 +19,29 @@ const TRIGGER_COMMON = {
18
19
  'hover:enabled:tw-text-secondary-500 hover:enabled:tw-border-secondary-500 hover:enabled:tw-cursor-pointer',
19
20
  };
20
21
 
22
+ /**
23
+ * Tab trigger descriptor. `value` keys the tab; `title` is the visible label; `onClose` makes the tab
24
+ * closable (renders an x button on hover). Generic over `T` so consumers can use string-literal unions.
25
+ */
21
26
  type Trigger<T extends string> = {
22
27
  value: T;
23
28
  title: ReactNode;
29
+ /** When set, renders an inline close button on the trigger. Called with the tab's `value`. */
24
30
  onClose?: (value: T) => void;
25
31
  } & Omit<RadixTabs.TabsTriggerProps, 'title'>;
26
32
 
33
+ /**
34
+ * Tabbed navigation with a single content area. Built on Radix Tabs.
35
+ *
36
+ * **When to use:** switching between sibling views of the same context (overview / runs / settings on a
37
+ * detail page; sections inside a modal body). The triggers row is horizontally scrollable on overflow.
38
+ * **When NOT to use:** for top-level routing prefer `PageHeader` `headerNav` slot or `PageLayout.Tabs`;
39
+ * for accordion-style disclosure use `Collapsible`.
40
+ * **Composition:** pass `triggers` as the declarative list and one or more `TabsContent` children keyed
41
+ * by `value`. Use `compact` for dense in-modal tabs.
42
+ *
43
+ * @see RECIPES.md#page-header-tabs — for the page-header + routed tabs pattern
44
+ */
27
45
  const Tabs = <T extends string>({
28
46
  triggers,
29
47
  compact = false,
@@ -76,7 +94,6 @@ const Tabs = <T extends string>({
76
94
  <div>
77
95
  {title}
78
96
  <Button
79
- icon="xmark"
80
97
  variant="inline"
81
98
  size="icon-3xs"
82
99
  disabled={rest.disabled}
@@ -85,7 +102,9 @@ const Tabs = <T extends string>({
85
102
  onMouseDown={(e) => e.preventDefault()}
86
103
  className="!tw-size-8 tw-opacity-0 tw-duration-200 group-hover/tab-trigger:tw-opacity-100"
87
104
  aria-label="Close tab"
88
- />
105
+ >
106
+ <X className="tw-size-4" />
107
+ </Button>
89
108
  </div>
90
109
  </RadixTabs.Trigger>
91
110
  );
@@ -104,6 +123,10 @@ const Tabs = <T extends string>({
104
123
  );
105
124
  };
106
125
 
126
+ /**
127
+ * Content panel for a single tab. Matched to a `Tabs` trigger by `value`. Receives `tabIndex={-1}` so the
128
+ * panel is focusable for screen readers without being a tab stop.
129
+ */
107
130
  const TabsContent = (props: RadixTabs.TabsContentProps) => (
108
131
  <RadixTabs.Content tabIndex={-1} {...props} />
109
132
  );
@@ -1,7 +1,7 @@
1
1
  import type { ComponentProps, ElementRef, Ref } from 'react';
2
2
 
3
+ import { X } from '../../icons';
3
4
  import { cn } from '../../utils';
4
- import { Icon } from '../Icon';
5
5
 
6
6
  type Props = ComponentProps<'button'>;
7
7
 
@@ -21,7 +21,7 @@ export const ToastDismiss = ({
21
21
  'tailwindcss-preflight',
22
22
  '[&_svg]:tw-aspect-square',
23
23
  '[&_svg]:tw-size-4',
24
- '[&_svg]:tw-p-1',
24
+ '[&_svg]:tw-m-1',
25
25
 
26
26
  'tw-text-white',
27
27
  'group-data-[variant=default]/toast:[&_svg]:tw-text-popover-foreground',
@@ -30,7 +30,7 @@ export const ToastDismiss = ({
30
30
  type="button"
31
31
  {...rest}
32
32
  >
33
- <Icon icon="times" />
33
+ <X />
34
34
  <span className="tw-sr-only">Dismiss</span>
35
35
  </button>
36
36
  );
@@ -1,10 +1,11 @@
1
- import type { ComponentProps, MouseEventHandler, ReactNode } from 'react';
1
+ import type { MouseEventHandler, ReactNode } from 'react';
2
2
  import type { VariantProps } from 'class-variance-authority';
3
3
  import { cva } from 'class-variance-authority';
4
4
  import { type Action, toast as sonnerToast } from 'sonner';
5
5
 
6
+ import { AlertCircle, AlertTriangle, CircleCheck, Info, Loader2 } from '../../icons';
6
7
  import { cn } from '../../utils';
7
- import { Icon } from '../Icon';
8
+ import { renderLegacyIcon } from '../../utils/legacyIcon';
8
9
 
9
10
  import { ToastAction } from './ToastAction';
10
11
  import { ToastDescription } from './ToastDescription';
@@ -55,12 +56,11 @@ const variants = cva(
55
56
  type Variants = VariantProps<typeof variants>;
56
57
  type Variant = NonNullable<Variants['variant']>;
57
58
  type VariantIcon = Exclude<Variant, 'default'>;
58
- type Icon = ComponentProps<typeof Icon>['icon'];
59
59
 
60
60
  type Props = {
61
61
  id: string | number;
62
62
  title: ReactNode;
63
- icon?: Icon;
63
+ icon?: ReactNode;
64
64
  description?: ReactNode;
65
65
  action?: Action;
66
66
  loading?: boolean;
@@ -68,11 +68,11 @@ type Props = {
68
68
  // offset?: number;
69
69
  } & Variants;
70
70
 
71
- const DEFAULT_VARIANT_ICON: Record<VariantIcon, ComponentProps<typeof Icon>['icon']> = {
72
- error: 'exclamation-triangle',
73
- info: 'info-circle',
74
- success: 'check-circle',
75
- warning: 'exclamation-circle',
71
+ const DEFAULT_VARIANT_ICON: Record<VariantIcon, ReactNode> = {
72
+ error: <AlertTriangle />,
73
+ info: <Info />,
74
+ success: <CircleCheck />,
75
+ warning: <AlertCircle />,
76
76
  };
77
77
 
78
78
  export const ToastRoot = ({
@@ -85,9 +85,9 @@ export const ToastRoot = ({
85
85
  loading = false,
86
86
  icon: iconProp,
87
87
  }: Props) => {
88
- const icon: Icon | null = (() => {
89
- if (loading) return 'spinner';
90
- if (iconProp) return iconProp;
88
+ const icon: ReactNode = (() => {
89
+ if (loading) return <Loader2 className="tw-animate-spin" />;
90
+ if (iconProp) return renderLegacyIcon(iconProp);
91
91
  if (variant === 'default' || variant === null) return null;
92
92
  return DEFAULT_VARIANT_ICON[variant];
93
93
  })();
@@ -100,20 +100,25 @@ export const ToastRoot = ({
100
100
  return (
101
101
  <div data-variant={variant} className={cn(variants({ variant }))}>
102
102
  {icon && (
103
- <Icon
103
+ <span
104
104
  data-slot="toast-icon"
105
- spin={loading}
106
105
  className={cn(
107
- // styles
106
+ 'tw-inline-flex',
107
+ 'tw-items-center',
108
+ 'tw-justify-center',
109
+ // styles — container at 20px with no padding so the 20px Lucide
110
+ // glyph fills the slot (previous tw-size-4 + tw-p-1 clipped a
111
+ // 16px child into 8px of visible space)
108
112
  'tw-aspect-square',
109
- 'tw-size-4',
113
+ 'tw-size-5',
110
114
  'tw-shrink-0',
111
- 'tw-p-1',
115
+ '[&_svg]:tw-size-5',
112
116
  'tw-text-white',
113
117
  'group-data-[variant=default]/toast:tw-text-popover-foreground',
114
118
  )}
115
- icon={icon}
116
- />
119
+ >
120
+ {icon}
121
+ </span>
117
122
  )}
118
123
 
119
124
  <ToastTitle>{title}</ToastTitle>
@@ -3,7 +3,7 @@ import { Root } from '@radix-ui/react-toggle';
3
3
  import { cva, type VariantProps } from 'class-variance-authority';
4
4
 
5
5
  import { cn } from '../../utils';
6
- import { Icon } from '../Icon';
6
+ import { renderLegacyIcon } from '../../utils/legacyIcon';
7
7
 
8
8
  const toggleVariants = cva(
9
9
  [
@@ -72,9 +72,12 @@ const toggleVariants = cva(
72
72
  'hover:aria-pressed:tw-from-secondary-700',
73
73
  'hover:aria-pressed:tw-to-purple-700',
74
74
 
75
- // icon color - gradient when off, white when on
76
- '[&_path]:tw-fill-aigradient-600',
77
- '[&_path]:aria-pressed:tw-fill-white',
75
+ // icon color - gradient stroke when off, white stroke when on.
76
+ // Lucide icons are stroke-based; targeting every shape (`*`)
77
+ // catches paths, circles, and other non-path sub-shapes so the
78
+ // whole glyph picks up the gradient.
79
+ '[&_*]:tw-stroke-aigradient-600',
80
+ '[&_*]:aria-pressed:tw-stroke-white',
78
81
  '*:aria-pressed:tw-text-white',
79
82
  ],
80
83
  'ai-vivid': [
@@ -150,7 +153,7 @@ const toggleVariants = cva(
150
153
  );
151
154
 
152
155
  type Props = {
153
- icon?: ComponentProps<typeof Icon>['icon'];
156
+ icon?: ReactNode;
154
157
  badge?: ReactNode;
155
158
  } & ComponentProps<typeof Root> &
156
159
  Omit<VariantProps<typeof toggleVariants>, 'iconOnly'>;
@@ -182,7 +185,9 @@ export const Toggle = ({
182
185
  </span>
183
186
  )}
184
187
  {icon && (
185
- <Icon icon={icon} className="tw-inline-block tw-size-4 tw-shrink-0 tw-align-middle" />
188
+ <span className="tw-inline-flex tw-size-4 tw-shrink-0 tw-items-center tw-justify-center tw-align-middle [&_svg]:tw-size-4">
189
+ {renderLegacyIcon(icon)}
190
+ </span>
186
191
  )}
187
192
 
188
193
  {isAiVariant ? (
@@ -15,19 +15,42 @@ type TriggerProps = {
15
15
  triggerAsChild?: boolean;
16
16
  };
17
17
 
18
+ /**
19
+ * Props for `Tooltip`. `tooltip` is the popup content; `children` is the trigger element.
20
+ * `variant` auto-resolves to `"explanatory"` for strings >30 chars, else `"action"`.
21
+ */
18
22
  export type Props = {
23
+ /** Keep the tooltip visible when the user clicks the trigger (useful for click-to-copy feedback). */
19
24
  keepOpenOnTriggerClick?: boolean;
25
+ /** Content rendered inside the tooltip. Plain text only — no interactive elements (use `HoverCard` or `Popover` for that). */
20
26
  tooltip: ReactNode;
21
27
  children: ReactNode;
22
28
  placement?: ComponentProps<typeof Content>['side'];
29
+ /** Forcibly hide the tooltip regardless of hover state. */
23
30
  forceHide?: boolean;
31
+ /** Forcibly show the tooltip regardless of hover state. Useful for guided-tour overlays. */
24
32
  forceShow?: boolean;
25
33
  className?: string;
26
34
  } & TriggerProps &
27
35
  Pick<ComponentProps<typeof TooltipContent>, 'variant' | 'className'>;
28
36
 
37
+ /** Re-export of Radix `Tooltip.Provider`. Mount once at the app root so tooltips share delay state. */
29
38
  export const TooltipProvider = Provider;
30
39
 
40
+ /**
41
+ * Short plain-text label that appears on hover or focus.
42
+ *
43
+ * **When to use:** describing an icon-only `Button`, explaining a disabled control, or labelling a
44
+ * truncated value. Plain text only — auto-switches to the longer `"explanatory"` variant when the string
45
+ * exceeds 30 characters.
46
+ * **When NOT to use:** for rich preview content (multiple lines, formatted text, structured data) use
47
+ * `HoverCard`; for tooltips containing links or buttons use `Popover` (interactive elements break a11y
48
+ * inside a `Tooltip`).
49
+ * **Composition:** wrap any focusable element via `triggerAsChild` so the tooltip targets the underlying
50
+ * native element. Mount `TooltipProvider` once at the app root.
51
+ *
52
+ * @see RECIPES.md#disabled-button-tooltip — for the disabled-button + tooltip pattern
53
+ */
31
54
  export const Tooltip = ({
32
55
  tooltip,
33
56
  children,
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
 
3
3
  import { ClipboardButton } from '../../blocks/ClipboardButton';
4
+ import { Maximize2, Minimize2 } from '../../icons';
4
5
  import { cn } from '../../utils';
5
6
  import { Button } from '../Button';
6
7
  import { Tooltip } from '../Tooltip';
@@ -159,15 +160,16 @@ export const ZoomableSvg = ({ html, copyText, className }: Props) => {
159
160
  <Button
160
161
  size="icon-sm"
161
162
  variant="outline"
162
- icon={
163
- isFullscreen
164
- ? 'down-left-and-up-right-to-center'
165
- : 'up-right-and-down-left-from-center'
166
- }
167
163
  aria-label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
168
164
  onClick={() => setIsFullscreen((f) => !f)}
169
165
  argo
170
- />
166
+ >
167
+ {isFullscreen ? (
168
+ <Minimize2 className="tw-size-4" />
169
+ ) : (
170
+ <Maximize2 className="tw-size-4" />
171
+ )}
172
+ </Button>
171
173
  </Tooltip>
172
174
  </div>
173
175
  </div>
@@ -0,0 +1,28 @@
1
+ import { forwardRef } from 'react';
2
+ import type { LucideIcon, LucideProps } from 'lucide-react';
3
+
4
+ // `forwardRef` here is required for type compatibility with `LucideIcon`
5
+ // (a `ForwardRefExoticComponent` from lucide-react). React 19's ref-as-prop
6
+ // pattern produces a plain function component which isn't structurally
7
+ // assignable to `LucideIcon`, so consumers couldn't pass `Keboola`
8
+ // interchangeably with imported Lucide icons (e.g. `<Button><Keboola className="tw-size-4" /></Button>`).
9
+ // oxlint-disable-next-line react-x/no-forward-ref
10
+ export const Keboola: LucideIcon = forwardRef<SVGSVGElement, LucideProps>(
11
+ ({ size = 24, color = 'currentColor', className, children, ...rest }, ref) => (
12
+ <svg
13
+ ref={ref}
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ width={size}
16
+ height={size}
17
+ viewBox="0 0 300 512"
18
+ fill={color}
19
+ className={className}
20
+ {...rest}
21
+ >
22
+ <path d="M152 320C79.7 320 24.5 233.9 24.5 124.7C24.5 15.6 70.5 0 152 0s123.6 15.6 123.6 124.7C275.5 233.9 217.7 320 152 320zm-8.7-50.5c0-11.2-9.5-20.3-21.2-20.3s-21.2 9.1-21.2 20.3c0 11.2 9.5 20.3 21.2 20.3s21.2-9.1 21.2-20.3zm55.7 0c0-11.2-9.5-20.3-21.2-20.3s-21.2 9.1-21.2 20.3c0 11.2 9.5 20.3 21.2 20.3s21.2-9.1 21.2-20.3zm68.9 115.1c10.2 9.3 20.2 20.5 29 35c5.9 9.7 2.6 22.1-7.5 27.8c-3.4 1.9-7.1 2.8-10.7 2.8c-7.2 0-14.3-3.6-18.3-10c-6.8-11.1-13.8-18.9-21.7-26.1c-7.3-6.7-15.6-12.7-24.9-19.5c-.7-.5-1.4-1-2.1-1.5c-5-3.6-10-6.8-14.9-9.4c.6 1 1.2 2.1 1.8 3.2c14.4 26.4 29.2 63.1 29.2 104.9c0 11.2-9.5 20.3-21.2 20.3c-11.7 0-21.2-9.1-21.2-20.3c.1-31.8-11.6-62.3-23.8-84.9c-4-7.4-8-13.9-11.6-19.3c-3.4 5.2-7.2 11.3-11 18.3c-12.5 22.7-24.5 53.8-24.4 86c0 11.2-9.5 20.3-21.2 20.3s-21.2-9.1-21.2-20.3c.1-41.8 14.9-78.4 29.2-104.9c.6-1.1 1.2-2.1 1.8-3.2c-5 2.7-9.9 5.8-14.9 9.4c-.6 .5-1.3 .9-1.9 1.4c-9.5 6.9-17.8 12.9-25.1 19.7c-7.9 7.2-14.9 15.1-21.7 26.1c-5.9 9.7-18.9 12.9-29 7.2C.5 441.8-2.9 429.3 3.1 419.7c8.9-14.4 18.8-25.7 29-35c9.2-8.4 18.5-15.1 27.6-21.7c1-.7 2-1.4 3-2.2c4.7-3.5 9.6-6.7 14.6-9.6c-5-2.9-9.8-6.1-14.6-9.6C39.8 325 19.9 302.5 3.1 275.1c-5.9-9.7-2.6-22.1 7.5-27.8c10.1-5.7 23.1-2.5 29 7.2c14.6 23.7 31.1 42 48.7 54.9c18.9 13.7 37.4 20.5 56.5 21.5c1.7-.1 3.5-.1 5.2-.1c1.8 0 3.5 .1 5.2 .1c0 0 0 0 0 0c19.1-1.1 37.6-7.9 56.5-21.5c17.6-12.8 34.2-31.2 48.7-54.9c6-9.7 18.9-12.9 29-7.2c10.1 5.7 13.4 18.1 7.5 27.8c-16.8 27.4-36.7 49.9-59.6 66.6c-4.7 3.5-9.6 6.7-14.5 9.6c4.9 2.9 9.8 6.1 14.5 9.6c.9 .7 1.8 1.3 2.7 2c9.2 6.7 18.5 13.5 27.9 22z" />
23
+ {children}
24
+ </svg>
25
+ ),
26
+ );
27
+
28
+ Keboola.displayName = 'Keboola';