@keboola/design 1.2.1 → 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 (177) hide show
  1. package/README.md +25 -0
  2. package/dist/icon-names.cjs +18 -0
  3. package/dist/icon-names.cjs.map +1 -0
  4. package/dist/icon-names.js +16 -0
  5. package/dist/icon-names.js.map +1 -0
  6. package/dist/icons.cjs +731 -0
  7. package/dist/icons.cjs.map +1 -0
  8. package/dist/icons.js +29 -0
  9. package/dist/icons.js.map +1 -0
  10. package/dist/index.cjs +14728 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.js +14553 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/markdown-editor.cjs +4440 -0
  15. package/dist/markdown-editor.cjs.map +1 -0
  16. package/dist/markdown-editor.js +4413 -0
  17. package/dist/markdown-editor.js.map +1 -0
  18. package/dist/mermaid.cjs +1011 -0
  19. package/dist/mermaid.cjs.map +1 -0
  20. package/dist/mermaid.js +1005 -0
  21. package/dist/mermaid.js.map +1 -0
  22. package/dist/styles.css +1 -0
  23. package/package.json +10 -3
  24. package/src/blocks/AIKit/CollapsibleStepHeader.tsx +7 -8
  25. package/src/blocks/AIKit/InlineStepLabel.tsx +7 -7
  26. package/src/blocks/AIKit/MessageBubble.stories.tsx +11 -3
  27. package/src/blocks/AIKit/MessageBubble.tsx +1 -1
  28. package/src/blocks/AIKit/PlanApproval.tsx +9 -8
  29. package/src/blocks/AIKit/PlanRewritingIndicator.tsx +2 -2
  30. package/src/blocks/AIKit/PromptInput.stories.tsx +10 -2
  31. package/src/blocks/AIKit/PromptInputSubmit.tsx +12 -8
  32. package/src/blocks/AIKit/Question.tsx +8 -9
  33. package/src/blocks/AIKit/SuggestedPrompt.tsx +12 -5
  34. package/src/blocks/AIKit/ThinkingIndicator.tsx +2 -2
  35. package/src/blocks/AutoSaveTextarea/AutoSaveTextarea.tsx +4 -4
  36. package/src/blocks/ClipboardButton/ClipboardButton.tsx +7 -5
  37. package/src/blocks/ClipboardButton/ClipboardInline.tsx +2 -3
  38. package/src/blocks/ColorInput/ColorInput.tsx +4 -2
  39. package/src/blocks/ColorInput/ColorPicker.tsx +5 -2
  40. package/src/blocks/EmptyState/EmptyState.tsx +16 -0
  41. package/src/blocks/ErrorState/ErrorState.tsx +15 -0
  42. package/src/blocks/Form/formHook.ts +15 -0
  43. package/src/blocks/MultiActionButtonGroup/MultiActionButton.stories.tsx +12 -11
  44. package/src/blocks/MultiActionButtonGroup/MultiActionButton.tsx +4 -4
  45. package/src/blocks/MultiCombobox/MultiCombobox.tsx +28 -12
  46. package/src/blocks/PageHeader/AGENTS.md +63 -3
  47. package/src/blocks/PageHeader/PageHeader.stories.tsx +124 -28
  48. package/src/blocks/PageHeader/PageHeader.tsx +15 -0
  49. package/src/blocks/PageHeader/PageHeaderDetail.tsx +23 -1
  50. package/src/blocks/PageHeader/PageHeaderIndex.tsx +20 -1
  51. package/src/blocks/PageLayout/PageLayout.tsx +26 -0
  52. package/src/blocks/PageToolbar/PageToolbar.stories.tsx +348 -0
  53. package/src/blocks/PageToolbar/PageToolbar.tsx +119 -0
  54. package/src/blocks/PageToolbar/index.ts +1 -0
  55. package/src/blocks/PasswordInput/PasswordInput.tsx +10 -10
  56. package/src/blocks/SearchInput/SearchInput.tsx +30 -4
  57. package/src/blocks/SearchInputWithNavigation/SearchInputWithNavigation.tsx +9 -6
  58. package/src/blocks/SplitButton/SplitButton.stories.tsx +7 -4
  59. package/src/blocks/SplitButton/SplitButton.tsx +17 -4
  60. package/src/blocks/Tagger/TagItem.tsx +4 -1
  61. package/src/blocks/Tagger/TagSelectPopover/TagSelectCreateItem.tsx +3 -1
  62. package/src/blocks/Tagger/TagSelectPopover/TagSelectList.tsx +2 -2
  63. package/src/blocks/Tagger/Tagger.tsx +2 -2
  64. package/src/components/Alert/Alert.stories.tsx +11 -3
  65. package/src/components/Alert/Alert.tsx +19 -1
  66. package/src/components/Alert/AlertCollapsibleIcon.tsx +3 -4
  67. package/src/components/Alert/AlertIcon.tsx +18 -21
  68. package/src/components/Badge/Badge.stories.tsx +22 -4
  69. package/src/components/Badge/Badge.tsx +16 -0
  70. package/src/components/Badge/BadgeButton.tsx +7 -6
  71. package/src/components/Breadcrumb/BreadcrumbEllipsis.tsx +2 -2
  72. package/src/components/Breadcrumb/BreadcrumbSeparator.tsx +2 -2
  73. package/src/components/Button/Button.stories.tsx +14 -2
  74. package/src/components/Button/Button.tsx +42 -5
  75. package/src/components/Button/styles.ts +4 -6
  76. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +32 -10
  77. package/src/components/Card/Card.tsx +17 -0
  78. package/src/components/Checkbox/Checkbox.tsx +3 -6
  79. package/src/components/CodeEditor/highlighting.ts +1 -1
  80. package/src/components/CollapsiblePanel.tsx +2 -5
  81. package/src/components/Combobox/Combobox.tsx +12 -14
  82. package/src/components/Combobox/ComboboxChips.tsx +6 -1
  83. package/src/components/Combobox/ComboboxItem.tsx +3 -4
  84. package/src/components/Combobox/ComboboxItemIndicator.tsx +6 -4
  85. package/src/components/Combobox/types.ts +2 -6
  86. package/src/components/Command/Command.stories.tsx +47 -56
  87. package/src/components/Command/CommandItem/CheckIndicator.tsx +26 -14
  88. package/src/components/Command/CommandItem/CommandItem.tsx +6 -3
  89. package/src/components/DropdownMenu/DropdownMenu.stories.tsx +8 -4
  90. package/src/components/DropdownMenu/DropdownMenuCheckboxItem.tsx +4 -1
  91. package/src/components/DropdownMenu/DropdownMenuItem.tsx +12 -6
  92. package/src/components/DropdownMenu/DropdownMenuItemIndicator.tsx +12 -6
  93. package/src/components/DropdownMenu/DropdownMenuRadioGroup/DropdownMenuRadioItem.tsx +8 -5
  94. package/src/components/DropdownMenu/DropdownMenuSub/DropdownMenuSubTrigger.tsx +2 -2
  95. package/src/components/DropdownMenu/ItemIcon.tsx +16 -8
  96. package/src/components/FormGroup/FormGroup.tsx +15 -0
  97. package/src/components/FullScreenModal/FullScreenModalHeader.tsx +4 -1
  98. package/src/components/Input/DateInput/DateInput.tsx +4 -1
  99. package/src/components/Input/DateRangeInput/DateRangeInput.tsx +4 -6
  100. package/src/components/Input/Input.tsx +18 -2
  101. package/src/components/Input/TextInput/TextInput.tsx +16 -0
  102. package/src/components/Input/TimeInput/TimeInput.tsx +4 -1
  103. package/src/components/InputGroup/InputGroup.stories.tsx +11 -3
  104. package/src/components/MarkdownEditor/MarkdownEditor.stories.tsx +63 -1
  105. package/src/components/MarkdownEditor/MarkdownEditor.tsx +105 -15
  106. package/src/components/MarkdownEditor/nodes/ImageNode.tsx +10 -10
  107. package/src/components/MarkdownEditor/nodes/MermaidNode.tsx +8 -8
  108. package/src/components/MarkdownEditor/plugins/KeyboardPlugin.tsx +47 -3
  109. package/src/components/MarkdownEditor/plugins/MarkdownSyncPlugin.tsx +12 -3
  110. package/src/components/MarkdownEditor/plugins/ToolbarPlugin.tsx +20 -10
  111. package/src/components/MarkdownEditor/plugins/toolbar/AlignmentDropdown.tsx +20 -14
  112. package/src/components/MarkdownEditor/plugins/toolbar/BlockTypeDropdown.tsx +25 -16
  113. package/src/components/MarkdownEditor/plugins/toolbar/LinkPanel.tsx +3 -3
  114. package/src/components/MarkdownEditor/transformers.ts +10 -0
  115. package/src/components/MergeEditor/AcceptControls/AcceptButton.tsx +9 -4
  116. package/src/components/MergeEditor/AcceptControls/AcceptControls.tsx +25 -14
  117. package/src/components/MergeEditor/ConflictStatus/ConflictStatusIndicator.tsx +2 -2
  118. package/src/components/MergeEditor/MergeEditor.stories.tsx +15 -0
  119. package/src/components/MergeEditor/MergeEditor.tsx +9 -0
  120. package/src/components/MergeEditor/MergePane.tsx +22 -4
  121. package/src/components/MergeEditor/decorations.ts +2 -2
  122. package/src/components/Modal/Modal.stories.tsx +6 -4
  123. package/src/components/Modal/Modal.tsx +26 -0
  124. package/src/components/Modal/ModalBody.tsx +5 -0
  125. package/src/components/Modal/ModalCloseIcon.tsx +2 -3
  126. package/src/components/Modal/ModalContent.tsx +8 -0
  127. package/src/components/Modal/ModalFooter.tsx +5 -0
  128. package/src/components/Modal/ModalHeader.tsx +11 -0
  129. package/src/components/Modal/ModalIcon.tsx +45 -19
  130. package/src/components/Navbar/Navbar.stories.tsx +26 -13
  131. package/src/components/Navbar/NavbarMenuButton.tsx +11 -10
  132. package/src/components/Pagination/PaginationEllipsis.tsx +2 -2
  133. package/src/components/Pagination/PaginationNext.tsx +2 -1
  134. package/src/components/Pagination/PaginationPrevious.tsx +4 -1
  135. package/src/components/PanelWithDetails.tsx +3 -4
  136. package/src/components/Popconfirm/Popconfirm.stories.tsx +13 -5
  137. package/src/components/RadioGroup/Radio.tsx +6 -12
  138. package/src/components/Search/ClearButton.tsx +2 -2
  139. package/src/components/Search/Search.tsx +3 -3
  140. package/src/components/Select/Select.tsx +16 -5
  141. package/src/components/Select/SelectContent.tsx +13 -4
  142. package/src/components/Select/SelectItem.tsx +6 -5
  143. package/src/components/Select/SelectTrigger.tsx +2 -2
  144. package/src/components/Sidebar/Sidebar.stories.tsx +55 -19
  145. package/src/components/Sidebar/SidebarGroup/SidebarGroupAction.tsx +8 -4
  146. package/src/components/Sidebar/SidebarMenu/SidebarMenuButton.tsx +4 -2
  147. package/src/components/Sidebar/SidebarMenu/SidebarMenuCard.tsx +3 -2
  148. package/src/components/Sidebar/SidebarMenu/SidebarMenuIcon.tsx +12 -12
  149. package/src/components/Sidebar/SidebarMenu/SidebarMenuSub.tsx +3 -7
  150. package/src/components/Sidebar/SidebarMenu/SidebarSearchInput.tsx +3 -5
  151. package/src/components/Skeleton/Skeleton.stories.tsx +52 -0
  152. package/src/components/Skeleton/Skeleton.tsx +24 -1
  153. package/src/components/Table/Table.stories.tsx +223 -1
  154. package/src/components/Table/Table.tsx +17 -0
  155. package/src/components/Tabs.tsx +25 -2
  156. package/src/components/Toast/ToastDismiss.tsx +3 -3
  157. package/src/components/Toast/ToastRoot.tsx +24 -19
  158. package/src/components/Toggle/Toggle.tsx +11 -6
  159. package/src/components/Tooltip/Tooltip.tsx +23 -0
  160. package/src/components/ZoomableSvg/ZoomableSvg.tsx +8 -6
  161. package/src/icons/Keboola.tsx +28 -0
  162. package/src/icons/icons.stories.tsx +174 -0
  163. package/src/icons/index.ts +180 -0
  164. package/src/index.ts +1 -0
  165. package/src/patterns/AlertWithAction.stories.tsx +30 -0
  166. package/src/patterns/CardWithEmptyState.stories.tsx +66 -0
  167. package/src/patterns/ConfirmDialog.stories.tsx +72 -0
  168. package/src/patterns/CreatableMultiCombobox.stories.tsx +33 -0
  169. package/src/patterns/DataCardWithSkeleton.stories.tsx +48 -0
  170. package/src/patterns/DisabledButtonTooltip.stories.tsx +30 -0
  171. package/src/patterns/FilterToolbar.stories.tsx +71 -0
  172. package/src/patterns/InfoTooltip.stories.tsx +56 -0
  173. package/src/patterns/ListDetail.stories.tsx +65 -0
  174. package/src/patterns/ModalForm.stories.tsx +59 -0
  175. package/src/patterns/PageHeaderTabs.stories.tsx +48 -0
  176. package/src/styles.css +16 -8
  177. package/src/utils/legacyIcon.tsx +28 -0
@@ -0,0 +1,348 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+
4
+ import { Badge } from '../../components/Badge';
5
+ import { Button } from '../../components/Button';
6
+ import { ButtonGroup } from '../../components/ButtonGroup';
7
+ import { Combobox } from '../../components/Combobox';
8
+ import { Icon } from '../../components/Icon';
9
+ import { PageLayout } from '../PageLayout';
10
+ import { SearchInput } from '../SearchInput';
11
+
12
+ import { PageToolbar } from './PageToolbar';
13
+
14
+ type Story = StoryObj<typeof PageToolbar>;
15
+
16
+ export default {
17
+ component: PageToolbar,
18
+ parameters: {
19
+ layout: 'fullscreen',
20
+ docs: {
21
+ description: {
22
+ component:
23
+ 'Two-row toolbar for index pages — `Search` left, `Filters` → `Sort` → `Actions` → `View` right, and an optional `ActiveFilters` row below for filter chips (Standard variant) or filter tokens (Query Builder variant). The block is variant-agnostic: callers compose chips or tokens as plain `ReactNode` children of `ActiveFilters`. See the [design-system pattern](https://github.com/evviku/design-system-docs/blob/main/docs/patterns/toolbar/toolbar.md) for the source spec.',
24
+ },
25
+ },
26
+ },
27
+ } satisfies Meta<typeof PageToolbar>;
28
+
29
+ const PlaceholderList = () => (
30
+ <div className="tw-flex tw-h-72 tw-items-center tw-justify-center tw-rounded tw-border tw-border-dashed tw-border-border tw-bg-muted/30 tw-text-sm tw-text-muted-foreground">
31
+ EntityList / Table
32
+ </div>
33
+ );
34
+
35
+ // Default — Main row only (Search left, Filters + Sort + View right). No active filters,
36
+ // so the Active Filters row is not rendered.
37
+ export const Default: Story = {
38
+ render: () => {
39
+ const [query, setQuery] = useState('');
40
+ const [status, setStatus] = useState<'all' | 'failed' | 'success' | null>(null);
41
+ const [sort, setSort] = useState<'newest' | 'oldest' | null>('newest');
42
+
43
+ return (
44
+ <PageLayout>
45
+ <PageToolbar>
46
+ <PageToolbar.Search>
47
+ <SearchInput
48
+ variant="argo"
49
+ sizing="medium"
50
+ allowClear
51
+ placeholder="Search jobs"
52
+ value={query}
53
+ onValueChange={setQuery}
54
+ />
55
+ </PageToolbar.Search>
56
+
57
+ <PageToolbar.Right>
58
+ <PageToolbar.Filters>
59
+ <Combobox
60
+ variant="argo"
61
+ sizing="medium"
62
+ allowClear
63
+ items={['all', 'failed', 'success'] as const}
64
+ value={status}
65
+ onValueChange={setStatus}
66
+ placeholder="Status"
67
+ className="tw-w-40"
68
+ />
69
+ </PageToolbar.Filters>
70
+ <PageToolbar.Sort>
71
+ <Combobox
72
+ variant="argo"
73
+ sizing="medium"
74
+ items={['newest', 'oldest'] as const}
75
+ value={sort}
76
+ onValueChange={setSort}
77
+ placeholder="Sort"
78
+ className="tw-w-32"
79
+ />
80
+ </PageToolbar.Sort>
81
+ </PageToolbar.Right>
82
+ </PageToolbar>
83
+
84
+ <PageLayout.Content>
85
+ <PlaceholderList />
86
+ </PageLayout.Content>
87
+ </PageLayout>
88
+ );
89
+ },
90
+ };
91
+
92
+ // Standard Filters variant — non-editable chips rendered into the Active Filters row,
93
+ // with a Clear action self-aligned to the far right. Uses `Badge` as a stand-in until
94
+ // the dedicated `FilterChip` primitive ships.
95
+ export const WithStandardFilters: Story = {
96
+ render: () => {
97
+ type Active = { id: string; label: string; value: string };
98
+ const [active, setActive] = useState<Active[]>([
99
+ { id: 'status', label: 'Status', value: 'Failed' },
100
+ { id: 'owner', label: 'Owner', value: 'John Doe' },
101
+ ]);
102
+
103
+ return (
104
+ <PageLayout>
105
+ <PageToolbar>
106
+ <PageToolbar.Search>
107
+ <SearchInput variant="argo" sizing="medium" allowClear placeholder="Search jobs" />
108
+ </PageToolbar.Search>
109
+
110
+ <PageToolbar.Right>
111
+ <PageToolbar.Filters>
112
+ <Combobox
113
+ variant="argo"
114
+ sizing="medium"
115
+ allowClear
116
+ items={['Failed', 'Success', 'Running'] as const}
117
+ placeholder="Status"
118
+ className="tw-w-36"
119
+ />
120
+ <Combobox
121
+ variant="argo"
122
+ sizing="medium"
123
+ allowClear
124
+ items={['John Doe', 'Jane Doe'] as const}
125
+ placeholder="Owner"
126
+ className="tw-w-40"
127
+ />
128
+ </PageToolbar.Filters>
129
+ </PageToolbar.Right>
130
+
131
+ {active.length > 0 && (
132
+ <PageToolbar.ActiveFilters>
133
+ {active.map((filter) => (
134
+ <Badge
135
+ key={filter.id}
136
+ variant="default"
137
+ color="gray"
138
+ suffix={
139
+ <button
140
+ type="button"
141
+ aria-label={`Remove ${filter.label} filter`}
142
+ onClick={() => setActive((prev) => prev.filter((f) => f.id !== filter.id))}
143
+ className="tw-flex tw-items-center"
144
+ >
145
+ <Icon icon="xmark" />
146
+ </button>
147
+ }
148
+ >
149
+ {filter.label}: {filter.value}
150
+ </Badge>
151
+ ))}
152
+ <PageToolbar.Clear onClick={() => setActive([])} />
153
+ </PageToolbar.ActiveFilters>
154
+ )}
155
+ </PageToolbar>
156
+
157
+ <PageLayout.Content>
158
+ <PlaceholderList />
159
+ </PageLayout.Content>
160
+ </PageLayout>
161
+ );
162
+ },
163
+ };
164
+
165
+ // Query Builder variant — editable tokens rendered into the Active Filters row, with
166
+ // `AddFilter` between the tokens and Clear. Uses `Badge` with a `chevron-down` suffix
167
+ // as a stand-in until the dedicated `FilterToken` primitive ships.
168
+ export const WithQueryBuilder: Story = {
169
+ render: () => {
170
+ type Token = { id: string; label: string; values: string[] };
171
+ const [tokens, setTokens] = useState<Token[]>([
172
+ { id: 'status', label: 'Status', values: ['Failed', 'Success'] },
173
+ { id: 'owner', label: 'Owner', values: ['John'] },
174
+ ]);
175
+
176
+ return (
177
+ <PageLayout>
178
+ <PageToolbar>
179
+ <PageToolbar.Search>
180
+ <SearchInput variant="argo" sizing="medium" allowClear placeholder="Search jobs" />
181
+ </PageToolbar.Search>
182
+
183
+ <PageToolbar.Right>
184
+ <PageToolbar.Filters>
185
+ <Button argo variant="outline">
186
+ <Icon icon="sliders" />
187
+ Filter
188
+ </Button>
189
+ </PageToolbar.Filters>
190
+ </PageToolbar.Right>
191
+
192
+ {tokens.length > 0 && (
193
+ <PageToolbar.ActiveFilters>
194
+ {tokens.map((token) => (
195
+ <Badge
196
+ key={token.id}
197
+ variant="default"
198
+ color="gray"
199
+ suffix={<Icon icon="chevron-down" />}
200
+ >
201
+ {token.label}: {token.values.join(', ')}
202
+ </Badge>
203
+ ))}
204
+ <PageToolbar.AddFilter onClick={() => {}} />
205
+ <PageToolbar.Clear onClick={() => setTokens([])} />
206
+ </PageToolbar.ActiveFilters>
207
+ )}
208
+ </PageToolbar>
209
+
210
+ <PageLayout.Content>
211
+ <PlaceholderList />
212
+ </PageLayout.Content>
213
+ </PageLayout>
214
+ );
215
+ },
216
+ };
217
+
218
+ // View Controls — list/grid toggle is **always** the last control group on the right.
219
+ // Never render anything after `PageToolbar.View`.
220
+ export const WithViewControls: Story = {
221
+ render: () => {
222
+ const [view, setView] = useState<'list' | 'grid'>('list');
223
+
224
+ return (
225
+ <PageLayout>
226
+ <PageToolbar>
227
+ <PageToolbar.Search>
228
+ <SearchInput variant="argo" sizing="medium" allowClear placeholder="Search items" />
229
+ </PageToolbar.Search>
230
+
231
+ <PageToolbar.Right>
232
+ <PageToolbar.Filters>
233
+ <Combobox
234
+ variant="argo"
235
+ sizing="medium"
236
+ allowClear
237
+ items={['Active', 'Archived'] as const}
238
+ placeholder="Status"
239
+ className="tw-w-36"
240
+ />
241
+ </PageToolbar.Filters>
242
+ <PageToolbar.Sort>
243
+ <Combobox
244
+ variant="argo"
245
+ sizing="medium"
246
+ items={['Newest', 'Oldest'] as const}
247
+ value="Newest"
248
+ placeholder="Sort"
249
+ className="tw-w-32"
250
+ />
251
+ </PageToolbar.Sort>
252
+ <PageToolbar.Actions>
253
+ <Button argo variant="outline">
254
+ <Icon icon="cloud-arrow-down" />
255
+ Export
256
+ </Button>
257
+ </PageToolbar.Actions>
258
+ <PageToolbar.View>
259
+ {/* Temporary view toggle until the dedicated `ToggleGroup` primitive ships.
260
+ Two plain outline icon buttons with `aria-pressed` for state; selected
261
+ styling lives on the consumer until the primitive lands. */}
262
+ <ButtonGroup space="extra-small">
263
+ <Button
264
+ argo
265
+ variant="outline"
266
+ size="icon"
267
+ onClick={() => setView('list')}
268
+ aria-label="List view"
269
+ aria-pressed={view === 'list'}
270
+ className={view === 'list' ? 'tw-bg-accent tw-text-accent-foreground' : undefined}
271
+ >
272
+ <Icon icon="table-rows" />
273
+ </Button>
274
+ <Button
275
+ argo
276
+ variant="outline"
277
+ size="icon"
278
+ onClick={() => setView('grid')}
279
+ aria-label="Grid view"
280
+ aria-pressed={view === 'grid'}
281
+ className={view === 'grid' ? 'tw-bg-accent tw-text-accent-foreground' : undefined}
282
+ >
283
+ <Icon icon="table-cells" />
284
+ </Button>
285
+ </ButtonGroup>
286
+ </PageToolbar.View>
287
+ </PageToolbar.Right>
288
+ </PageToolbar>
289
+
290
+ <PageLayout.Content>
291
+ <PlaceholderList />
292
+ </PageLayout.Content>
293
+ </PageLayout>
294
+ );
295
+ },
296
+ };
297
+
298
+ // Responsive — at narrow viewport the right cluster wraps below Search, the Active
299
+ // Filters row keeps chips on multiple lines, and Search stays visible. No horizontal
300
+ // scroll. Shown inside a 480px frame to simulate a small breakpoint.
301
+ export const Responsive: Story = {
302
+ parameters: {
303
+ layout: 'centered',
304
+ },
305
+ render: () => (
306
+ <div className="tw-w-[480px] tw-rounded tw-border tw-border-border tw-bg-background tw-p-4">
307
+ <PageToolbar>
308
+ <PageToolbar.Search>
309
+ <SearchInput variant="argo" sizing="medium" allowClear placeholder="Search jobs" />
310
+ </PageToolbar.Search>
311
+
312
+ <PageToolbar.Right>
313
+ <PageToolbar.Filters>
314
+ <Combobox
315
+ variant="argo"
316
+ sizing="medium"
317
+ allowClear
318
+ items={['Failed', 'Success'] as const}
319
+ placeholder="Status"
320
+ className="tw-w-32"
321
+ />
322
+ <Combobox
323
+ variant="argo"
324
+ sizing="medium"
325
+ allowClear
326
+ items={['John', 'Jane'] as const}
327
+ placeholder="Owner"
328
+ className="tw-w-32"
329
+ />
330
+ </PageToolbar.Filters>
331
+ </PageToolbar.Right>
332
+
333
+ <PageToolbar.ActiveFilters>
334
+ <Badge variant="default" color="gray" suffix={<Icon icon="xmark" />}>
335
+ Status: Failed
336
+ </Badge>
337
+ <Badge variant="default" color="gray" suffix={<Icon icon="xmark" />}>
338
+ Owner: John Doe
339
+ </Badge>
340
+ <Badge variant="default" color="gray" suffix={<Icon icon="xmark" />}>
341
+ Created by: Marketing team
342
+ </Badge>
343
+ <PageToolbar.Clear onClick={() => {}} />
344
+ </PageToolbar.ActiveFilters>
345
+ </PageToolbar>
346
+ </div>
347
+ ),
348
+ };
@@ -0,0 +1,119 @@
1
+ import type { ComponentPropsWithoutRef, ElementRef, Ref } from 'react';
2
+
3
+ import { Button } from '../../components/Button';
4
+ import { Icon } from '../../components/Icon';
5
+ import { cn } from '../../utils';
6
+
7
+ type DivProps = ComponentPropsWithoutRef<'div'> & { ref?: Ref<ElementRef<'div'>> };
8
+
9
+ const Root = ({ ref, className, children, ...props }: DivProps) => (
10
+ <div
11
+ ref={ref}
12
+ className={cn('tw-flex tw-flex-wrap tw-items-center tw-gap-3 tw-w-full', className)}
13
+ {...props}
14
+ >
15
+ {children}
16
+ </div>
17
+ );
18
+
19
+ const Search = ({ ref, className, children, ...props }: DivProps) => (
20
+ <div
21
+ ref={ref}
22
+ className={cn(
23
+ 'tw-mr-auto tw-flex tw-items-center tw-gap-2 tw-w-full sm:tw-w-auto sm:tw-min-w-64 sm:tw-max-w-md',
24
+ className,
25
+ )}
26
+ {...props}
27
+ >
28
+ {children}
29
+ </div>
30
+ );
31
+
32
+ const Right = ({ ref, className, children, ...props }: DivProps) => (
33
+ <div
34
+ ref={ref}
35
+ className={cn('tw-flex tw-flex-wrap tw-items-center tw-gap-2', className)}
36
+ {...props}
37
+ >
38
+ {children}
39
+ </div>
40
+ );
41
+
42
+ const ControlGroup = ({ ref, className, children, ...props }: DivProps) => (
43
+ <div ref={ref} className={cn('tw-flex tw-items-center tw-gap-2', className)} {...props}>
44
+ {children}
45
+ </div>
46
+ );
47
+
48
+ const ActiveFilters = ({ ref, className, children, ...props }: DivProps) => (
49
+ <div
50
+ ref={ref}
51
+ className={cn('tw-flex tw-flex-wrap tw-items-center tw-gap-2 tw-basis-full', className)}
52
+ {...props}
53
+ >
54
+ {children}
55
+ </div>
56
+ );
57
+
58
+ type ActionProps = ComponentPropsWithoutRef<typeof Button>;
59
+
60
+ const AddFilter = ({ className, children, ...props }: ActionProps) => (
61
+ <Button argo variant="invisible" size="small" className={cn('tw-gap-1.5', className)} {...props}>
62
+ <Icon icon="plus" />
63
+ {children ?? 'Add filter'}
64
+ </Button>
65
+ );
66
+
67
+ const Clear = ({ className, children, ...props }: ActionProps) => (
68
+ <Button argo variant="invisible" size="small" className={cn('tw-ml-auto', className)} {...props}>
69
+ {children ?? 'Clear all'}
70
+ </Button>
71
+ );
72
+
73
+ /**
74
+ * Two-row toolbar for index pages — search on the left, filter / sort / utility / view
75
+ * controls on the right, an optional Active Filters row below for filter chips or tokens.
76
+ *
77
+ * **When to use:** every index page that supports search or filtering. Hosts inside
78
+ * `PageLayout` as the search / filter row — replaces the older bare-`<div>` `PageLayout.Toolbar`.
79
+ * **When NOT to use:** detail pages, form pages, or anywhere there is no search / filter
80
+ * surface. For routing-based tab rows reach for `PageLayout.Tabs`.
81
+ *
82
+ * **Recommended composition (enforced at the call site):**
83
+ *
84
+ * - `Search` sits on the left; the right cluster auto-aligns via flex.
85
+ * - Right cluster order: `Filters` → `Sort` → `Actions` → `View`. View controls are always
86
+ * the final control group — do not render any control after `View`.
87
+ * - `ActiveFilters` is the second row. Render it only when one or more filters are active.
88
+ * - `Clear` self-aligns to the far right of `ActiveFilters`.
89
+ *
90
+ * The block is variant-agnostic: render filter chips (Standard variant, non-editable) or
91
+ * tokens (Query Builder variant, editable) into `ActiveFilters` as plain children — do not
92
+ * mix the two in one toolbar.
93
+ *
94
+ * @see https://github.com/evviku/design-system-docs/blob/main/docs/patterns/toolbar/toolbar.md
95
+ */
96
+ export const PageToolbar = Object.assign(Root, {
97
+ /** Left slot — typically a `SearchInput`. */
98
+ Search,
99
+ /** Right cluster — host the four control groups below in the recommended order. */
100
+ Right,
101
+ /** Filter controls (`Combobox`, `Select`, `DatePicker`). */
102
+ Filters: ControlGroup,
103
+ /** Sort control. */
104
+ Sort: ControlGroup,
105
+ /** Utility actions (Export, Refresh, …). */
106
+ Actions: ControlGroup,
107
+ /** View toggle (List / Grid). Always render last in the right cluster. */
108
+ View: ControlGroup,
109
+ /**
110
+ * Active filters row. Render only when one or more filters are active. Pass filter chips
111
+ * (Standard variant) or tokens (Query Builder variant) as children, optionally followed
112
+ * by `AddFilter` / `Clear`.
113
+ */
114
+ ActiveFilters,
115
+ /** "+ Add filter" action — typically rendered after the chips/tokens in `ActiveFilters`. */
116
+ AddFilter,
117
+ /** "Clear all" action — self-aligns to the far right of `ActiveFilters`. */
118
+ Clear,
119
+ });
@@ -0,0 +1 @@
1
+ export * from './PageToolbar';
@@ -9,8 +9,8 @@ import type {
9
9
  Ref,
10
10
  } from 'react';
11
11
 
12
- import { Icon } from '../../components/Icon';
13
12
  import { InputGroup } from '../../components/InputGroup';
13
+ import { ArrowBigUpDash, Eye, EyeOff, X } from '../../icons';
14
14
  import { createChangeInputEvent, mergeRefs } from '../../utils';
15
15
 
16
16
  type InputGroupProps = Pick<
@@ -151,27 +151,27 @@ export const PasswordInput = ({
151
151
  {(capsLock || canClear || allowPasswordToggle) && (
152
152
  <InputGroup.Addon align="inline-end">
153
153
  {capsLock && (
154
- <Icon
155
- icon="up-from-line"
156
- title="Caps Lock is on"
157
- className="tw-size-4 tw-text-primary"
158
- />
154
+ <ArrowBigUpDash className="tw-size-4 tw-text-primary">
155
+ <title>Caps Lock is on</title>
156
+ </ArrowBigUpDash>
159
157
  )}
160
158
  {canClear && (
161
159
  <InputGroup.Button
162
- icon="xmark"
163
160
  aria-label="Clear password"
164
161
  onMouseDown={(event) => event.preventDefault()}
165
162
  onClick={handleClear}
166
- />
163
+ >
164
+ <X className="tw-size-4" />
165
+ </InputGroup.Button>
167
166
  )}
168
167
  {allowPasswordToggle && (
169
168
  <InputGroup.Button
170
- icon={isPasswordVisible ? 'eye-slash' : 'eye'}
171
169
  aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
172
170
  onMouseDown={(event) => event.preventDefault()}
173
171
  onClick={handleTogglePasswordVisibility}
174
- />
172
+ >
173
+ {isPasswordVisible ? <EyeOff className="tw-size-4" /> : <Eye className="tw-size-4" />}
174
+ </InputGroup.Button>
175
175
  )}
176
176
  </InputGroup.Addon>
177
177
  )}
@@ -1,8 +1,8 @@
1
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import type { ChangeEventHandler, ComponentProps, ElementRef, FocusEventHandler, Ref } from 'react';
3
3
 
4
- import { Icon } from '../../components/Icon';
5
4
  import { InputGroup } from '../../components/InputGroup';
5
+ import { Search, X } from '../../icons';
6
6
  import { debounce } from '../../utils';
7
7
 
8
8
  type InputGroupProps = Pick<
@@ -12,18 +12,43 @@ type InputGroupProps = Pick<
12
12
 
13
13
  type InputGroupInputProps = Omit<ComponentProps<typeof InputGroup.Input>, 'value' | 'defaultValue'>;
14
14
 
15
+ /**
16
+ * Props for `SearchInput`. `onValueChange` is debounced by default (300ms) so callers can wire it directly
17
+ * to expensive filter / fetch operations without `useMemo` plumbing. `value={null}` / `value={undefined}`
18
+ * is treated as "not provided" — the component falls back to `defaultValue` (or, if absent, internal
19
+ * state initialized to empty). `value={""}` is controlled-empty.
20
+ */
15
21
  type Props = InputGroupProps &
16
22
  InputGroupInputProps & {
17
23
  value?: string | null;
18
24
  defaultValue?: string | null;
25
+ /** Called with the latest string value after the debounce window (default 300ms). Fires synchronously on clear and when `disableDebounce` is set. For sync echo of every keystroke use `onChange`. */
19
26
  onValueChange?: (value: string) => void;
27
+ /** Called with the string value on blur. */
20
28
  onValueBlur?: (value: string) => void;
29
+ /** Called when the user clicks the clear button. Receives no arguments — the value is reset to "". */
21
30
  onClear?: () => void;
31
+ /** Render the trailing clear (x) button when there's a non-empty value. */
22
32
  allowClear?: boolean;
33
+ /** Bypass the debounce — `onValueChange` fires synchronously on every keystroke. */
23
34
  disableDebounce?: boolean;
35
+ /** Debounce window in ms. Default 300. Ignored when `disableDebounce` is set. */
24
36
  debounceDelay?: number;
25
37
  };
26
38
 
39
+ /**
40
+ * Debounced search input with a leading magnifying-glass icon and optional clear button.
41
+ *
42
+ * **When to use:** the search field inside `PageLayout.Toolbar`, list / table search, combobox-less
43
+ * free-text filters. `onValueChange` is debounced so callers can wire it directly to fetches without
44
+ * `useMemo` plumbing.
45
+ * **When NOT to use:** for a plain string input use `TextInput`; for selecting from a known enumerated
46
+ * list use `Combobox` (which has its own search built in); for free-form tag entry use `Tagger`.
47
+ * **Composition:** typically rendered in `PageLayout.Toolbar` next to `MultiCombobox` filter pills. Use
48
+ * `allowClear` for the canonical x-to-reset affordance.
49
+ *
50
+ * @see RECIPES.md#filter-toolbar — for the search + filter chips toolbar pattern
51
+ */
27
52
  export const SearchInput = ({
28
53
  ref,
29
54
 
@@ -118,7 +143,7 @@ export const SearchInput = ({
118
143
  className={className}
119
144
  >
120
145
  <InputGroup.Addon align="inline-start">
121
- <Icon icon="magnifying-glass" className="tw-text-muted-foreground" />
146
+ <Search className="tw-size-4 tw-text-muted-foreground" />
122
147
  </InputGroup.Addon>
123
148
 
124
149
  <InputGroup.Input
@@ -134,11 +159,12 @@ export const SearchInput = ({
134
159
  {canClear && (
135
160
  <InputGroup.Addon align="inline-end">
136
161
  <InputGroup.Button
137
- icon="xmark"
138
162
  aria-label="Clear search"
139
163
  onMouseDown={(event) => event.preventDefault()}
140
164
  onClick={handleClear}
141
- />
165
+ >
166
+ <X className="tw-size-4" />
167
+ </InputGroup.Button>
142
168
  </InputGroup.Addon>
143
169
  )}
144
170
  </InputGroup>
@@ -1,8 +1,9 @@
1
1
  import type { ComponentProps, KeyboardEvent, Ref } from 'react';
2
2
 
3
- import { Button, ButtonGroup, cn, Icon, Separator, TextInput } from '@keboola/design';
3
+ import { Button, ButtonGroup, cn, Separator, TextInput } from '@keboola/design';
4
4
 
5
5
  import { keyCodes } from '../../constants/keyCodes';
6
+ import { ChevronDown, ChevronUp, Search } from '../../icons';
6
7
 
7
8
  type SearchInputWithNavigationProps = {
8
9
  currentIndex: number | null;
@@ -51,7 +52,7 @@ export const SearchInputWithNavigation = ({
51
52
  placeholder={placeholder}
52
53
  onValueChange={onValueChange}
53
54
  onClear={onClear}
54
- prefix={<Icon className="tw-text-neutral-400" icon="magnifying-glass" />}
55
+ prefix={<Search className="tw-size-4 tw-text-neutral-400" />}
55
56
  focusVariant={focusVariant}
56
57
  onKeyDown={handleKeyDown}
57
58
  variant={variant}
@@ -72,15 +73,17 @@ export const SearchInputWithNavigation = ({
72
73
  size="icon-3xs"
73
74
  onClick={onNavigatePrevious}
74
75
  disabled={!totalResults}
75
- icon="angle-up"
76
- />
76
+ >
77
+ <ChevronUp className="tw-size-4" />
78
+ </Button>
77
79
  <Button
78
80
  variant="inline"
79
81
  size="icon-3xs"
80
82
  onClick={onNavigateNext}
81
83
  disabled={!totalResults}
82
- icon="angle-down"
83
- />
84
+ >
85
+ <ChevronDown className="tw-size-4" />
86
+ </Button>
84
87
  </ButtonGroup>
85
88
  </div>
86
89
  }
@@ -1,6 +1,8 @@
1
1
  import type { ComponentProps } from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
3
 
4
+ import { Pencil, Trash2 } from '@keboola/design/icons';
5
+
4
6
  import { Modal } from '../../components/Modal';
5
7
 
6
8
  import { SplitButton } from './SplitButton';
@@ -49,10 +51,11 @@ export const Default: Story = {
49
51
  {...args}
50
52
  actions={
51
53
  <SplitButton.Actions>
52
- <SplitButton.Action icon="trash">Action 1</SplitButton.Action>
53
- <SplitButton.Action icon="pen">Action 2</SplitButton.Action>
54
+ <SplitButton.Action>Action 1</SplitButton.Action>
55
+ <SplitButton.Action>Action 2</SplitButton.Action>
54
56
  </SplitButton.Actions>
55
57
  }
58
+ icon={<Pencil className="tw-size-4" />}
56
59
  >
57
60
  {args.children}
58
61
  </SplitButton>
@@ -70,8 +73,8 @@ export const PrimaryButtonAsTrigger: Story = {
70
73
  args: {
71
74
  actions: (
72
75
  <SplitButton.Actions>
73
- <SplitButton.Action icon="trash">Action 1</SplitButton.Action>
74
- <SplitButton.Action icon="pen">Action 2</SplitButton.Action>
76
+ <SplitButton.Action icon={<Trash2 />}>Action 1</SplitButton.Action>
77
+ <SplitButton.Action icon={<Pencil />}>Action 2</SplitButton.Action>
75
78
  </SplitButton.Actions>
76
79
  ),
77
80
  },