@keboola/design 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/icon-names.cjs +18 -0
- package/dist/icon-names.cjs.map +1 -0
- package/dist/icon-names.js +16 -0
- package/dist/icon-names.js.map +1 -0
- package/dist/icons.cjs +731 -0
- package/dist/icons.cjs.map +1 -0
- package/dist/icons.js +29 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.cjs +14728 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +14553 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown-editor.cjs +4440 -0
- package/dist/markdown-editor.cjs.map +1 -0
- package/dist/markdown-editor.js +4413 -0
- package/dist/markdown-editor.js.map +1 -0
- package/dist/mermaid.cjs +1011 -0
- package/dist/mermaid.cjs.map +1 -0
- package/dist/mermaid.js +1005 -0
- package/dist/mermaid.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +10 -3
- package/src/blocks/AIKit/CollapsibleStepHeader.tsx +7 -8
- package/src/blocks/AIKit/InlineStepLabel.tsx +7 -7
- package/src/blocks/AIKit/MessageBubble.stories.tsx +11 -3
- package/src/blocks/AIKit/MessageBubble.tsx +1 -1
- package/src/blocks/AIKit/PlanApproval.tsx +9 -8
- package/src/blocks/AIKit/PlanRewritingIndicator.tsx +2 -2
- package/src/blocks/AIKit/PromptInput.stories.tsx +10 -2
- package/src/blocks/AIKit/PromptInputSubmit.tsx +12 -8
- package/src/blocks/AIKit/Question.tsx +8 -9
- package/src/blocks/AIKit/SuggestedPrompt.tsx +12 -5
- package/src/blocks/AIKit/ThinkingIndicator.tsx +2 -2
- package/src/blocks/AutoSaveTextarea/AutoSaveTextarea.tsx +4 -4
- package/src/blocks/ClipboardButton/ClipboardButton.tsx +7 -5
- package/src/blocks/ClipboardButton/ClipboardInline.tsx +2 -3
- package/src/blocks/ColorInput/ColorInput.tsx +4 -2
- package/src/blocks/ColorInput/ColorPicker.tsx +5 -2
- package/src/blocks/EmptyState/EmptyState.tsx +16 -0
- package/src/blocks/ErrorState/ErrorState.tsx +15 -0
- package/src/blocks/Form/formHook.ts +15 -0
- package/src/blocks/MultiActionButtonGroup/MultiActionButton.stories.tsx +12 -11
- package/src/blocks/MultiActionButtonGroup/MultiActionButton.tsx +5 -4
- package/src/blocks/MultiCombobox/MultiCombobox.tsx +28 -12
- package/src/blocks/PageHeader/AGENTS.md +63 -3
- package/src/blocks/PageHeader/PageHeader.stories.tsx +124 -28
- package/src/blocks/PageHeader/PageHeader.tsx +15 -0
- package/src/blocks/PageHeader/PageHeaderDetail.tsx +23 -1
- package/src/blocks/PageHeader/PageHeaderIndex.tsx +20 -1
- package/src/blocks/PageLayout/PageLayout.tsx +26 -0
- package/src/blocks/PageToolbar/PageToolbar.stories.tsx +348 -0
- package/src/blocks/PageToolbar/PageToolbar.tsx +119 -0
- package/src/blocks/PageToolbar/index.ts +1 -0
- package/src/blocks/PasswordInput/PasswordInput.tsx +10 -10
- package/src/blocks/SearchInput/SearchInput.tsx +30 -4
- package/src/blocks/SearchInputWithNavigation/SearchInputWithNavigation.tsx +9 -6
- package/src/blocks/SplitButton/SplitButton.stories.tsx +7 -4
- package/src/blocks/SplitButton/SplitButton.tsx +17 -4
- package/src/blocks/Tagger/TagItem.tsx +4 -1
- package/src/blocks/Tagger/TagSelectPopover/TagSelectCreateItem.tsx +3 -1
- package/src/blocks/Tagger/TagSelectPopover/TagSelectList.tsx +2 -2
- package/src/blocks/Tagger/Tagger.tsx +2 -2
- package/src/components/Alert/Alert.stories.tsx +11 -3
- package/src/components/Alert/Alert.tsx +19 -1
- package/src/components/Alert/AlertCollapsibleIcon.tsx +3 -4
- package/src/components/Alert/AlertIcon.tsx +18 -21
- package/src/components/Badge/Badge.stories.tsx +22 -4
- package/src/components/Badge/Badge.tsx +16 -0
- package/src/components/Badge/BadgeButton.tsx +7 -6
- package/src/components/Breadcrumb/BreadcrumbEllipsis.tsx +2 -2
- package/src/components/Breadcrumb/BreadcrumbSeparator.tsx +2 -2
- package/src/components/Button/Button.stories.tsx +14 -2
- package/src/components/Button/Button.tsx +42 -5
- package/src/components/Button/styles.ts +4 -6
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +32 -10
- package/src/components/Card/Card.tsx +17 -0
- package/src/components/Checkbox/Checkbox.tsx +3 -6
- package/src/components/CodeEditor/highlighting.ts +1 -1
- package/src/components/CollapsiblePanel.tsx +2 -5
- package/src/components/Combobox/Combobox.tsx +12 -14
- package/src/components/Combobox/ComboboxChips.tsx +6 -1
- package/src/components/Combobox/ComboboxItem.tsx +3 -4
- package/src/components/Combobox/ComboboxItemIndicator.tsx +6 -4
- package/src/components/Combobox/types.ts +2 -6
- package/src/components/Command/Command.stories.tsx +47 -56
- package/src/components/Command/CommandItem/CheckIndicator.tsx +26 -14
- package/src/components/Command/CommandItem/CommandItem.tsx +6 -3
- package/src/components/DropdownMenu/DropdownMenu.stories.tsx +8 -4
- package/src/components/DropdownMenu/DropdownMenuCheckboxItem.tsx +4 -1
- package/src/components/DropdownMenu/DropdownMenuItem.tsx +12 -6
- package/src/components/DropdownMenu/DropdownMenuItemIndicator.tsx +12 -6
- package/src/components/DropdownMenu/DropdownMenuRadioGroup/DropdownMenuRadioItem.tsx +8 -5
- package/src/components/DropdownMenu/DropdownMenuSub/DropdownMenuSubTrigger.tsx +2 -2
- package/src/components/DropdownMenu/ItemIcon.tsx +16 -8
- package/src/components/FormGroup/FormGroup.tsx +15 -0
- package/src/components/FullScreenModal/FullScreenModalHeader.tsx +4 -1
- package/src/components/Input/DateInput/DateInput.tsx +4 -1
- package/src/components/Input/DateRangeInput/DateRangeInput.tsx +4 -6
- package/src/components/Input/Input.tsx +18 -2
- package/src/components/Input/TextInput/TextInput.tsx +16 -0
- package/src/components/Input/TimeInput/TimeInput.tsx +4 -1
- package/src/components/InputGroup/InputGroup.stories.tsx +11 -3
- package/src/components/MarkdownEditor/MarkdownEditor.stories.tsx +63 -1
- package/src/components/MarkdownEditor/MarkdownEditor.tsx +105 -15
- package/src/components/MarkdownEditor/nodes/ImageNode.tsx +10 -10
- package/src/components/MarkdownEditor/nodes/MermaidNode.tsx +8 -8
- package/src/components/MarkdownEditor/plugins/KeyboardPlugin.tsx +47 -3
- package/src/components/MarkdownEditor/plugins/MarkdownSyncPlugin.tsx +12 -3
- package/src/components/MarkdownEditor/plugins/ToolbarPlugin.tsx +20 -10
- package/src/components/MarkdownEditor/plugins/toolbar/AlignmentDropdown.tsx +20 -14
- package/src/components/MarkdownEditor/plugins/toolbar/BlockTypeDropdown.tsx +25 -16
- package/src/components/MarkdownEditor/plugins/toolbar/LinkPanel.tsx +3 -3
- package/src/components/MarkdownEditor/transformers.ts +10 -0
- package/src/components/MergeEditor/AcceptControls/AcceptButton.tsx +9 -4
- package/src/components/MergeEditor/AcceptControls/AcceptControls.tsx +25 -14
- package/src/components/MergeEditor/ConflictStatus/ConflictStatusIndicator.tsx +2 -2
- package/src/components/MergeEditor/MergeEditor.stories.tsx +15 -0
- package/src/components/MergeEditor/MergeEditor.tsx +9 -0
- package/src/components/MergeEditor/MergePane.tsx +22 -4
- package/src/components/MergeEditor/decorations.ts +2 -2
- package/src/components/Modal/Modal.stories.tsx +6 -4
- package/src/components/Modal/Modal.tsx +26 -0
- package/src/components/Modal/ModalBody.tsx +5 -0
- package/src/components/Modal/ModalCloseIcon.tsx +2 -3
- package/src/components/Modal/ModalContent.tsx +8 -0
- package/src/components/Modal/ModalFooter.tsx +5 -0
- package/src/components/Modal/ModalHeader.tsx +11 -0
- package/src/components/Modal/ModalIcon.tsx +45 -19
- package/src/components/Navbar/Navbar.stories.tsx +26 -13
- package/src/components/Navbar/NavbarMenuButton.tsx +11 -10
- package/src/components/Pagination/PaginationEllipsis.tsx +2 -2
- package/src/components/Pagination/PaginationNext.tsx +2 -1
- package/src/components/Pagination/PaginationPrevious.tsx +4 -1
- package/src/components/PanelWithDetails.tsx +3 -4
- package/src/components/Popconfirm/Popconfirm.stories.tsx +13 -5
- package/src/components/RadioGroup/Radio.tsx +6 -12
- package/src/components/Search/ClearButton.tsx +2 -2
- package/src/components/Search/Search.tsx +3 -3
- package/src/components/Select/Select.tsx +16 -5
- package/src/components/Select/SelectContent.tsx +13 -4
- package/src/components/Select/SelectItem.tsx +6 -5
- package/src/components/Select/SelectTrigger.tsx +2 -2
- package/src/components/Sidebar/Sidebar.stories.tsx +55 -19
- package/src/components/Sidebar/SidebarGroup/SidebarGroupAction.tsx +8 -4
- package/src/components/Sidebar/SidebarMenu/SidebarMenuButton.tsx +4 -2
- package/src/components/Sidebar/SidebarMenu/SidebarMenuCard.tsx +3 -2
- package/src/components/Sidebar/SidebarMenu/SidebarMenuIcon.tsx +12 -12
- package/src/components/Sidebar/SidebarMenu/SidebarMenuSub.tsx +3 -7
- package/src/components/Sidebar/SidebarMenu/SidebarSearchInput.tsx +3 -5
- package/src/components/Skeleton/Skeleton.stories.tsx +52 -0
- package/src/components/Skeleton/Skeleton.tsx +24 -1
- package/src/components/Table/Table.stories.tsx +223 -1
- package/src/components/Table/Table.tsx +17 -0
- package/src/components/Tabs.tsx +25 -2
- package/src/components/Toast/ToastDismiss.tsx +3 -3
- package/src/components/Toast/ToastRoot.tsx +24 -19
- package/src/components/Toggle/Toggle.tsx +11 -6
- package/src/components/Tooltip/Tooltip.tsx +23 -0
- package/src/components/ZoomableSvg/ZoomableSvg.tsx +8 -6
- package/src/icons/Keboola.tsx +28 -0
- package/src/icons/icons.stories.tsx +174 -0
- package/src/icons/index.ts +180 -0
- package/src/index.ts +1 -0
- package/src/patterns/AlertWithAction.stories.tsx +30 -0
- package/src/patterns/CardWithEmptyState.stories.tsx +66 -0
- package/src/patterns/ConfirmDialog.stories.tsx +72 -0
- package/src/patterns/CreatableMultiCombobox.stories.tsx +33 -0
- package/src/patterns/DataCardWithSkeleton.stories.tsx +48 -0
- package/src/patterns/DisabledButtonTooltip.stories.tsx +30 -0
- package/src/patterns/FilterToolbar.stories.tsx +71 -0
- package/src/patterns/InfoTooltip.stories.tsx +56 -0
- package/src/patterns/ListDetail.stories.tsx +65 -0
- package/src/patterns/ModalForm.stories.tsx +59 -0
- package/src/patterns/PageHeaderTabs.stories.tsx +48 -0
- package/src/styles.css +16 -8
- 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
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
<
|
|
146
|
+
<Search className="tw-size-4 tw-shrink-0 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,
|
|
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={<
|
|
55
|
+
prefix={<Search className="tw-size-4 tw-shrink-0 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
|
-
|
|
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
|
-
|
|
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
|
|
53
|
-
<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=
|
|
74
|
-
<SplitButton.Action icon=
|
|
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
|
},
|