@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
|
@@ -2,6 +2,7 @@ import type { ComponentType } from 'react';
|
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
3
|
|
|
4
4
|
import { TooltipProvider } from '../../components/Tooltip';
|
|
5
|
+
import { Copy, Pencil, Trash2 } from '../../icons';
|
|
5
6
|
|
|
6
7
|
import { MultiActionButton } from './MultiActionButton';
|
|
7
8
|
import { MultiActionButtonGroup } from './MultiActionButtonGroup';
|
|
@@ -20,7 +21,7 @@ export default {
|
|
|
20
21
|
disabled: false,
|
|
21
22
|
isLoading: false,
|
|
22
23
|
tooltipContent: 'Action',
|
|
23
|
-
icon:
|
|
24
|
+
icon: <Trash2 />,
|
|
24
25
|
onClick: () => alert('Button clicked'),
|
|
25
26
|
},
|
|
26
27
|
argTypes: {
|
|
@@ -37,21 +38,21 @@ type Story = StoryObj<typeof MultiActionButton>;
|
|
|
37
38
|
|
|
38
39
|
export const Default: Story = {
|
|
39
40
|
args: {
|
|
40
|
-
icon:
|
|
41
|
+
icon: <Pencil />,
|
|
41
42
|
tooltipContent: 'Edit',
|
|
42
43
|
},
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
export const Delete: Story = {
|
|
46
47
|
args: {
|
|
47
|
-
icon:
|
|
48
|
+
icon: <Trash2 />,
|
|
48
49
|
tooltipContent: 'Delete',
|
|
49
50
|
},
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
export const Loading: Story = {
|
|
53
54
|
args: {
|
|
54
|
-
icon:
|
|
55
|
+
icon: <Pencil />,
|
|
55
56
|
tooltipContent: 'Saving...',
|
|
56
57
|
isLoading: true,
|
|
57
58
|
},
|
|
@@ -59,7 +60,7 @@ export const Loading: Story = {
|
|
|
59
60
|
|
|
60
61
|
export const Disabled: Story = {
|
|
61
62
|
args: {
|
|
62
|
-
icon:
|
|
63
|
+
icon: <Trash2 />,
|
|
63
64
|
tooltipContent: 'Delete (disabled)',
|
|
64
65
|
disabled: true,
|
|
65
66
|
},
|
|
@@ -69,19 +70,19 @@ export const ButtonGroup: Story = {
|
|
|
69
70
|
render: () => (
|
|
70
71
|
<MultiActionButtonGroup>
|
|
71
72
|
<MultiActionButtonGroup.Button
|
|
72
|
-
icon=
|
|
73
|
+
icon={<Pencil />}
|
|
73
74
|
label="Edit"
|
|
74
75
|
tooltipContent="Edit"
|
|
75
76
|
onClick={() => alert('Edit clicked')}
|
|
76
77
|
/>
|
|
77
78
|
<MultiActionButtonGroup.Button
|
|
78
|
-
icon=
|
|
79
|
+
icon={<Copy />}
|
|
79
80
|
label="Copy"
|
|
80
81
|
tooltipContent="Copy"
|
|
81
82
|
onClick={() => alert('Copy clicked')}
|
|
82
83
|
/>
|
|
83
84
|
<MultiActionButtonGroup.Button
|
|
84
|
-
icon=
|
|
85
|
+
icon={<Trash2 />}
|
|
85
86
|
label="Delete"
|
|
86
87
|
tooltipContent="Delete"
|
|
87
88
|
onClick={() => alert('Delete clicked')}
|
|
@@ -94,20 +95,20 @@ export const ButtonGroupWithMixedStates: Story = {
|
|
|
94
95
|
render: () => (
|
|
95
96
|
<MultiActionButtonGroup>
|
|
96
97
|
<MultiActionButtonGroup.Button
|
|
97
|
-
icon=
|
|
98
|
+
icon={<Pencil />}
|
|
98
99
|
label="Edit"
|
|
99
100
|
tooltipContent="Edit"
|
|
100
101
|
onClick={() => alert('Edit clicked')}
|
|
101
102
|
/>
|
|
102
103
|
<MultiActionButtonGroup.Button
|
|
103
|
-
icon=
|
|
104
|
+
icon={<Copy />}
|
|
104
105
|
label="Copy"
|
|
105
106
|
tooltipContent="Processing..."
|
|
106
107
|
isLoading={true}
|
|
107
108
|
onClick={() => {}}
|
|
108
109
|
/>
|
|
109
110
|
<MultiActionButtonGroup.Button
|
|
110
|
-
icon=
|
|
111
|
+
icon={<Trash2 />}
|
|
111
112
|
label="Delete"
|
|
112
113
|
tooltipContent="Delete (disabled)"
|
|
113
114
|
disabled={true}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ComponentProps, ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
import { Button } from '../../components/Button';
|
|
4
|
-
import type { Icon } from '../../components/Icon';
|
|
5
4
|
import { Tooltip } from '../../components/Tooltip';
|
|
5
|
+
import { renderLegacyIcon } from '../../utils/legacyIcon';
|
|
6
6
|
|
|
7
7
|
type Props = {
|
|
8
8
|
disabled?: boolean;
|
|
9
|
-
icon:
|
|
9
|
+
icon: ReactNode;
|
|
10
10
|
isLoading?: boolean;
|
|
11
11
|
label: string;
|
|
12
12
|
onClick: ComponentProps<typeof Button>['onClick'];
|
|
@@ -28,13 +28,14 @@ export const MultiActionButton = ({
|
|
|
28
28
|
<Tooltip placement="top" tooltip={tooltipContent} triggerAsChild variant={tooltipVariant}>
|
|
29
29
|
<Button
|
|
30
30
|
disabled={disabled}
|
|
31
|
-
icon={icon}
|
|
32
31
|
isLoading={isLoading}
|
|
33
32
|
aria-label={label}
|
|
34
33
|
onClick={onClick}
|
|
35
34
|
variant="invisible"
|
|
36
35
|
size="icon"
|
|
37
36
|
data-testid={dataTestId}
|
|
38
|
-
|
|
37
|
+
>
|
|
38
|
+
{!isLoading && renderLegacyIcon(icon)}
|
|
39
|
+
</Button>
|
|
39
40
|
</Tooltip>
|
|
40
41
|
);
|
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
isGroupItem,
|
|
12
12
|
isLabeledItem,
|
|
13
13
|
} from '../../components/Combobox/utils';
|
|
14
|
-
import type { Icon } from '../../components/Icon';
|
|
15
14
|
import { InputGroup } from '../../components/InputGroup';
|
|
15
|
+
import { ChevronDown, X } from '../../icons';
|
|
16
16
|
|
|
17
17
|
type ComboboxProps<Value> = BaseCombobox.Root.Props<Value, true>;
|
|
18
18
|
type ComboboxInputProps = ComponentProps<typeof Combobox.Input>;
|
|
@@ -20,9 +20,13 @@ type ComboboxInputGroupProps = ComponentProps<typeof Combobox.InputGroup>;
|
|
|
20
20
|
type ComboboxItemProps = ComponentProps<typeof Combobox.Item>;
|
|
21
21
|
type ComboboxChipsProps = ComponentProps<typeof Combobox.Chips>;
|
|
22
22
|
|
|
23
|
-
type IconName = ComponentProps<typeof Icon>['icon'];
|
|
24
23
|
type Indicator = NonNullable<ComboboxItemProps['indicator']>;
|
|
25
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Props for `MultiCombobox`. Generic over `Value` (the item shape) and `GroupShape` (optional group wrapper).
|
|
27
|
+
* Use `itemToLabel` / `itemToIcon` / `itemToDisabled` to project custom item shapes; use `filter` to override
|
|
28
|
+
* the default substring matcher.
|
|
29
|
+
*/
|
|
26
30
|
type Props<Value, GroupShape extends Group<Value> = Group<Value>> = Pick<
|
|
27
31
|
ComboboxInputProps,
|
|
28
32
|
'placeholder' | 'id'
|
|
@@ -50,7 +54,7 @@ type Props<Value, GroupShape extends Group<Value> = Group<Value>> = Pick<
|
|
|
50
54
|
|
|
51
55
|
// item specific
|
|
52
56
|
itemToLabel?: (item: Value) => ReactNode;
|
|
53
|
-
itemToIcon?: (item: Value) =>
|
|
57
|
+
itemToIcon?: (item: Value) => ReactNode;
|
|
54
58
|
itemToDisabled?: (item: Value) => boolean;
|
|
55
59
|
|
|
56
60
|
// gropu specific
|
|
@@ -65,6 +69,19 @@ const selectAllItem: LabeledItem = {
|
|
|
65
69
|
const isSelectAllItem = <Value,>(item: Value) =>
|
|
66
70
|
isLabeledItem(item) && item.value === '__select_all__';
|
|
67
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Multi-selection combobox with chip display and search. Built on Base UI Combobox.
|
|
74
|
+
*
|
|
75
|
+
* **When to use:** picking several items from a searchable enumerated list (filter by tags, assign labels).
|
|
76
|
+
* Items render as chips above the input; `maxVisible` collapses excess into a `+N more` chip.
|
|
77
|
+
* **When NOT to use:** for single selection use `Combobox`; for short enums without search use `Select`;
|
|
78
|
+
* for free-form chip entry where the user types arbitrary new values use `Tagger`.
|
|
79
|
+
* **Composition:** wrap in `FormGroup` for label + help text, or use `useAppForm` for form integration.
|
|
80
|
+
* Pass `allowSelectAll` for a sticky "Select All" item when the full enum is small.
|
|
81
|
+
*
|
|
82
|
+
* @see RECIPES.md#creatable-multi-combobox — for free-form-plus-enumerated patterns
|
|
83
|
+
* @see RECIPES.md#filter-toolbar — for combining multi-select with other filter chips
|
|
84
|
+
*/
|
|
68
85
|
export const MultiCombobox = <Value, GroupShape extends Group<Value> = Group<Value>>({
|
|
69
86
|
// root props
|
|
70
87
|
items: _itemsProp,
|
|
@@ -194,17 +211,16 @@ export const MultiCombobox = <Value, GroupShape extends Group<Value> = Group<Val
|
|
|
194
211
|
className="tw-w-auto tw-min-w-[120px] tw-flex-1"
|
|
195
212
|
/>
|
|
196
213
|
{allowClear && (
|
|
197
|
-
<InputGroup.Button asChild
|
|
198
|
-
<BaseCombobox.Clear
|
|
214
|
+
<InputGroup.Button asChild aria-label="Clear" disabled={disabled}>
|
|
215
|
+
<BaseCombobox.Clear>
|
|
216
|
+
<X className="tw-size-4" />
|
|
217
|
+
</BaseCombobox.Clear>
|
|
199
218
|
</InputGroup.Button>
|
|
200
219
|
)}
|
|
201
|
-
<InputGroup.Button
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
aria-label="Open combobox"
|
|
206
|
-
>
|
|
207
|
-
<BaseCombobox.Trigger />
|
|
220
|
+
<InputGroup.Button disabled={disabled} asChild aria-label="Open combobox">
|
|
221
|
+
<BaseCombobox.Trigger>
|
|
222
|
+
<ChevronDown className="tw-size-4" />
|
|
223
|
+
</BaseCombobox.Trigger>
|
|
208
224
|
</InputGroup.Button>
|
|
209
225
|
</div>
|
|
210
226
|
</div>
|
|
@@ -65,13 +65,73 @@ either variant.
|
|
|
65
65
|
- **Compose** existing `Breadcrumb`, `Badge`, `DropdownMenu`, `Tabs` from
|
|
66
66
|
`@keboola/design` — slots take any `ReactNode`, but use the design-system
|
|
67
67
|
pieces.
|
|
68
|
-
- Title and description can be any `ReactNode
|
|
69
|
-
`
|
|
70
|
-
|
|
68
|
+
- Title and description can be any `ReactNode`. For Linear-style inline
|
|
69
|
+
editing, compose `MarkdownEditor` (from `@keboola/design`) directly into
|
|
70
|
+
the slots — `PageHeader` does not depend on Lexical itself; only callers
|
|
71
|
+
that opt into editing pull it in.
|
|
72
|
+
|
|
73
|
+
## Inline editing
|
|
74
|
+
|
|
75
|
+
The `title` and `description` slots accept any `ReactNode`, so they can hold
|
|
76
|
+
an editable `MarkdownEditor` in place of the static text. Follows the
|
|
77
|
+
[inline-editable-text pattern](https://github.com/evviku/design-system-docs/tree/main/docs/patterns/inline-editable-text):
|
|
78
|
+
|
|
79
|
+
- **No edit icon, no save / cancel buttons.**
|
|
80
|
+
- **Blur, Cmd+Enter, or Enter (title only) commits** via `onSave`.
|
|
81
|
+
- **Esc reverts** via `onCancel` — the caller resets state to the last
|
|
82
|
+
committed value inside this handler.
|
|
83
|
+
- **Description clamps to ~2 lines with a bottom fade** in display state
|
|
84
|
+
(use the `clampDisplay` prop); focus expands to full height. Fade
|
|
85
|
+
replaces ellipsis per the spec.
|
|
86
|
+
|
|
87
|
+
Use `compact` + `saveOnBlur` for both slots, plus `singleLine` for the
|
|
88
|
+
title and `clampDisplay` for the description. Maintain a two-state
|
|
89
|
+
(`committed` / `draft`) pattern so `onCancel` can revert without losing
|
|
90
|
+
the committed value:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { MarkdownEditor } from '@keboola/design';
|
|
94
|
+
|
|
95
|
+
const [committedTitle, setCommittedTitle] = useState(initial.title);
|
|
96
|
+
const [draftTitle, setDraftTitle] = useState(committedTitle);
|
|
97
|
+
// (same for description)
|
|
98
|
+
|
|
99
|
+
<PageHeader.Detail
|
|
100
|
+
breadcrumbs={...}
|
|
101
|
+
title={
|
|
102
|
+
<MarkdownEditor
|
|
103
|
+
compact singleLine saveOnBlur
|
|
104
|
+
value={draftTitle}
|
|
105
|
+
onChange={setDraftTitle}
|
|
106
|
+
onSave={() => { setCommittedTitle(draftTitle); save({ title: draftTitle }); }}
|
|
107
|
+
onCancel={() => setDraftTitle(committedTitle)}
|
|
108
|
+
placeholder="Untitled"
|
|
109
|
+
className="tw-m-0 tw-text-3xl tw-font-semibold tw-leading-9 tw-text-foreground tw-break-words"
|
|
110
|
+
/>
|
|
111
|
+
}
|
|
112
|
+
description={
|
|
113
|
+
<MarkdownEditor
|
|
114
|
+
compact saveOnBlur clampDisplay
|
|
115
|
+
value={draftDescription}
|
|
116
|
+
onChange={setDraftDescription}
|
|
117
|
+
onSave={() => { setCommittedDescription(draftDescription); save({ description: draftDescription }); }}
|
|
118
|
+
onCancel={() => setDraftDescription(committedDescription)}
|
|
119
|
+
placeholder="Add a description…"
|
|
120
|
+
className="tw-m-0 tw-max-w-[800px] tw-break-words tw-text-sm tw-leading-5 tw-text-muted-foreground"
|
|
121
|
+
/>
|
|
122
|
+
}
|
|
123
|
+
primaryAction={...}
|
|
124
|
+
/>;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The `titleClass` / `descriptionClass` constants exported from
|
|
128
|
+
`./styles.ts` carry the canonical typography — pass them through
|
|
129
|
+
`className` so the editor visually matches the static variant.
|
|
71
130
|
|
|
72
131
|
## Stories
|
|
73
132
|
|
|
74
133
|
- `IndexFlows` — description + secondary/primary actions, no header nav.
|
|
75
134
|
- `IndexStorage` — `metadata` count summary + 5-tab `headerNav`.
|
|
76
135
|
- `DetailBucket` — breadcrumbs + overflow + badges + primary action.
|
|
136
|
+
- `DetailEditable` — inline-editable title + description via `MarkdownEditor`.
|
|
77
137
|
- `DetailDataApp` — primary + secondary actions + 3-tab `headerNav`.
|
|
@@ -1,14 +1,27 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
3
|
|
|
4
|
+
import {
|
|
5
|
+
BookOpen,
|
|
6
|
+
Bug,
|
|
7
|
+
Copy,
|
|
8
|
+
EllipsisVertical,
|
|
9
|
+
Trash2,
|
|
10
|
+
Upload,
|
|
11
|
+
Users,
|
|
12
|
+
} from '@keboola/design/icons';
|
|
13
|
+
|
|
3
14
|
import { Badge } from '../../components/Badge';
|
|
4
15
|
import { Breadcrumb } from '../../components/Breadcrumb';
|
|
5
16
|
import { Button } from '../../components/Button';
|
|
6
17
|
import { DropdownMenu } from '../../components/DropdownMenu';
|
|
18
|
+
import { MarkdownEditor } from '../../components/MarkdownEditor';
|
|
7
19
|
import { Tabs, TabsContent } from '../../components/Tabs';
|
|
8
20
|
import { PageLayout } from '../PageLayout';
|
|
9
21
|
import { SplitButton } from '../SplitButton';
|
|
10
22
|
|
|
11
23
|
import { PageHeader } from './PageHeader';
|
|
24
|
+
import { descriptionClass, titleClass } from './styles';
|
|
12
25
|
|
|
13
26
|
type Story = StoryObj<typeof PageHeader.Index>;
|
|
14
27
|
|
|
@@ -41,7 +54,9 @@ export const IndexFlows: Story = {
|
|
|
41
54
|
title="Flows"
|
|
42
55
|
description="Build flows with branching logic and conditions. Tasks run based on previous results, errors, or custom expressions."
|
|
43
56
|
documentation={
|
|
44
|
-
<Button argo variant="invisible"
|
|
57
|
+
<Button argo variant="invisible" aria-label="Documentation">
|
|
58
|
+
<BookOpen className="tw-size-4" />
|
|
59
|
+
</Button>
|
|
45
60
|
}
|
|
46
61
|
action={
|
|
47
62
|
<>
|
|
@@ -73,7 +88,9 @@ export const IndexStorage: Story = {
|
|
|
73
88
|
title="Storage"
|
|
74
89
|
metadata="3 buckets and 39 tables"
|
|
75
90
|
documentation={
|
|
76
|
-
<Button argo variant="invisible"
|
|
91
|
+
<Button argo variant="invisible" aria-label="Documentation">
|
|
92
|
+
<BookOpen className="tw-size-4" />
|
|
93
|
+
</Button>
|
|
77
94
|
}
|
|
78
95
|
action={
|
|
79
96
|
<SplitButton
|
|
@@ -81,14 +98,13 @@ export const IndexStorage: Story = {
|
|
|
81
98
|
onClick={() => {}}
|
|
82
99
|
actions={
|
|
83
100
|
<>
|
|
84
|
-
<SplitButton.Action
|
|
85
|
-
Upload file
|
|
86
|
-
</SplitButton.Action>
|
|
101
|
+
<SplitButton.Action onClick={() => {}}>Upload file</SplitButton.Action>
|
|
87
102
|
<SplitButton.Action icon="cloud-arrow-down" onClick={() => {}}>
|
|
88
103
|
Import from external storage
|
|
89
104
|
</SplitButton.Action>
|
|
90
105
|
</>
|
|
91
106
|
}
|
|
107
|
+
icon={<Upload className="tw-size-4" />}
|
|
92
108
|
>
|
|
93
109
|
New bucket
|
|
94
110
|
</SplitButton>
|
|
@@ -147,19 +163,17 @@ export const DetailBucket: Story = {
|
|
|
147
163
|
overflowActions={
|
|
148
164
|
<DropdownMenu>
|
|
149
165
|
<DropdownMenu.Trigger asChild>
|
|
150
|
-
<Button
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
size="icon-sm"
|
|
154
|
-
icon="ellipsis-vertical"
|
|
155
|
-
aria-label="More actions"
|
|
156
|
-
/>
|
|
166
|
+
<Button argo variant="invisible" size="icon-sm" aria-label="More actions">
|
|
167
|
+
<EllipsisVertical className="tw-size-4" />
|
|
168
|
+
</Button>
|
|
157
169
|
</DropdownMenu.Trigger>
|
|
158
170
|
<DropdownMenu.Content align="end">
|
|
159
|
-
<DropdownMenu.Item icon="
|
|
160
|
-
<DropdownMenu.Item icon="
|
|
171
|
+
<DropdownMenu.Item icon={<Copy className="tw-size-4" />}>Copy</DropdownMenu.Item>
|
|
172
|
+
<DropdownMenu.Item icon={<Users className="tw-size-4" />}>
|
|
173
|
+
Manage permissions
|
|
174
|
+
</DropdownMenu.Item>
|
|
161
175
|
<DropdownMenu.Separator />
|
|
162
|
-
<DropdownMenu.Item icon="
|
|
176
|
+
<DropdownMenu.Item icon={<Trash2 className="tw-size-4" />}>Delete</DropdownMenu.Item>
|
|
163
177
|
</DropdownMenu.Content>
|
|
164
178
|
</DropdownMenu>
|
|
165
179
|
}
|
|
@@ -177,14 +191,13 @@ export const DetailBucket: Story = {
|
|
|
177
191
|
onClick={() => {}}
|
|
178
192
|
actions={
|
|
179
193
|
<>
|
|
180
|
-
<SplitButton.Action
|
|
181
|
-
Upload CSV
|
|
182
|
-
</SplitButton.Action>
|
|
194
|
+
<SplitButton.Action onClick={() => {}}>Upload CSV</SplitButton.Action>
|
|
183
195
|
<SplitButton.Action icon="link" onClick={() => {}}>
|
|
184
196
|
Link existing table
|
|
185
197
|
</SplitButton.Action>
|
|
186
198
|
</>
|
|
187
199
|
}
|
|
200
|
+
icon={<Upload className="tw-size-4" />}
|
|
188
201
|
>
|
|
189
202
|
Add table
|
|
190
203
|
</SplitButton>
|
|
@@ -197,6 +210,93 @@ export const DetailBucket: Story = {
|
|
|
197
210
|
),
|
|
198
211
|
};
|
|
199
212
|
|
|
213
|
+
// Detail — editable (inline edit on title + description per the inline-editable-text pattern spec).
|
|
214
|
+
const DetailEditableRender = () => {
|
|
215
|
+
// Two-state pattern: `committed` is what's persisted; `draft` tracks in-progress edits.
|
|
216
|
+
// onSave (blur / Cmd+Enter / Enter on title) commits draft → committed.
|
|
217
|
+
// onCancel (Esc) reverts draft ← committed.
|
|
218
|
+
const [committedTitle, setCommittedTitle] = useState('Marketing bucket');
|
|
219
|
+
const [draftTitle, setDraftTitle] = useState(committedTitle);
|
|
220
|
+
const [committedDescription, setCommittedDescription] = useState(
|
|
221
|
+
'Contains marketing datasets and transformation outputs. This description is intentionally long so you can see how the display state clamps to two lines with a bottom fade — focus the field and it expands to full height for editing.',
|
|
222
|
+
);
|
|
223
|
+
const [draftDescription, setDraftDescription] = useState(committedDescription);
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<PageLayout>
|
|
227
|
+
<PageHeader.Detail
|
|
228
|
+
breadcrumbs={
|
|
229
|
+
<Breadcrumb>
|
|
230
|
+
<Breadcrumb.List>
|
|
231
|
+
<Breadcrumb.Item>
|
|
232
|
+
<Breadcrumb.Link href="#">Storage</Breadcrumb.Link>
|
|
233
|
+
</Breadcrumb.Item>
|
|
234
|
+
<Breadcrumb.Separator />
|
|
235
|
+
<Breadcrumb.Item>
|
|
236
|
+
<Breadcrumb.Page>{committedTitle || 'Untitled'}</Breadcrumb.Page>
|
|
237
|
+
</Breadcrumb.Item>
|
|
238
|
+
</Breadcrumb.List>
|
|
239
|
+
</Breadcrumb>
|
|
240
|
+
}
|
|
241
|
+
title={
|
|
242
|
+
<MarkdownEditor
|
|
243
|
+
compact
|
|
244
|
+
singleLine
|
|
245
|
+
saveOnBlur
|
|
246
|
+
value={draftTitle}
|
|
247
|
+
onChange={setDraftTitle}
|
|
248
|
+
onSave={() => setCommittedTitle(draftTitle)}
|
|
249
|
+
onCancel={() => setDraftTitle(committedTitle)}
|
|
250
|
+
placeholder="Untitled"
|
|
251
|
+
className={titleClass}
|
|
252
|
+
/>
|
|
253
|
+
}
|
|
254
|
+
badges={<Badge color="green">Active</Badge>}
|
|
255
|
+
description={
|
|
256
|
+
<MarkdownEditor
|
|
257
|
+
compact
|
|
258
|
+
saveOnBlur
|
|
259
|
+
clampDisplay
|
|
260
|
+
value={draftDescription}
|
|
261
|
+
onChange={setDraftDescription}
|
|
262
|
+
onSave={() => setCommittedDescription(draftDescription)}
|
|
263
|
+
onCancel={() => setDraftDescription(committedDescription)}
|
|
264
|
+
placeholder="Add a description…"
|
|
265
|
+
className={descriptionClass}
|
|
266
|
+
/>
|
|
267
|
+
}
|
|
268
|
+
primaryAction={
|
|
269
|
+
<Button argo onClick={() => {}}>
|
|
270
|
+
Add table
|
|
271
|
+
</Button>
|
|
272
|
+
}
|
|
273
|
+
/>
|
|
274
|
+
<PageLayout.Content>
|
|
275
|
+
<details className="tw-rounded tw-border tw-border-border tw-bg-muted/30 tw-p-4">
|
|
276
|
+
<summary className="tw-cursor-pointer tw-text-sm tw-text-muted-foreground">
|
|
277
|
+
Committed state (only updates on save — Esc reverts draft)
|
|
278
|
+
</summary>
|
|
279
|
+
<pre className="tw-mt-2 tw-whitespace-pre-wrap tw-text-xs">
|
|
280
|
+
{JSON.stringify({ title: committedTitle, description: committedDescription }, null, 2)}
|
|
281
|
+
</pre>
|
|
282
|
+
</details>
|
|
283
|
+
</PageLayout.Content>
|
|
284
|
+
</PageLayout>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export const DetailEditable: Story = {
|
|
289
|
+
render: () => <DetailEditableRender />,
|
|
290
|
+
parameters: {
|
|
291
|
+
docs: {
|
|
292
|
+
description: {
|
|
293
|
+
story:
|
|
294
|
+
'Inline editing per the [inline-editable-text pattern spec](https://github.com/evviku/design-system-docs/tree/main/docs/patterns/inline-editable-text). Click the title or description to edit in place; no edit icon, no save / cancel buttons. **Blur, Enter (title), or Cmd+Enter commits** via `onSave`. **Esc reverts** via `onCancel` (caller resets state to last committed). Description uses `clampDisplay` so it clamps to ~2 lines + bottom fade when not focused; focusing expands to full height. Both slots accept any `ReactNode`, so `PageHeader` stays Lexical-free.',
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
200
300
|
// Detail — sales dashboard (primary + secondary actions + header nav).
|
|
201
301
|
export const DetailDataApp: Story = {
|
|
202
302
|
render: () => (
|
|
@@ -218,19 +318,15 @@ export const DetailDataApp: Story = {
|
|
|
218
318
|
overflowActions={
|
|
219
319
|
<DropdownMenu>
|
|
220
320
|
<DropdownMenu.Trigger asChild>
|
|
221
|
-
<Button
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
size="icon-sm"
|
|
225
|
-
icon="ellipsis-vertical"
|
|
226
|
-
aria-label="More actions"
|
|
227
|
-
/>
|
|
321
|
+
<Button argo variant="invisible" size="icon-sm" aria-label="More actions">
|
|
322
|
+
<EllipsisVertical className="tw-size-4" />
|
|
323
|
+
</Button>
|
|
228
324
|
</DropdownMenu.Trigger>
|
|
229
325
|
<DropdownMenu.Content align="end">
|
|
230
|
-
<DropdownMenu.Item icon="
|
|
231
|
-
<DropdownMenu.Item icon="
|
|
326
|
+
<DropdownMenu.Item icon={<Copy className="tw-size-4" />}>Duplicate</DropdownMenu.Item>
|
|
327
|
+
<DropdownMenu.Item icon={<Bug className="tw-size-4" />}>Debug mode</DropdownMenu.Item>
|
|
232
328
|
<DropdownMenu.Separator />
|
|
233
|
-
<DropdownMenu.Item icon="
|
|
329
|
+
<DropdownMenu.Item icon={<Trash2 className="tw-size-4" />}>Delete</DropdownMenu.Item>
|
|
234
330
|
</DropdownMenu.Content>
|
|
235
331
|
</DropdownMenu>
|
|
236
332
|
}
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { PageHeaderDetail } from './PageHeaderDetail';
|
|
2
2
|
import { PageHeaderIndex } from './PageHeaderIndex';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Structured page-header block that encodes the evviku page-header patterns 1:1 at the type level.
|
|
6
|
+
*
|
|
7
|
+
* **When to use:** every kbc-ui page with a structured header (title + actions + optional badges /
|
|
8
|
+
* description / tabs). Choose `PageHeader.Index` for list / collection pages, `PageHeader.Detail` for
|
|
9
|
+
* single-entity pages with breadcrumbs.
|
|
10
|
+
* **When NOT to use:** for layouts that don't match either pattern, host custom JSX inside
|
|
11
|
+
* `PageLayout.Header` directly. For routing-driven tab rows below the header use `PageLayout.Tabs`.
|
|
12
|
+
* **Composition:** nest inside `PageLayout.Header`. Always opt into `Button argo` for any buttons passed
|
|
13
|
+
* into header slots. The variant types enforce pattern constraints (e.g. `Index` rejects breadcrumbs).
|
|
14
|
+
*
|
|
15
|
+
* @see RECIPES.md#page-header-tabs — for the canonical page-header + tabs composition
|
|
16
|
+
*/
|
|
4
17
|
export const PageHeader = {
|
|
18
|
+
/** Index variant — top-level list / collection pages. No breadcrumbs; `metadata` wins over `description`. */
|
|
5
19
|
Index: PageHeaderIndex,
|
|
20
|
+
/** Detail variant — single-entity pages with required breadcrumbs and an overflow-actions slot. Max 2 visible actions. */
|
|
6
21
|
Detail: PageHeaderDetail,
|
|
7
22
|
};
|
|
@@ -9,17 +9,39 @@ import { badgesClass, descriptionClass, titleClass, titleRowClass } from './styl
|
|
|
9
9
|
// - Breadcrumbs are required; overflow menu sits on the breadcrumbs row.
|
|
10
10
|
// - Max two visible actions: `primaryAction` + optional `secondaryAction`.
|
|
11
11
|
// - All other actions belong in the breadcrumbs-row overflow menu.
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Props for `PageHeader.Detail`. Detail pages enforce mandatory breadcrumbs, ≤ 2 visible actions, and an
|
|
14
|
+
* overflow-menu slot on the breadcrumbs row.
|
|
15
|
+
*/
|
|
16
|
+
type PageHeaderDetailProps = Omit<ComponentPropsWithoutRef<'header'>, 'title'> & {
|
|
17
|
+
/** `Breadcrumb` chain ending in the entity name (last item is not a link). Required. */
|
|
13
18
|
breadcrumbs: ReactNode;
|
|
19
|
+
/** `DropdownMenu` for tertiary actions. Sits on the breadcrumbs row, not the title row. */
|
|
14
20
|
overflowActions?: ReactNode;
|
|
15
21
|
title: ReactNode;
|
|
22
|
+
/** Inline `Badge`s rendered next to the title (status, environment, version). */
|
|
16
23
|
badges?: ReactNode;
|
|
17
24
|
description?: ReactNode;
|
|
25
|
+
/** Primary action (`Button argo`). Use the verb appropriate to the entity (Deploy / Run / Save). */
|
|
18
26
|
primaryAction?: ReactNode;
|
|
27
|
+
/** Optional second action (`Button argo variant="secondary"`). Everything beyond goes in `overflowActions`. */
|
|
19
28
|
secondaryAction?: ReactNode;
|
|
29
|
+
/** Optional `Tabs` row below the title block. */
|
|
20
30
|
headerNav?: ReactNode;
|
|
21
31
|
};
|
|
22
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Page header for single-entity pages.
|
|
35
|
+
*
|
|
36
|
+
* **When to use:** the page shows one entity (a bucket, flow, data app) with a parent hierarchy worth
|
|
37
|
+
* breadcrumbing. Primary action is typically "edit / deploy / run on this entity".
|
|
38
|
+
* **When NOT to use:** for top-level list pages use `PageHeader.Index`. For pages without a parent
|
|
39
|
+
* hierarchy use `PageHeader.Index` even if they show one entity.
|
|
40
|
+
* **Composition:** nest inside `PageLayout.Header`. Pass `Button argo` for actions. Anything beyond the
|
|
41
|
+
* 2-visible-actions cap belongs in `overflowActions` (a `DropdownMenu`), not in extra slots.
|
|
42
|
+
*
|
|
43
|
+
* @see RECIPES.md#list-detail — for the list-page → detail-page navigation pattern
|
|
44
|
+
*/
|
|
23
45
|
export const PageHeaderDetail = ({
|
|
24
46
|
ref,
|
|
25
47
|
className,
|
|
@@ -9,15 +9,34 @@ import { descriptionClass, titleClass, titleRowClass } from './styles';
|
|
|
9
9
|
// - No breadcrumbs (top-level pages).
|
|
10
10
|
// - `metadata` wins over `description` (mutually exclusive in pattern).
|
|
11
11
|
// - At most one primary `action`; pair with the `documentation` icon button.
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Props for `PageHeader.Index`. Index pages have no breadcrumbs and no overflow menu — search and filters
|
|
14
|
+
* belong in `PageLayout.Toolbar`, not here.
|
|
15
|
+
*/
|
|
16
|
+
type PageHeaderIndexProps = Omit<ComponentPropsWithoutRef<'header'>, 'title'> & {
|
|
13
17
|
title: ReactNode;
|
|
18
|
+
/** Prose description shown below the title. Mutually exclusive with `metadata` in the pattern; `metadata` wins when both are passed. */
|
|
14
19
|
description?: ReactNode;
|
|
20
|
+
/** Count / system summary shown below the title (e.g. "3 buckets and 39 tables"). Wins over `description`. */
|
|
15
21
|
metadata?: ReactNode;
|
|
22
|
+
/** Icon-only docs button (`Button argo variant="invisible" icon="book-blank"`). Sits next to `action`. */
|
|
16
23
|
documentation?: ReactNode;
|
|
24
|
+
/** Primary "create / add new X" `Button`. Pass `Button argo`. */
|
|
17
25
|
action?: ReactNode;
|
|
26
|
+
/** Optional `Tabs` row rendered below the title block. Use only when >1 dataset / section is worth navigating to. */
|
|
18
27
|
headerNav?: ReactNode;
|
|
19
28
|
};
|
|
20
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Page header for top-level list / collection pages.
|
|
32
|
+
*
|
|
33
|
+
* **When to use:** the page is a list (Flows, Storage, Jobs, …) with no parent in the same section.
|
|
34
|
+
* Primary action is typically "create / add a new X".
|
|
35
|
+
* **When NOT to use:** for single-entity detail pages use `PageHeader.Detail`; for layouts that don't fit
|
|
36
|
+
* either pattern host custom JSX in `PageLayout.Header` directly.
|
|
37
|
+
* **Composition:** nest inside `PageLayout.Header`. Always pass `Button argo` into `action` and
|
|
38
|
+
* `documentation`. Search and filter chips belong in `PageLayout.Toolbar`, not here.
|
|
39
|
+
*/
|
|
21
40
|
export const PageHeaderIndex = ({
|
|
22
41
|
ref,
|
|
23
42
|
className,
|
|
@@ -2,6 +2,11 @@ import type { ComponentPropsWithoutRef, ElementRef, Ref } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { cn } from '../../utils';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Props for `PageLayout` root. `maxWidth` constrains the content column —
|
|
7
|
+
* `narrow` (976px) for forms, `default` (1280px) for most pages, `wide` (1600px) for dense detail views,
|
|
8
|
+
* `full` for edge-to-edge layouts.
|
|
9
|
+
*/
|
|
5
10
|
type RootProps = ComponentPropsWithoutRef<'div'> & {
|
|
6
11
|
maxWidth?: 'default' | 'narrow' | 'wide' | 'full';
|
|
7
12
|
};
|
|
@@ -120,11 +125,32 @@ const Footer = ({
|
|
|
120
125
|
</footer>
|
|
121
126
|
);
|
|
122
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Compound page-chrome block. Provides every kbc-ui page with the same content-column shape:
|
|
130
|
+
* max-width container + vertical rhythm + named slots.
|
|
131
|
+
*
|
|
132
|
+
* **When to use:** every new kbc-ui page. Do not roll your own `<div className="tw-flex tw-flex-col">`
|
|
133
|
+
* chrome. Host `PageHeader.Index` / `PageHeader.Detail` inside `PageLayout.Header` for the structured
|
|
134
|
+
* page-header patterns.
|
|
135
|
+
* **When NOT to use:** the left-rail `Sidebar` lives in the app shell outside `PageLayout`. For embedded
|
|
136
|
+
* widgets or modal bodies the layout chrome is the modal / card itself, not `PageLayout`.
|
|
137
|
+
* **Composition:** `PageLayout` is opinionated about chrome only — slots accept any `ReactNode`. Put
|
|
138
|
+
* search / filter pills in `PageLayout.Toolbar`, never in the header. Form submit bars go in
|
|
139
|
+
* `PageLayout.Footer sticky`.
|
|
140
|
+
*
|
|
141
|
+
* @see RECIPES.md#list-detail — for the index → detail page pattern
|
|
142
|
+
*/
|
|
123
143
|
export const PageLayout = Object.assign(Root, {
|
|
144
|
+
/** Detail-pattern breadcrumb row. Usually composed automatically by `PageHeader.Detail` — slot exists for layouts that don't fit either `PageHeader` variant. */
|
|
124
145
|
Breadcrumbs,
|
|
146
|
+
/** Host slot for `PageHeader.Index` / `PageHeader.Detail`, or custom header JSX when neither variant fits. */
|
|
125
147
|
Header,
|
|
148
|
+
/** Escape hatch for routing-based tab rows. Use `Tabs` / `TabsContent` core components inside, never a raw `<button>`. */
|
|
126
149
|
Tabs,
|
|
150
|
+
/** Search / filter pill row. Belongs here, NOT in the header per the page-header patterns. */
|
|
127
151
|
Toolbar,
|
|
152
|
+
/** Main content slot. Lists, cards, tables. */
|
|
128
153
|
Content,
|
|
154
|
+
/** Bottom action bar — form submit + cancel. Pass `sticky` for a footer that pins to the viewport. */
|
|
129
155
|
Footer,
|
|
130
156
|
});
|