@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.
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 +5 -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
@@ -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: 'trash',
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: 'pen',
41
+ icon: <Pencil />,
41
42
  tooltipContent: 'Edit',
42
43
  },
43
44
  };
44
45
 
45
46
  export const Delete: Story = {
46
47
  args: {
47
- icon: 'trash',
48
+ icon: <Trash2 />,
48
49
  tooltipContent: 'Delete',
49
50
  },
50
51
  };
51
52
 
52
53
  export const Loading: Story = {
53
54
  args: {
54
- icon: 'pen',
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: 'trash',
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="pen"
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="copy"
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="trash"
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="pen"
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="copy"
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="trash"
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: ComponentProps<typeof Icon>['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) => IconName | undefined;
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 icon="xmark" aria-label="Clear" disabled={disabled}>
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
- disabled={disabled}
203
- asChild
204
- icon="chevron-down"
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`; pass an
69
- `InlineEditableText` (when it lands) for editable titles per the Detail
70
- pattern.
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" icon="book-blank" aria-label="Documentation" />
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" icon="book-blank" aria-label="Documentation" />
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 icon="upload" onClick={() => {}}>
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
- argo
152
- variant="invisible"
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="copy">Copy</DropdownMenu.Item>
160
- <DropdownMenu.Item icon="user-group">Manage permissions</DropdownMenu.Item>
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="trash">Delete</DropdownMenu.Item>
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 icon="upload" onClick={() => {}}>
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
- argo
223
- variant="invisible"
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="copy">Duplicate</DropdownMenu.Item>
231
- <DropdownMenu.Item icon="bug">Debug mode</DropdownMenu.Item>
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="trash">Delete</DropdownMenu.Item>
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
- type PageHeaderDetailProps = ComponentPropsWithoutRef<'header'> & {
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
- type PageHeaderIndexProps = ComponentPropsWithoutRef<'header'> & {
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
  });