@keboola/design 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +25 -0
  2. package/dist/icon-names.cjs +18 -0
  3. package/dist/icon-names.cjs.map +1 -0
  4. package/dist/icon-names.js +16 -0
  5. package/dist/icon-names.js.map +1 -0
  6. package/dist/icons.cjs +731 -0
  7. package/dist/icons.cjs.map +1 -0
  8. package/dist/icons.js +29 -0
  9. package/dist/icons.js.map +1 -0
  10. package/dist/index.cjs +14728 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.js +14553 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/markdown-editor.cjs +4440 -0
  15. package/dist/markdown-editor.cjs.map +1 -0
  16. package/dist/markdown-editor.js +4413 -0
  17. package/dist/markdown-editor.js.map +1 -0
  18. package/dist/mermaid.cjs +1011 -0
  19. package/dist/mermaid.cjs.map +1 -0
  20. package/dist/mermaid.js +1005 -0
  21. package/dist/mermaid.js.map +1 -0
  22. package/dist/styles.css +1 -0
  23. package/package.json +10 -3
  24. package/src/blocks/AIKit/CollapsibleStepHeader.tsx +7 -8
  25. package/src/blocks/AIKit/InlineStepLabel.tsx +7 -7
  26. package/src/blocks/AIKit/MessageBubble.stories.tsx +11 -3
  27. package/src/blocks/AIKit/MessageBubble.tsx +1 -1
  28. package/src/blocks/AIKit/PlanApproval.tsx +9 -8
  29. package/src/blocks/AIKit/PlanRewritingIndicator.tsx +2 -2
  30. package/src/blocks/AIKit/PromptInput.stories.tsx +10 -2
  31. package/src/blocks/AIKit/PromptInputSubmit.tsx +12 -8
  32. package/src/blocks/AIKit/Question.tsx +8 -9
  33. package/src/blocks/AIKit/SuggestedPrompt.tsx +12 -5
  34. package/src/blocks/AIKit/ThinkingIndicator.tsx +2 -2
  35. package/src/blocks/AutoSaveTextarea/AutoSaveTextarea.tsx +4 -4
  36. package/src/blocks/ClipboardButton/ClipboardButton.tsx +7 -5
  37. package/src/blocks/ClipboardButton/ClipboardInline.tsx +2 -3
  38. package/src/blocks/ColorInput/ColorInput.tsx +4 -2
  39. package/src/blocks/ColorInput/ColorPicker.tsx +5 -2
  40. package/src/blocks/EmptyState/EmptyState.tsx +16 -0
  41. package/src/blocks/ErrorState/ErrorState.tsx +15 -0
  42. package/src/blocks/Form/formHook.ts +15 -0
  43. package/src/blocks/MultiActionButtonGroup/MultiActionButton.stories.tsx +12 -11
  44. package/src/blocks/MultiActionButtonGroup/MultiActionButton.tsx +4 -4
  45. package/src/blocks/MultiCombobox/MultiCombobox.tsx +28 -12
  46. package/src/blocks/PageHeader/AGENTS.md +63 -3
  47. package/src/blocks/PageHeader/PageHeader.stories.tsx +124 -28
  48. package/src/blocks/PageHeader/PageHeader.tsx +15 -0
  49. package/src/blocks/PageHeader/PageHeaderDetail.tsx +23 -1
  50. package/src/blocks/PageHeader/PageHeaderIndex.tsx +20 -1
  51. package/src/blocks/PageLayout/PageLayout.tsx +26 -0
  52. package/src/blocks/PageToolbar/PageToolbar.stories.tsx +348 -0
  53. package/src/blocks/PageToolbar/PageToolbar.tsx +119 -0
  54. package/src/blocks/PageToolbar/index.ts +1 -0
  55. package/src/blocks/PasswordInput/PasswordInput.tsx +10 -10
  56. package/src/blocks/SearchInput/SearchInput.tsx +30 -4
  57. package/src/blocks/SearchInputWithNavigation/SearchInputWithNavigation.tsx +9 -6
  58. package/src/blocks/SplitButton/SplitButton.stories.tsx +7 -4
  59. package/src/blocks/SplitButton/SplitButton.tsx +17 -4
  60. package/src/blocks/Tagger/TagItem.tsx +4 -1
  61. package/src/blocks/Tagger/TagSelectPopover/TagSelectCreateItem.tsx +3 -1
  62. package/src/blocks/Tagger/TagSelectPopover/TagSelectList.tsx +2 -2
  63. package/src/blocks/Tagger/Tagger.tsx +2 -2
  64. package/src/components/Alert/Alert.stories.tsx +11 -3
  65. package/src/components/Alert/Alert.tsx +19 -1
  66. package/src/components/Alert/AlertCollapsibleIcon.tsx +3 -4
  67. package/src/components/Alert/AlertIcon.tsx +18 -21
  68. package/src/components/Badge/Badge.stories.tsx +22 -4
  69. package/src/components/Badge/Badge.tsx +16 -0
  70. package/src/components/Badge/BadgeButton.tsx +7 -6
  71. package/src/components/Breadcrumb/BreadcrumbEllipsis.tsx +2 -2
  72. package/src/components/Breadcrumb/BreadcrumbSeparator.tsx +2 -2
  73. package/src/components/Button/Button.stories.tsx +14 -2
  74. package/src/components/Button/Button.tsx +42 -5
  75. package/src/components/Button/styles.ts +4 -6
  76. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +32 -10
  77. package/src/components/Card/Card.tsx +17 -0
  78. package/src/components/Checkbox/Checkbox.tsx +3 -6
  79. package/src/components/CodeEditor/highlighting.ts +1 -1
  80. package/src/components/CollapsiblePanel.tsx +2 -5
  81. package/src/components/Combobox/Combobox.tsx +12 -14
  82. package/src/components/Combobox/ComboboxChips.tsx +6 -1
  83. package/src/components/Combobox/ComboboxItem.tsx +3 -4
  84. package/src/components/Combobox/ComboboxItemIndicator.tsx +6 -4
  85. package/src/components/Combobox/types.ts +2 -6
  86. package/src/components/Command/Command.stories.tsx +47 -56
  87. package/src/components/Command/CommandItem/CheckIndicator.tsx +26 -14
  88. package/src/components/Command/CommandItem/CommandItem.tsx +6 -3
  89. package/src/components/DropdownMenu/DropdownMenu.stories.tsx +8 -4
  90. package/src/components/DropdownMenu/DropdownMenuCheckboxItem.tsx +4 -1
  91. package/src/components/DropdownMenu/DropdownMenuItem.tsx +12 -6
  92. package/src/components/DropdownMenu/DropdownMenuItemIndicator.tsx +12 -6
  93. package/src/components/DropdownMenu/DropdownMenuRadioGroup/DropdownMenuRadioItem.tsx +8 -5
  94. package/src/components/DropdownMenu/DropdownMenuSub/DropdownMenuSubTrigger.tsx +2 -2
  95. package/src/components/DropdownMenu/ItemIcon.tsx +16 -8
  96. package/src/components/FormGroup/FormGroup.tsx +15 -0
  97. package/src/components/FullScreenModal/FullScreenModalHeader.tsx +4 -1
  98. package/src/components/Input/DateInput/DateInput.tsx +4 -1
  99. package/src/components/Input/DateRangeInput/DateRangeInput.tsx +4 -6
  100. package/src/components/Input/Input.tsx +18 -2
  101. package/src/components/Input/TextInput/TextInput.tsx +16 -0
  102. package/src/components/Input/TimeInput/TimeInput.tsx +4 -1
  103. package/src/components/InputGroup/InputGroup.stories.tsx +11 -3
  104. package/src/components/MarkdownEditor/MarkdownEditor.stories.tsx +63 -1
  105. package/src/components/MarkdownEditor/MarkdownEditor.tsx +105 -15
  106. package/src/components/MarkdownEditor/nodes/ImageNode.tsx +10 -10
  107. package/src/components/MarkdownEditor/nodes/MermaidNode.tsx +8 -8
  108. package/src/components/MarkdownEditor/plugins/KeyboardPlugin.tsx +47 -3
  109. package/src/components/MarkdownEditor/plugins/MarkdownSyncPlugin.tsx +12 -3
  110. package/src/components/MarkdownEditor/plugins/ToolbarPlugin.tsx +20 -10
  111. package/src/components/MarkdownEditor/plugins/toolbar/AlignmentDropdown.tsx +20 -14
  112. package/src/components/MarkdownEditor/plugins/toolbar/BlockTypeDropdown.tsx +25 -16
  113. package/src/components/MarkdownEditor/plugins/toolbar/LinkPanel.tsx +3 -3
  114. package/src/components/MarkdownEditor/transformers.ts +10 -0
  115. package/src/components/MergeEditor/AcceptControls/AcceptButton.tsx +9 -4
  116. package/src/components/MergeEditor/AcceptControls/AcceptControls.tsx +25 -14
  117. package/src/components/MergeEditor/ConflictStatus/ConflictStatusIndicator.tsx +2 -2
  118. package/src/components/MergeEditor/MergeEditor.stories.tsx +15 -0
  119. package/src/components/MergeEditor/MergeEditor.tsx +9 -0
  120. package/src/components/MergeEditor/MergePane.tsx +22 -4
  121. package/src/components/MergeEditor/decorations.ts +2 -2
  122. package/src/components/Modal/Modal.stories.tsx +6 -4
  123. package/src/components/Modal/Modal.tsx +26 -0
  124. package/src/components/Modal/ModalBody.tsx +5 -0
  125. package/src/components/Modal/ModalCloseIcon.tsx +2 -3
  126. package/src/components/Modal/ModalContent.tsx +8 -0
  127. package/src/components/Modal/ModalFooter.tsx +5 -0
  128. package/src/components/Modal/ModalHeader.tsx +11 -0
  129. package/src/components/Modal/ModalIcon.tsx +45 -19
  130. package/src/components/Navbar/Navbar.stories.tsx +26 -13
  131. package/src/components/Navbar/NavbarMenuButton.tsx +11 -10
  132. package/src/components/Pagination/PaginationEllipsis.tsx +2 -2
  133. package/src/components/Pagination/PaginationNext.tsx +2 -1
  134. package/src/components/Pagination/PaginationPrevious.tsx +4 -1
  135. package/src/components/PanelWithDetails.tsx +3 -4
  136. package/src/components/Popconfirm/Popconfirm.stories.tsx +13 -5
  137. package/src/components/RadioGroup/Radio.tsx +6 -12
  138. package/src/components/Search/ClearButton.tsx +2 -2
  139. package/src/components/Search/Search.tsx +3 -3
  140. package/src/components/Select/Select.tsx +16 -5
  141. package/src/components/Select/SelectContent.tsx +13 -4
  142. package/src/components/Select/SelectItem.tsx +6 -5
  143. package/src/components/Select/SelectTrigger.tsx +2 -2
  144. package/src/components/Sidebar/Sidebar.stories.tsx +55 -19
  145. package/src/components/Sidebar/SidebarGroup/SidebarGroupAction.tsx +8 -4
  146. package/src/components/Sidebar/SidebarMenu/SidebarMenuButton.tsx +4 -2
  147. package/src/components/Sidebar/SidebarMenu/SidebarMenuCard.tsx +3 -2
  148. package/src/components/Sidebar/SidebarMenu/SidebarMenuIcon.tsx +12 -12
  149. package/src/components/Sidebar/SidebarMenu/SidebarMenuSub.tsx +3 -7
  150. package/src/components/Sidebar/SidebarMenu/SidebarSearchInput.tsx +3 -5
  151. package/src/components/Skeleton/Skeleton.stories.tsx +52 -0
  152. package/src/components/Skeleton/Skeleton.tsx +24 -1
  153. package/src/components/Table/Table.stories.tsx +223 -1
  154. package/src/components/Table/Table.tsx +17 -0
  155. package/src/components/Tabs.tsx +25 -2
  156. package/src/components/Toast/ToastDismiss.tsx +3 -3
  157. package/src/components/Toast/ToastRoot.tsx +24 -19
  158. package/src/components/Toggle/Toggle.tsx +11 -6
  159. package/src/components/Tooltip/Tooltip.tsx +23 -0
  160. package/src/components/ZoomableSvg/ZoomableSvg.tsx +8 -6
  161. package/src/icons/Keboola.tsx +28 -0
  162. package/src/icons/icons.stories.tsx +174 -0
  163. package/src/icons/index.ts +180 -0
  164. package/src/index.ts +1 -0
  165. package/src/patterns/AlertWithAction.stories.tsx +30 -0
  166. package/src/patterns/CardWithEmptyState.stories.tsx +66 -0
  167. package/src/patterns/ConfirmDialog.stories.tsx +72 -0
  168. package/src/patterns/CreatableMultiCombobox.stories.tsx +33 -0
  169. package/src/patterns/DataCardWithSkeleton.stories.tsx +48 -0
  170. package/src/patterns/DisabledButtonTooltip.stories.tsx +30 -0
  171. package/src/patterns/FilterToolbar.stories.tsx +71 -0
  172. package/src/patterns/InfoTooltip.stories.tsx +56 -0
  173. package/src/patterns/ListDetail.stories.tsx +65 -0
  174. package/src/patterns/ModalForm.stories.tsx +59 -0
  175. package/src/patterns/PageHeaderTabs.stories.tsx +48 -0
  176. package/src/styles.css +16 -8
  177. package/src/utils/legacyIcon.tsx +28 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keboola/design",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Keboola UI platform design system — shadcn-aligned React primitives, Keboola-branded blocks, and the form bindings built on TanStack Form.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,6 +13,7 @@
13
13
  "./styles": "./src/styles.css",
14
14
  "./mermaid": "./src/blocks/MermaidDiagram/index.ts",
15
15
  "./markdown-editor": "./src/components/MarkdownEditor/index.ts",
16
+ "./icons": "./src/icons/index.ts",
16
17
  "./icon-names": "./src/icon-names.ts"
17
18
  },
18
19
  "files": [
@@ -48,6 +49,7 @@
48
49
  "build:js": "tsup",
49
50
  "build:css": "tailwindcss --postcss -c tailwind.config.js -i src/styles.css -o dist/styles.css --minify",
50
51
  "test": "vitest run --passWithNoTests --reporter=dot",
52
+ "test:stories": "vitest run --config vitest.storybook.config.ts",
51
53
  "lint": "yarn lint:format && yarn lint:oxlint",
52
54
  "lint:fix": "oxlint --fix . && oxfmt",
53
55
  "knip:check": "yarn --cwd ../.. exec knip --workspace packages/design",
@@ -65,15 +67,19 @@
65
67
  "@keboola/tsconfig": "workspace:^",
66
68
  "@storybook/addon-docs": "^10.2.19",
67
69
  "@storybook/addon-links": "^10.2.19",
70
+ "@storybook/addon-vitest": "10.2.19",
68
71
  "@storybook/react-vite": "^10.2.19",
69
72
  "@testing-library/dom": "^10.4.1",
70
73
  "@testing-library/react": "^16.3.2",
71
74
  "@testing-library/user-event": "^14.6.1",
72
75
  "@types/react": "^19.2.15",
73
76
  "@vitejs/plugin-react": "^6.0.0",
77
+ "@vitest/browser": "4.1.2",
78
+ "@vitest/browser-playwright": "4.1.2",
74
79
  "eslint-plugin-storybook": "^10.3.5",
75
80
  "oxfmt": "^0.44.0",
76
81
  "oxlint": "^1.56.0",
82
+ "playwright": "1.59.1",
77
83
  "storybook": "^10.2.19",
78
84
  "tailwindcss": "^3.4.17",
79
85
  "tsup": "^8.5.1",
@@ -82,7 +88,7 @@
82
88
  "vitest": "^4.1.2"
83
89
  },
84
90
  "dependencies": {
85
- "@awesome.me/kit-be8bcf7e5e": "^1.0.217",
91
+ "@awesome.me/kit-be8bcf7e5e": "1.0.220",
86
92
  "@base-ui/react": "1.5.0",
87
93
  "@codemirror/autocomplete": "^6.20.1",
88
94
  "@codemirror/commands": "^6.10.3",
@@ -98,7 +104,7 @@
98
104
  "@codemirror/view": "^6.41.0",
99
105
  "@fortawesome/fontawesome-svg-core": "^6.7.2",
100
106
  "@fortawesome/react-fontawesome": "^0.2.2",
101
- "@keboola/codemirror-lang-sfsql": "^1.1.0",
107
+ "@keboola/codemirror-lang-sfsql": "^1.2.0",
102
108
  "@keboola/codemirror-lang-sql": "^0.2.0",
103
109
  "@keboola/tailwind-config": "^0.1.1",
104
110
  "@lexical/code": "^0.42.0",
@@ -138,6 +144,7 @@
138
144
  "cmdk": "^1.1.1",
139
145
  "diff": "^9.0.0",
140
146
  "lexical": "^0.42.0",
147
+ "lucide-react": "^1.17.0",
141
148
  "mermaid": "^11.15.0",
142
149
  "node-diff3": "^3.2.0",
143
150
  "react": "^19.2.6",
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
 
3
- import { Icon } from '../../components/Icon';
3
+ import { ChevronDown, CircleCheck, Loader2, XCircle } from '../../icons';
4
4
  import { cn } from '../../utils';
5
5
 
6
6
  type Variant = 'success' | 'error' | 'processing' | 'dismissed';
@@ -25,15 +25,15 @@ const variantClasses: Record<Variant, string> = {
25
25
 
26
26
  const renderVariantIcon = (variant: Variant) => {
27
27
  if (variant === 'processing') {
28
- return <Icon icon="spinner" spin className="tw-size-4" />;
28
+ return <Loader2 className="tw-size-4 tw-animate-spin" />;
29
29
  }
30
30
  if (variant === 'error') {
31
- return <Icon icon="circle-xmark" className="tw-size-4" />;
31
+ return <XCircle className="tw-size-4" />;
32
32
  }
33
33
  if (variant === 'dismissed') {
34
- return <Icon icon="circle-xmark" className="tw-size-4 tw-text-neutral-500" />;
34
+ return <XCircle className="tw-size-4 tw-text-neutral-500" />;
35
35
  }
36
- return <Icon icon="circle-check" className="tw-size-4" />;
36
+ return <CircleCheck className="tw-size-4" />;
37
37
  };
38
38
 
39
39
  export const CollapsibleStepHeader = ({
@@ -56,9 +56,8 @@ export const CollapsibleStepHeader = ({
56
56
  {renderVariantIcon(variant)}
57
57
  <span>{label}</span>
58
58
  </div>
59
- <Icon
60
- icon="chevron-down"
61
- className={cn('tw-size-3 tw-transition-transform', isExpanded && 'tw-rotate-180')}
59
+ <ChevronDown
60
+ className={cn('tw-size-4 tw-transition-transform', isExpanded && 'tw-rotate-180')}
62
61
  />
63
62
  </button>
64
63
  );
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
 
3
- import { Icon } from '../../components/Icon';
3
+ import { CircleCheck, Loader2, XCircle } from '../../icons';
4
4
 
5
5
  type Variant = 'processing' | 'success' | 'error' | 'dismissed';
6
6
 
@@ -11,23 +11,23 @@ type Props = {
11
11
 
12
12
  const renderVariantIcon = (variant: Variant) => {
13
13
  if (variant === 'processing') {
14
- return <Icon icon="spinner" spin className="tw-size-3 tw-text-neutral-200" />;
14
+ return <Loader2 className="tw-size-4 tw-animate-spin tw-text-neutral-200" />;
15
15
  }
16
16
  if (variant === 'error') {
17
- return <Icon icon="circle-xmark" className="tw-size-3 tw-text-error-500" />;
17
+ return <XCircle className="tw-size-4 tw-text-error-500" />;
18
18
  }
19
19
  if (variant === 'dismissed') {
20
- return <Icon icon="circle-xmark" className="tw-size-3 tw-text-neutral-200" />;
20
+ return <XCircle className="tw-size-4 tw-text-neutral-200" />;
21
21
  }
22
- return <Icon icon="circle-check" className="tw-size-3 tw-text-neutral-200" />;
22
+ return <CircleCheck className="tw-size-4 tw-text-neutral-200" />;
23
23
  };
24
24
 
25
25
  export const InlineStepLabel = ({ variant, children }: Props) => (
26
- <div className="tw-flex tw-items-center tw-gap-1">
26
+ <div className="tw-flex tw-min-w-0 tw-items-center tw-gap-1">
27
27
  <div className="tw-flex tw-size-5 tw-shrink-0 tw-items-center tw-justify-center">
28
28
  {renderVariantIcon(variant)}
29
29
  </div>
30
- <div className="tw-flex tw-flex-wrap tw-items-center tw-gap-x-1.5 tw-gap-y-1 tw-text-sm tw-font-medium tw-italic tw-text-neutral-800">
30
+ <div className="tw-flex tw-min-w-0 tw-flex-wrap tw-items-center tw-gap-x-1.5 tw-gap-y-1 tw-text-sm tw-font-medium tw-italic tw-text-neutral-800">
31
31
  {children}
32
32
  </div>
33
33
  </div>
@@ -1,5 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
 
3
+ import { Copy, ThumbsDown, ThumbsUp } from '@keboola/design/icons';
4
+
3
5
  import { Button } from '../../components/Button';
4
6
 
5
7
  import { MessageBubble } from './MessageBubble';
@@ -66,9 +68,15 @@ export const AssistantWithActions: Story = {
66
68
  ),
67
69
  actions: (
68
70
  <div className="tw-flex tw-gap-2">
69
- <Button size="icon-sm" variant="outline" icon="copy" aria-label="Copy" />
70
- <Button size="icon-sm" variant="outline" icon="thumbs-up" aria-label="Helpful" />
71
- <Button size="icon-sm" variant="outline" icon="thumbs-down" aria-label="Not helpful" />
71
+ <Button size="icon-sm" variant="outline" aria-label="Copy">
72
+ <Copy className="tw-size-4" />
73
+ </Button>
74
+ <Button size="icon-sm" variant="outline" aria-label="Helpful">
75
+ <ThumbsUp className="tw-size-4" />
76
+ </Button>
77
+ <Button size="icon-sm" variant="outline" aria-label="Not helpful">
78
+ <ThumbsDown className="tw-size-4" />
79
+ </Button>
72
80
  </div>
73
81
  ),
74
82
  },
@@ -28,7 +28,7 @@ const contentClasses = cva(
28
28
  {
29
29
  variants: {
30
30
  variant: {
31
- assistant: ['tw-flex-1', 'tw-w-full', 'tw-min-w-0'],
31
+ assistant: ['tw-flex-1', 'tw-w-full', 'tw-min-w-0', '[&>*]:tw-min-w-0'],
32
32
  user: [
33
33
  'tw-max-w-screen-sm',
34
34
  'tw-w-fit',
@@ -2,7 +2,7 @@ import { type FormEvent, type KeyboardEvent, type ReactNode, useState } from 're
2
2
 
3
3
  import { Badge } from '../../components/Badge';
4
4
  import { Button } from '../../components/Button';
5
- import { Icon } from '../../components/Icon';
5
+ import { ChevronDown, Send, X } from '../../icons';
6
6
  import { cn } from '../../utils';
7
7
  import { Textarea } from '../Textarea';
8
8
 
@@ -102,9 +102,8 @@ const ExpandToggle = ({
102
102
  className="tailwindcss-preflight tw-inline-flex tw-cursor-pointer tw-items-center tw-gap-1 tw-bg-transparent tw-p-0 tw-text-sm tw-font-medium tw-text-secondary-500 hover:tw-text-secondary-600 focus-visible:tw-outline focus-visible:tw-outline-2 focus-visible:tw-outline-offset-2 focus-visible:tw-outline-secondary-500"
103
103
  >
104
104
  <span>{isExpanded ? showLessLabel : showMoreLabel}</span>
105
- <Icon
106
- icon="chevron-down"
107
- className={cn('tw-size-3 tw-transition-transform', isExpanded && 'tw-rotate-180')}
105
+ <ChevronDown
106
+ className={cn('tw-size-5 tw-transition-transform', isExpanded && 'tw-rotate-180')}
108
107
  />
109
108
  </button>
110
109
  );
@@ -221,12 +220,13 @@ export const PlanApproval = ({
221
220
  argo
222
221
  variant="outline"
223
222
  size="icon-sm"
224
- icon={['far', 'xmark']}
225
223
  aria-label={closeLabel}
226
224
  onClick={onClose}
227
225
  disabled={isSubmitting}
228
226
  data-testid="plan-close-button"
229
- />
227
+ >
228
+ <X className="tw-size-4" />
229
+ </Button>
230
230
  )}
231
231
  </div>
232
232
 
@@ -271,12 +271,13 @@ export const PlanApproval = ({
271
271
  type="submit"
272
272
  variant="invisible"
273
273
  size="icon-sm"
274
- icon="paper-plane-top"
275
274
  aria-label={changeRequestSubmitLabel}
276
275
  disabled={!canSubmitChangeRequest}
277
276
  className="tw-absolute tw-right-2 tw-top-1/2 tw--translate-y-1/2 [&_svg]:tw-text-secondary-500 not-disabled:hover:[&_svg]:tw-text-secondary-600"
278
277
  data-testid="plan-change-request-submit"
279
- />
278
+ >
279
+ <Send className="tw-size-4" />
280
+ </Button>
280
281
  </div>
281
282
  </form>
282
283
  )}
@@ -1,4 +1,4 @@
1
- import { Icon } from '../../components/Icon';
1
+ import { Loader2 } from '../../icons';
2
2
 
3
3
  type PlanRewritingIndicatorProps = {
4
4
  label?: string;
@@ -12,7 +12,7 @@ export const PlanRewritingIndicator = ({
12
12
  role="status"
13
13
  data-testid="plan-rewriting-indicator"
14
14
  >
15
- <Icon icon="spinner" spin className="tw-size-4 tw-text-secondary-500" />
15
+ <Loader2 className="tw-size-4 tw-animate-spin tw-text-secondary-500" />
16
16
  <span className="tw-text-sm tw-italic tw-text-secondary-500">{label}</span>
17
17
  </div>
18
18
  );
@@ -2,6 +2,8 @@ import { useState } from 'react';
2
2
  import type { ComponentProps } from 'react';
3
3
  import type { Meta, StoryObj } from '@storybook/react-vite';
4
4
 
5
+ import { Upload } from '@keboola/design/icons';
6
+
5
7
  import { Button } from '../../components/Button';
6
8
  import { DropdownMenu } from '../../components/DropdownMenu';
7
9
 
@@ -47,7 +49,9 @@ export default {
47
49
  disabled: false,
48
50
  suffix: (
49
51
  <PromptInput.Action>
50
- <Button size="icon-sm" variant="invisible" icon="upload" />
52
+ <Button size="icon-sm" variant="invisible">
53
+ <Upload className="tw-size-4" />
54
+ </Button>
51
55
  </PromptInput.Action>
52
56
  ),
53
57
 
@@ -87,7 +91,11 @@ export const Inline: Story = {
87
91
 
88
92
  export const Actions: Story = {
89
93
  args: {
90
- suffix: <Button size="icon-3xs" variant="inline" icon="upload" />,
94
+ suffix: (
95
+ <Button size="icon-3xs" variant="inline">
96
+ <Upload className="tw-size-4" />
97
+ </Button>
98
+ ),
91
99
  },
92
100
  };
93
101
 
@@ -1,7 +1,8 @@
1
- import type { ComponentProps, ElementRef, Ref } from 'react';
1
+ import type { ComponentProps, ElementRef, ReactNode, Ref } from 'react';
2
2
 
3
3
  import { Button } from '../../components/Button';
4
4
  import { Tooltip } from '../../components/Tooltip';
5
+ import { CirclePause, Play, Send } from '../../icons';
5
6
 
6
7
  import { PromptInputAction } from './PromptInputAction';
7
8
 
@@ -16,11 +17,11 @@ export const PromptInputSubmit = ({
16
17
  disabled,
17
18
  status = 'ready',
18
19
  }: Props & { ref?: Ref<ElementRef<'button'>> }) => {
19
- const getIcon = () => {
20
- if (status === 'streaming') return 'stop-circle';
21
- if (status === 'interrupted') return 'play';
22
- return 'paper-plane-top';
23
- };
20
+ const icon: ReactNode = (() => {
21
+ if (status === 'streaming') return <CirclePause className="tw-size-4" />;
22
+ if (status === 'interrupted') return <Play className="tw-size-4" />;
23
+ return <Send className="tw-size-4" />;
24
+ })();
24
25
 
25
26
  const getTooltipText = () => {
26
27
  if (status === 'streaming') return 'Stop';
@@ -39,9 +40,12 @@ export const PromptInputSubmit = ({
39
40
  size="icon-sm"
40
41
  variant="invisible"
41
42
  isLoading={status === 'submitted'}
42
- icon={getIcon()}
43
43
  className="tw-ml-auto tw-flex tw-items-center tw-rounded-lg [&_svg]:tw-text-secondary-500 not-disabled:hover:[&_svg]:tw-text-secondary-600"
44
- />
44
+ >
45
+ {/* Suppress the child icon while isLoading — Button already renders
46
+ the spinner; rendering both would double-stack glyphs. */}
47
+ {status !== 'submitted' && icon}
48
+ </Button>
45
49
  </PromptInputAction>
46
50
  </Tooltip.Trigger>
47
51
 
@@ -2,9 +2,9 @@ import { type KeyboardEvent, type ReactNode, useState } from 'react';
2
2
 
3
3
  import { Button } from '../../components/Button';
4
4
  import { Checkbox } from '../../components/Checkbox';
5
- import { Icon } from '../../components/Icon';
6
5
  import { TextInput } from '../../components/Input';
7
6
  import { Progress } from '../../components/Progress';
7
+ import { ChevronLeft, ChevronRight, X } from '../../icons';
8
8
  import { cn } from '../../utils';
9
9
 
10
10
  type QuestionOption = {
@@ -142,11 +142,12 @@ export const Question = ({
142
142
  argo
143
143
  variant="outline"
144
144
  size="icon-sm"
145
- icon={['far', 'xmark']}
146
145
  aria-label="Close"
147
146
  onClick={onClose}
148
147
  data-testid="question-close-button"
149
- />
148
+ >
149
+ <X className="tw-size-4" />
150
+ </Button>
150
151
  )}
151
152
  </div>
152
153
 
@@ -264,10 +265,9 @@ export const Question = ({
264
265
  onClick={onPrevious}
265
266
  aria-label="Previous step"
266
267
  >
267
- <Icon
268
- icon="chevron-left"
268
+ <ChevronLeft
269
269
  className={cn(
270
- 'tw-size-4',
270
+ 'tw-size-5',
271
271
  isFirstStep ? 'tw-text-primary/25' : 'tw-text-primary',
272
272
  )}
273
273
  />
@@ -282,10 +282,9 @@ export const Question = ({
282
282
  onClick={onNext}
283
283
  aria-label="Next step"
284
284
  >
285
- <Icon
286
- icon="chevron-right"
285
+ <ChevronRight
287
286
  className={cn(
288
- 'tw-size-4',
287
+ 'tw-size-5',
289
288
  currentStep === totalSteps ? 'tw-text-primary/25' : 'tw-text-primary',
290
289
  )}
291
290
  />
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
 
3
- import { Icon } from '../../components/Icon';
3
+ import { FileText } from '../../icons';
4
4
  import { cn } from '../../utils';
5
5
 
6
6
  type Props = {
@@ -23,10 +23,17 @@ export const SuggestedPrompt = ({ label, disabled, onClick, icon, className }: P
23
23
  onClick={onClick}
24
24
  >
25
25
  {icon ?? (
26
- <Icon
27
- className="group-not-disabled:group-hover:[&_path]:tw-fill-secondary-600 tw-size-4 tw-shrink-0 tw-basis-4 [&_path]:tw-fill-aigradient-600"
28
- fixedWidth
29
- icon={['far', 'file-lines']}
26
+ <FileText
27
+ className={cn(
28
+ 'tw-size-4 tw-shrink-0 tw-basis-4',
29
+ // Lucide icons are stroke-based — paint the visible line with the
30
+ // AI gradient (no fill, so the glyph keeps its outline silhouette
31
+ // instead of becoming a solid block). The `*` selector catches
32
+ // every shape element (path, circle, polyline, …) so multi-part
33
+ // icons don't lose sub-shapes the way `path`-only selectors do.
34
+ '[&_*]:tw-stroke-aigradient-600',
35
+ 'group-not-disabled:group-hover:[&_*]:tw-stroke-secondary-600',
36
+ )}
30
37
  />
31
38
  )}
32
39
  <div className="tw-whitespace-normal tw-text-left">{label}</div>
@@ -1,4 +1,4 @@
1
- import { Icon } from '../../components/Icon';
1
+ import { Loader2 } from '../../icons';
2
2
 
3
3
  type ThinkingIndicatorProps = {
4
4
  label?: string;
@@ -7,7 +7,7 @@ type ThinkingIndicatorProps = {
7
7
  export const ThinkingIndicator = ({ label = 'Thinking...' }: ThinkingIndicatorProps) => {
8
8
  return (
9
9
  <div className="tw-flex tw-items-center tw-gap-2" data-testid="chat-thinking-indicator">
10
- <Icon icon="spinner" spin className="tw-size-4 tw-text-secondary-500" />
10
+ <Loader2 className="tw-size-4 tw-animate-spin tw-text-secondary-500" />
11
11
  <span className="tw-text-sm tw-italic tw-text-secondary-500">{label}</span>
12
12
  </div>
13
13
  );
@@ -1,6 +1,6 @@
1
1
  import { type ComponentProps, useState } from 'react';
2
2
 
3
- import { Icon } from '../../components/Icon';
3
+ import { CircleCheck, Loader2 } from '../../icons';
4
4
  import { cn } from '../../utils';
5
5
  import { Textarea } from '../Textarea';
6
6
 
@@ -83,17 +83,17 @@ export const AutoSaveTextarea = ({
83
83
  {currentValue.length} / {maxLength}
84
84
  </span>
85
85
  )}
86
- <span className="tw-text-neutral-400">
86
+ <span className="tw-flex tw-items-center tw-gap-1 tw-text-neutral-400">
87
87
  {isOverLimit && 'Character limit exceeded'}
88
88
  {!isOverLimit && isChanged && !isSaving && 'Unsaved'}
89
89
  {!isOverLimit && isChanged && isSaving && (
90
90
  <>
91
- <Icon spin icon="spinner" className="tw-mr-1" /> Saving
91
+ <Loader2 className="tw-size-4 tw-animate-spin" /> Saving
92
92
  </>
93
93
  )}
94
94
  {!isOverLimit && value !== null && !isChanged && !isSaving && (
95
95
  <>
96
- <Icon icon="check-circle" className="tw-text-primary-500" /> Saved
96
+ <CircleCheck className="tw-size-4 tw-text-primary-500" /> Saved
97
97
  </>
98
98
  )}
99
99
  </span>
@@ -1,18 +1,19 @@
1
- import type { ComponentProps } from 'react';
1
+ import type { ComponentProps, ReactNode } from 'react';
2
2
 
3
3
  import { Button } from '../../components/Button';
4
4
  import { Clipboard } from '../../components/Clipboard';
5
+ import { Copy } from '../../icons';
5
6
 
6
7
  type ClipboardProps = Pick<
7
8
  ComponentProps<typeof Clipboard>,
8
9
  'text' | 'tooltip' | 'tooltipPlacement' | 'tooltipVariant'
9
10
  >;
10
11
 
11
- type ButtonProps = Omit<ComponentProps<typeof Button>, 'disabled'>;
12
+ type ButtonProps = Omit<ComponentProps<typeof Button>, 'disabled' | 'icon'>;
12
13
 
13
- type Props = ClipboardProps & ButtonProps;
14
+ type Props = ClipboardProps & ButtonProps & { icon?: ReactNode };
14
15
 
15
- const DEFAULT_ICON: Props['icon'] = 'copy';
16
+ const DEFAULT_ICON: ReactNode = <Copy />;
16
17
 
17
18
  export const ClipboardButton = ({
18
19
  // clipboard props
@@ -34,7 +35,8 @@ export const ClipboardButton = ({
34
35
  tooltipVariant={tooltipVariant}
35
36
  text={text}
36
37
  >
37
- <Button {...rest} icon={icon} variant={variant} disabled={!text}>
38
+ <Button {...rest} variant={variant} disabled={!text}>
39
+ {icon}
38
40
  {children}
39
41
  </Button>
40
42
  </Clipboard>
@@ -1,9 +1,8 @@
1
1
  import type { ComponentProps } from 'react';
2
2
 
3
- import { Icon } from '@keboola/design';
4
-
5
3
  import { ButtonInline } from '../../components/Button';
6
4
  import { Clipboard } from '../../components/Clipboard';
5
+ import { Copy } from '../../icons';
7
6
 
8
7
  type ClipboardProps = Pick<
9
8
  ComponentProps<typeof Clipboard>,
@@ -34,7 +33,7 @@ export const ClipboardInline = ({
34
33
  tooltipVariant={tooltipVariant}
35
34
  >
36
35
  <ButtonInline {...rest} disabled={!text}>
37
- {!hideIcon && <Icon icon="copy" />}
36
+ {!hideIcon && <Copy className="tw-size-4" />}
38
37
  {children}
39
38
  </ButtonInline>
40
39
  </Clipboard>
@@ -9,6 +9,7 @@ import type {
9
9
  } from 'react';
10
10
 
11
11
  import { InputGroup } from '../../components/InputGroup';
12
+ import { X } from '../../icons';
12
13
  import { cn, createChangeInputEvent, mergeRefs } from '../../utils';
13
14
 
14
15
  import { ColorPicker } from './ColorPicker';
@@ -150,12 +151,13 @@ export const ColorInput = ({
150
151
  <InputGroup.Addon align="inline-end">
151
152
  {canClear && (
152
153
  <InputGroup.Button
153
- icon="xmark"
154
154
  // prevents input focus stealing
155
155
  onMouseDown={(event) => event.preventDefault()}
156
156
  onClick={handleClear}
157
157
  aria-label="Clear color"
158
- />
158
+ >
159
+ <X className="tw-size-4" />
160
+ </InputGroup.Button>
159
161
  )}
160
162
  <ColorPicker
161
163
  disabled={disabled}
@@ -1,5 +1,7 @@
1
1
  import { type ComponentProps, type ElementRef, useRef } from 'react';
2
2
 
3
+ import { Pipette } from '@keboola/design/icons';
4
+
3
5
  import { InputGroup } from '../../components/InputGroup';
4
6
 
5
7
  import { coerceColorPickerValue, showPicker } from './utils';
@@ -30,10 +32,11 @@ export const ColorPicker = ({ value, disabled, readOnly, ...props }: Props) => {
30
32
  <InputGroup.Button
31
33
  onMouseDown={(event) => event.preventDefault()}
32
34
  disabled={disabled || readOnly}
33
- icon="eye-dropper"
34
35
  onClick={handleOpenPicker}
35
36
  aria-label="Pick color"
36
- />
37
+ >
38
+ <Pipette className="tw-size-4" />
39
+ </InputGroup.Button>
37
40
  </div>
38
41
  );
39
42
  };
@@ -67,11 +67,27 @@ const emptyStateActionsClassnames = cva(['tw-flex', 'tw-items-center', 'tw-flex-
67
67
 
68
68
  type Variant = VariantProps<typeof emptyStateClassnames>['variant'];
69
69
 
70
+ /**
71
+ * Props for `EmptyState`. `variant` chooses spacing — `"default"` for full-page empty states,
72
+ * `"compact"` for inside cards / small surfaces.
73
+ */
70
74
  type RootProps = ComponentProps<'div'> &
71
75
  VariantProps<typeof emptyStateClassnames> & {
72
76
  variant?: Variant;
73
77
  };
74
78
 
79
+ /**
80
+ * Friendly "nothing here yet" placeholder with icon, title, description, and actions slots.
81
+ *
82
+ * **When to use:** the user reached a page or surface that is intentionally empty (no items created
83
+ * yet, an empty list after filtering, an empty inbox). The goal is to guide the user to the next action.
84
+ * **When NOT to use:** for an error or failure state use `ErrorState`; for inline loading placeholders
85
+ * use `Skeleton`; for "no matches found after search" inside a search/combobox use `EmptySearchContent`.
86
+ * **Composition:** nest `EmptyState.Icon`, `EmptyState.Title`, `EmptyState.Description`, and
87
+ * `EmptyState.Actions` (with one or two `Button`s). Use `variant="compact"` inside a `Card`.
88
+ *
89
+ * @see RECIPES.md#card-with-empty-state — for the empty-card pattern
90
+ */
75
91
  const EmptyStateRoot = ({ className, variant = 'default', children, ...rest }: RootProps) => (
76
92
  <div role="status" className={emptyStateClassnames({ className, variant })} {...rest}>
77
93
  {children}
@@ -69,11 +69,26 @@ const errorStateActionsClassnames = cva(['tw-flex', 'tw-items-center', 'tw-flex-
69
69
 
70
70
  type Variant = VariantProps<typeof errorStateClassnames>['variant'];
71
71
 
72
+ /**
73
+ * Props for `ErrorState`. `variant` chooses look — `"default"` for non-blocking errors, `"compact"` for
74
+ * inside cards, `"destructive"` for blocking failures (red title + icon).
75
+ */
72
76
  type RootProps = ComponentProps<'div'> &
73
77
  VariantProps<typeof errorStateClassnames> & {
74
78
  variant?: Variant;
75
79
  };
76
80
 
81
+ /**
82
+ * Page-level error placeholder with icon, title, description, and recovery-action slots.
83
+ *
84
+ * **When to use:** the page can't render its content because a request failed, the user lacks permission,
85
+ * or the entity doesn't exist. Show a recovery action (retry, go back, request access) in
86
+ * `ErrorState.Actions`.
87
+ * **When NOT to use:** for transient operation failures use `toast()`; for inline form validation use
88
+ * `FormGroup state="error"`; for contextual warning banners use `Alert variant="error"`.
89
+ * **Composition:** nest `ErrorState.Icon`, `ErrorState.Title`, `ErrorState.Description`, and
90
+ * `ErrorState.Actions`. Use `variant="destructive"` for full-page critical failures.
91
+ */
77
92
  const ErrorStateRoot = ({ className, variant = 'default', children, ...rest }: RootProps) => (
78
93
  <div role="alert" className={errorStateClassnames({ className, variant })} {...rest}>
79
94
  {children}
@@ -11,6 +11,21 @@ import { TextAreaField } from './connectors/TextAreaField';
11
11
  import { TextField } from './connectors/TextField';
12
12
  import { fieldContext, formContext } from './contexts';
13
13
 
14
+ /**
15
+ * Typed form hook + `withForm` binding for the design system.
16
+ *
17
+ * **When to use:** any form layout that has validation, server-side errors, or a submit state — pages,
18
+ * modals, drawers, multi-step wizards. Use `useAppForm({ defaultValues, validators, onSubmit })` at the
19
+ * top of the form component, then render the typed `field.TextField` / `field.NumberField` /
20
+ * `field.PasswordField` / `field.TextAreaField` / `field.SelectField` / `field.CheckboxField` /
21
+ * `field.SwitchField` connectors inside `form.AppForm` → `form.FormBody`. Submit via `form.SubmitButton`.
22
+ * **When NOT to use:** for a single uncontrolled `<input>` use `TextInput` directly; for a bare label + input
23
+ * pair without validation use `FormGroup` + `FormGroup.TextInput`.
24
+ * **Composition:** schema-library agnostic — pass any [Standard Schema](https://standardschema.dev/) validator
25
+ * (zod, valibot, …) in the consuming app. Server errors come from `setServerFieldErrors` (also re-exported).
26
+ *
27
+ * @see RECIPES.md#modal-form — for the canonical form-in-modal composition
28
+ */
14
29
  export const { useAppForm, withForm } = createFormHook({
15
30
  fieldContext,
16
31
  formContext,