@keboola/design 1.2.0 → 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 (173) hide show
  1. package/README.md +25 -0
  2. package/dist/icons.cjs +731 -0
  3. package/dist/icons.cjs.map +1 -0
  4. package/dist/icons.js +29 -0
  5. package/dist/icons.js.map +1 -0
  6. package/dist/index.cjs +429 -333
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +426 -331
  9. package/dist/index.js.map +1 -1
  10. package/dist/markdown-editor.cjs +270 -135
  11. package/dist/markdown-editor.cjs.map +1 -1
  12. package/dist/markdown-editor.js +270 -135
  13. package/dist/markdown-editor.js.map +1 -1
  14. package/dist/mermaid.cjs +24 -15
  15. package/dist/mermaid.cjs.map +1 -1
  16. package/dist/mermaid.js +24 -15
  17. package/dist/mermaid.js.map +1 -1
  18. package/dist/styles.css +1 -1
  19. package/package.json +12 -5
  20. package/src/blocks/AIKit/CollapsibleStepHeader.tsx +7 -8
  21. package/src/blocks/AIKit/InlineStepLabel.tsx +7 -7
  22. package/src/blocks/AIKit/MessageBubble.stories.tsx +11 -3
  23. package/src/blocks/AIKit/MessageBubble.tsx +1 -1
  24. package/src/blocks/AIKit/PlanApproval.tsx +9 -8
  25. package/src/blocks/AIKit/PlanRewritingIndicator.tsx +2 -2
  26. package/src/blocks/AIKit/PromptInput.stories.tsx +10 -2
  27. package/src/blocks/AIKit/PromptInputSubmit.tsx +12 -8
  28. package/src/blocks/AIKit/Question.tsx +8 -9
  29. package/src/blocks/AIKit/SuggestedPrompt.tsx +12 -5
  30. package/src/blocks/AIKit/ThinkingIndicator.tsx +2 -2
  31. package/src/blocks/AutoSaveTextarea/AutoSaveTextarea.tsx +4 -4
  32. package/src/blocks/ClipboardButton/ClipboardButton.tsx +7 -5
  33. package/src/blocks/ClipboardButton/ClipboardInline.tsx +2 -3
  34. package/src/blocks/ColorInput/ColorInput.tsx +4 -2
  35. package/src/blocks/ColorInput/ColorPicker.tsx +5 -2
  36. package/src/blocks/EmptyState/EmptyState.tsx +16 -0
  37. package/src/blocks/ErrorState/ErrorState.tsx +15 -0
  38. package/src/blocks/Form/formHook.ts +15 -0
  39. package/src/blocks/MultiActionButtonGroup/MultiActionButton.stories.tsx +12 -11
  40. package/src/blocks/MultiActionButtonGroup/MultiActionButton.tsx +4 -4
  41. package/src/blocks/MultiCombobox/MultiCombobox.tsx +28 -12
  42. package/src/blocks/PageHeader/AGENTS.md +63 -3
  43. package/src/blocks/PageHeader/PageHeader.stories.tsx +124 -28
  44. package/src/blocks/PageHeader/PageHeader.tsx +15 -0
  45. package/src/blocks/PageHeader/PageHeaderDetail.tsx +23 -1
  46. package/src/blocks/PageHeader/PageHeaderIndex.tsx +20 -1
  47. package/src/blocks/PageLayout/PageLayout.tsx +26 -0
  48. package/src/blocks/PageToolbar/PageToolbar.stories.tsx +348 -0
  49. package/src/blocks/PageToolbar/PageToolbar.tsx +119 -0
  50. package/src/blocks/PageToolbar/index.ts +1 -0
  51. package/src/blocks/PasswordInput/PasswordInput.tsx +10 -10
  52. package/src/blocks/SearchInput/SearchInput.tsx +30 -4
  53. package/src/blocks/SearchInputWithNavigation/SearchInputWithNavigation.tsx +9 -6
  54. package/src/blocks/SplitButton/SplitButton.stories.tsx +7 -4
  55. package/src/blocks/SplitButton/SplitButton.tsx +17 -4
  56. package/src/blocks/Tagger/TagItem.tsx +4 -1
  57. package/src/blocks/Tagger/TagSelectPopover/TagSelectCreateItem.tsx +3 -1
  58. package/src/blocks/Tagger/TagSelectPopover/TagSelectList.tsx +2 -2
  59. package/src/blocks/Tagger/Tagger.tsx +2 -2
  60. package/src/components/Alert/Alert.stories.tsx +11 -3
  61. package/src/components/Alert/Alert.tsx +19 -1
  62. package/src/components/Alert/AlertCollapsibleIcon.tsx +3 -4
  63. package/src/components/Alert/AlertIcon.tsx +18 -21
  64. package/src/components/Badge/Badge.stories.tsx +22 -4
  65. package/src/components/Badge/Badge.tsx +16 -0
  66. package/src/components/Badge/BadgeButton.tsx +7 -6
  67. package/src/components/Breadcrumb/BreadcrumbEllipsis.tsx +2 -2
  68. package/src/components/Breadcrumb/BreadcrumbSeparator.tsx +2 -2
  69. package/src/components/Button/Button.stories.tsx +14 -2
  70. package/src/components/Button/Button.tsx +42 -5
  71. package/src/components/Button/styles.ts +4 -6
  72. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +32 -10
  73. package/src/components/Card/Card.tsx +17 -0
  74. package/src/components/Checkbox/Checkbox.tsx +3 -6
  75. package/src/components/CodeEditor/highlighting.ts +1 -1
  76. package/src/components/CollapsiblePanel.tsx +2 -5
  77. package/src/components/Combobox/Combobox.tsx +12 -14
  78. package/src/components/Combobox/ComboboxChips.tsx +6 -1
  79. package/src/components/Combobox/ComboboxItem.tsx +3 -4
  80. package/src/components/Combobox/ComboboxItemIndicator.tsx +6 -4
  81. package/src/components/Combobox/types.ts +2 -6
  82. package/src/components/Command/Command.stories.tsx +47 -56
  83. package/src/components/Command/CommandItem/CheckIndicator.tsx +26 -14
  84. package/src/components/Command/CommandItem/CommandItem.tsx +6 -3
  85. package/src/components/DropdownMenu/DropdownMenu.stories.tsx +8 -4
  86. package/src/components/DropdownMenu/DropdownMenuCheckboxItem.tsx +4 -1
  87. package/src/components/DropdownMenu/DropdownMenuItem.tsx +12 -6
  88. package/src/components/DropdownMenu/DropdownMenuItemIndicator.tsx +12 -6
  89. package/src/components/DropdownMenu/DropdownMenuRadioGroup/DropdownMenuRadioItem.tsx +8 -5
  90. package/src/components/DropdownMenu/DropdownMenuSub/DropdownMenuSubTrigger.tsx +2 -2
  91. package/src/components/DropdownMenu/ItemIcon.tsx +16 -8
  92. package/src/components/FormGroup/FormGroup.tsx +15 -0
  93. package/src/components/FullScreenModal/FullScreenModalHeader.tsx +4 -1
  94. package/src/components/Input/DateInput/DateInput.tsx +4 -1
  95. package/src/components/Input/DateRangeInput/DateRangeInput.tsx +4 -6
  96. package/src/components/Input/Input.tsx +18 -2
  97. package/src/components/Input/TextInput/TextInput.tsx +16 -0
  98. package/src/components/Input/TimeInput/TimeInput.tsx +4 -1
  99. package/src/components/InputGroup/InputGroup.stories.tsx +11 -3
  100. package/src/components/MarkdownEditor/MarkdownEditor.stories.tsx +63 -1
  101. package/src/components/MarkdownEditor/MarkdownEditor.tsx +105 -15
  102. package/src/components/MarkdownEditor/nodes/ImageNode.tsx +10 -10
  103. package/src/components/MarkdownEditor/nodes/MermaidNode.tsx +8 -8
  104. package/src/components/MarkdownEditor/plugins/KeyboardPlugin.tsx +47 -3
  105. package/src/components/MarkdownEditor/plugins/MarkdownSyncPlugin.tsx +12 -3
  106. package/src/components/MarkdownEditor/plugins/ToolbarPlugin.tsx +20 -10
  107. package/src/components/MarkdownEditor/plugins/toolbar/AlignmentDropdown.tsx +20 -14
  108. package/src/components/MarkdownEditor/plugins/toolbar/BlockTypeDropdown.tsx +25 -16
  109. package/src/components/MarkdownEditor/plugins/toolbar/LinkPanel.tsx +3 -3
  110. package/src/components/MarkdownEditor/transformers.ts +10 -0
  111. package/src/components/MergeEditor/AcceptControls/AcceptButton.tsx +9 -4
  112. package/src/components/MergeEditor/AcceptControls/AcceptControls.tsx +25 -14
  113. package/src/components/MergeEditor/ConflictStatus/ConflictStatusIndicator.tsx +2 -2
  114. package/src/components/MergeEditor/MergeEditor.stories.tsx +15 -0
  115. package/src/components/MergeEditor/MergeEditor.tsx +9 -0
  116. package/src/components/MergeEditor/MergePane.tsx +22 -4
  117. package/src/components/MergeEditor/decorations.ts +2 -2
  118. package/src/components/Modal/Modal.stories.tsx +6 -4
  119. package/src/components/Modal/Modal.tsx +26 -0
  120. package/src/components/Modal/ModalBody.tsx +5 -0
  121. package/src/components/Modal/ModalCloseIcon.tsx +2 -3
  122. package/src/components/Modal/ModalContent.tsx +8 -0
  123. package/src/components/Modal/ModalFooter.tsx +5 -0
  124. package/src/components/Modal/ModalHeader.tsx +11 -0
  125. package/src/components/Modal/ModalIcon.tsx +45 -19
  126. package/src/components/Navbar/Navbar.stories.tsx +26 -13
  127. package/src/components/Navbar/NavbarMenuButton.tsx +11 -10
  128. package/src/components/Pagination/PaginationEllipsis.tsx +2 -2
  129. package/src/components/Pagination/PaginationNext.tsx +2 -1
  130. package/src/components/Pagination/PaginationPrevious.tsx +4 -1
  131. package/src/components/PanelWithDetails.tsx +3 -4
  132. package/src/components/Popconfirm/Popconfirm.stories.tsx +13 -5
  133. package/src/components/RadioGroup/Radio.tsx +6 -12
  134. package/src/components/Search/ClearButton.tsx +2 -2
  135. package/src/components/Search/Search.tsx +3 -3
  136. package/src/components/Select/Select.tsx +16 -5
  137. package/src/components/Select/SelectContent.tsx +13 -4
  138. package/src/components/Select/SelectItem.tsx +6 -5
  139. package/src/components/Select/SelectTrigger.tsx +2 -2
  140. package/src/components/Sidebar/Sidebar.stories.tsx +55 -19
  141. package/src/components/Sidebar/SidebarGroup/SidebarGroupAction.tsx +8 -4
  142. package/src/components/Sidebar/SidebarMenu/SidebarMenuButton.tsx +4 -2
  143. package/src/components/Sidebar/SidebarMenu/SidebarMenuCard.tsx +3 -2
  144. package/src/components/Sidebar/SidebarMenu/SidebarMenuIcon.tsx +12 -12
  145. package/src/components/Sidebar/SidebarMenu/SidebarMenuSub.tsx +3 -7
  146. package/src/components/Sidebar/SidebarMenu/SidebarSearchInput.tsx +3 -5
  147. package/src/components/Skeleton/Skeleton.stories.tsx +52 -0
  148. package/src/components/Skeleton/Skeleton.tsx +24 -1
  149. package/src/components/Table/Table.stories.tsx +223 -1
  150. package/src/components/Table/Table.tsx +17 -0
  151. package/src/components/Tabs.tsx +25 -2
  152. package/src/components/Toast/ToastDismiss.tsx +3 -3
  153. package/src/components/Toast/ToastRoot.tsx +24 -19
  154. package/src/components/Toggle/Toggle.tsx +11 -6
  155. package/src/components/Tooltip/Tooltip.tsx +23 -0
  156. package/src/components/ZoomableSvg/ZoomableSvg.tsx +8 -6
  157. package/src/icons/Keboola.tsx +28 -0
  158. package/src/icons/icons.stories.tsx +174 -0
  159. package/src/icons/index.ts +180 -0
  160. package/src/index.ts +1 -0
  161. package/src/patterns/AlertWithAction.stories.tsx +30 -0
  162. package/src/patterns/CardWithEmptyState.stories.tsx +66 -0
  163. package/src/patterns/ConfirmDialog.stories.tsx +72 -0
  164. package/src/patterns/CreatableMultiCombobox.stories.tsx +33 -0
  165. package/src/patterns/DataCardWithSkeleton.stories.tsx +48 -0
  166. package/src/patterns/DisabledButtonTooltip.stories.tsx +30 -0
  167. package/src/patterns/FilterToolbar.stories.tsx +71 -0
  168. package/src/patterns/InfoTooltip.stories.tsx +56 -0
  169. package/src/patterns/ListDetail.stories.tsx +65 -0
  170. package/src/patterns/ModalForm.stories.tsx +59 -0
  171. package/src/patterns/PageHeaderTabs.stories.tsx +48 -0
  172. package/src/styles.css +16 -8
  173. package/src/utils/legacyIcon.tsx +28 -0
@@ -1,5 +1,5 @@
1
- import { useState } from 'react';
2
- import type { ReactNode } from 'react';
1
+ import { useRef, useState } from 'react';
2
+ import type { FocusEvent, ReactNode } from 'react';
3
3
  import { CodeNode } from '@lexical/code';
4
4
  import { LinkNode } from '@lexical/link';
5
5
  import { ListItemNode, ListNode } from '@lexical/list';
@@ -31,7 +31,7 @@ import { MarkdownSyncPlugin } from './plugins/MarkdownSyncPlugin';
31
31
  import { ToolbarPlugin } from './plugins/ToolbarPlugin';
32
32
  import { ImageRendererProvider } from './context';
33
33
  import { editorTheme } from './theme';
34
- import { CUSTOM_TRANSFORMERS, preprocessMarkdown } from './transformers';
34
+ import { CUSTOM_TRANSFORMERS, preprocessMarkdown, SINGLE_LINE_TRANSFORMERS } from './transformers';
35
35
 
36
36
  export type MarkdownEditorProps = {
37
37
  /** Markdown string (controlled) */
@@ -51,7 +51,11 @@ export type MarkdownEditorProps = {
51
51
  autoFocus?: boolean;
52
52
  /** Invoked on Cmd/Ctrl+Enter — typically used to commit a form. */
53
53
  onSave?: () => void;
54
- /** Invoked on Escape — typically used to discard or close the editor. */
54
+ /**
55
+ * Invoked on Escape in inline-edit mode (`singleLine || saveOnBlur`). Per the
56
+ * inline-editable-text pattern spec, Esc reverts the in-progress edit — callers
57
+ * reset `value` to the last-committed state inside this handler.
58
+ */
55
59
  onCancel?: () => void;
56
60
  /**
57
61
  * Async image upload. Resolved URL is embedded as the image src
@@ -61,6 +65,30 @@ export type MarkdownEditorProps = {
61
65
  className?: string;
62
66
  imageRenderer?: (props: { src: string; alt: string }) => ReactNode;
63
67
  showToolbar?: boolean;
68
+ /**
69
+ * Strips the prose typography wrapper, removes the min-height, and tightens padding so
70
+ * the editor visually blends with surrounding text. Use for inline / Linear-style edit
71
+ * affordances inside headings, list rows, or table cells. Pair with `className` to set
72
+ * the caller's typography.
73
+ */
74
+ compact?: boolean;
75
+ /**
76
+ * Calls `onSave` when focus leaves the editor (and is not moved into the toolbar).
77
+ * `onChange` already keeps the caller's value in sync — `onSave` is the "commit" signal.
78
+ */
79
+ saveOnBlur?: boolean;
80
+ /**
81
+ * Disables block-level markdown (lists, headings, code blocks, HR) and makes a plain
82
+ * Enter press trigger `onSave` and blur the editor. Shift+Enter still inserts a soft
83
+ * line break. Use for editable titles.
84
+ */
85
+ singleLine?: boolean;
86
+ /**
87
+ * Clamps content to ~2 lines with a bottom fade in the display (not-focused) state;
88
+ * focus expands the editor to full height (no clamp). Per the inline-editable-text
89
+ * pattern spec for description-style slots — fade replaces truncation/ellipsis.
90
+ */
91
+ clampDisplay?: boolean;
64
92
  };
65
93
 
66
94
  const EDITOR_NODES = [
@@ -88,8 +116,13 @@ export const MarkdownEditor = ({
88
116
  className,
89
117
  imageRenderer,
90
118
  showToolbar = false,
119
+ compact = false,
120
+ saveOnBlur = false,
121
+ singleLine = false,
122
+ clampDisplay = false,
91
123
  }: MarkdownEditorProps) => {
92
124
  const editable = !readOnly && !disabled;
125
+ const transformers = singleLine ? SINGLE_LINE_TRANSFORMERS : CUSTOM_TRANSFORMERS;
93
126
 
94
127
  // Lexical reads editorState only on first render; later prop changes are applied by MarkdownSyncPlugin.
95
128
  const [initialConfig] = useState<InitialConfigType>(() => ({
@@ -101,18 +134,57 @@ export const MarkdownEditor = ({
101
134
  },
102
135
  editable,
103
136
  editorState: () => {
104
- $convertFromMarkdownString(preprocessMarkdown(value), CUSTOM_TRANSFORMERS);
137
+ $convertFromMarkdownString(preprocessMarkdown(value), transformers);
105
138
  },
106
139
  }));
107
140
 
141
+ // Set to `true` by KeyboardPlugin before it programmatically blurs on Escape so the
142
+ // blur handler can skip its `onSave` — otherwise blur-after-cancel would commit the
143
+ // value we just reverted. Reset on read inside handleBlur.
144
+ const skipBlurSaveRef = useRef(false);
145
+
146
+ const handleBlur =
147
+ saveOnBlur && onSave
148
+ ? (event: FocusEvent<HTMLDivElement>) => {
149
+ if (skipBlurSaveRef.current) {
150
+ skipBlurSaveRef.current = false;
151
+ return;
152
+ }
153
+ // Only fire when focus leaves the editor wrapper entirely (not when moving into the toolbar).
154
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
155
+ onSave();
156
+ }
157
+ }
158
+ : undefined;
159
+
108
160
  return (
109
161
  <LexicalComposer initialConfig={initialConfig}>
110
162
  <ImageRendererProvider value={imageRenderer ?? null}>
111
- <div className={cn('tw-prose', className)}>
163
+ <div
164
+ className={cn(
165
+ !compact && 'tw-prose',
166
+ // Compact mode drops `tw-prose`, so Lexical's <p> wrappers fall back to the browser
167
+ // default `margin: 1em 0` — visually huge at heading sizes. Zero them, and use the
168
+ // adjacent-sibling selector for paragraph spacing so single-paragraph blocks (titles,
169
+ // short descriptions) stay tight while multi-paragraph blocks still breathe.
170
+ compact && '[&_p]:tw-m-0 [&_p+p]:tw-mt-2',
171
+ // Display-state clamp: 2 lines + bottom fade. Drops in focus-within so editing
172
+ // expands to full height. Fade replaces ellipsis per the pattern spec — the
173
+ // mask gradient overlays line-clamp's default ellipsis so it's invisible.
174
+ clampDisplay &&
175
+ '[&:not(:focus-within)]:tw-line-clamp-2 [&:not(:focus-within)]:[mask-image:linear-gradient(to_bottom,black_50%,transparent)]',
176
+ className,
177
+ )}
178
+ onBlur={handleBlur}
179
+ >
112
180
  {showToolbar && editable && <ToolbarPlugin />}
113
181
  <div
114
182
  className={cn(
115
- 'tw-relative tw-rounded-md tw-p-2 tw-transition-colors',
183
+ 'tw-relative tw-rounded-md tw-transition-colors',
184
+ // In compact mode the padding lets the hover bg / focus ring extend past the text,
185
+ // but a matching negative margin shifts the wrapper outward so the text itself stays
186
+ // aligned with the parent's content edge (i.e. with where static text would render).
187
+ compact ? 'tw-px-1.5 tw-py-0.5 -tw-mx-1.5 -tw-my-0.5' : 'tw-p-2',
116
188
  editable &&
117
189
  'tw-cursor-text hover:tw-bg-neutral-50 focus-within:tw-ring-2 focus-within:tw-ring-secondary-500/40',
118
190
  )}
@@ -120,11 +192,18 @@ export const MarkdownEditor = ({
120
192
  <RichTextPlugin
121
193
  contentEditable={
122
194
  <ContentEditable
123
- className="tw-min-h-[100px] tw-outline-none"
195
+ className={cn('tw-outline-none', !compact && 'tw-min-h-[100px]')}
124
196
  aria-placeholder={placeholder ?? ''}
125
197
  placeholder={
126
198
  placeholder ? (
127
- <div className="tw-pointer-events-none tw-absolute tw-left-2 tw-top-[calc(0.5rem+0.35em)] tw-select-none tw-text-neutral-400">
199
+ <div
200
+ className={cn(
201
+ 'tw-pointer-events-none tw-absolute tw-select-none tw-text-neutral-400',
202
+ compact
203
+ ? 'tw-left-1.5 tw-top-0.5'
204
+ : 'tw-left-2 tw-top-[calc(0.5rem+0.35em)]',
205
+ )}
206
+ >
128
207
  {placeholder}
129
208
  </div>
130
209
  ) : (
@@ -139,14 +218,25 @@ export const MarkdownEditor = ({
139
218
  <HistoryPlugin />
140
219
  <LinkPlugin />
141
220
  <ClickableLinkPlugin newTab />
142
- <ListPlugin />
143
- <CheckListPlugin />
144
- <HorizontalRulePlugin />
221
+ {!singleLine && <ListPlugin />}
222
+ {!singleLine && <CheckListPlugin />}
223
+ {!singleLine && <HorizontalRulePlugin />}
145
224
  <TabIndentationPlugin />
146
- <MarkdownShortcutPlugin transformers={CUSTOM_TRANSFORMERS} />
147
- <MarkdownSyncPlugin value={value} editable={editable} onChange={onChange} />
225
+ <MarkdownShortcutPlugin transformers={transformers} />
226
+ <MarkdownSyncPlugin
227
+ value={value}
228
+ editable={editable}
229
+ onChange={onChange}
230
+ transformers={transformers}
231
+ />
148
232
  <ImagePastePlugin onImageUpload={onImageUpload} />
149
- <KeyboardPlugin onSave={onSave} onCancel={onCancel} />
233
+ <KeyboardPlugin
234
+ onSave={onSave}
235
+ onCancel={onCancel}
236
+ singleLine={singleLine}
237
+ blurOnEscape={singleLine || saveOnBlur}
238
+ skipBlurSaveRef={skipBlurSaveRef}
239
+ />
150
240
  {autoFocus && <AutoFocusPlugin />}
151
241
  </div>
152
242
  </ImageRendererProvider>
@@ -19,11 +19,11 @@ export class ImageNode extends DecoratorNode<ReactNode> {
19
19
  __src: string;
20
20
  __altText: string;
21
21
 
22
- static getType(): string {
22
+ static override getType(): string {
23
23
  return 'image';
24
24
  }
25
25
 
26
- static clone(node: ImageNode): ImageNode {
26
+ static override clone(node: ImageNode): ImageNode {
27
27
  return new ImageNode(node.__src, node.__altText, node.__key);
28
28
  }
29
29
 
@@ -51,7 +51,7 @@ export class ImageNode extends DecoratorNode<ReactNode> {
51
51
  writable.__altText = altText;
52
52
  }
53
53
 
54
- createDOM(config: EditorConfig): HTMLElement {
54
+ override createDOM(config: EditorConfig): HTMLElement {
55
55
  const span = document.createElement('span');
56
56
  const className = config.theme.image;
57
57
  if (typeof className === 'string') {
@@ -60,23 +60,23 @@ export class ImageNode extends DecoratorNode<ReactNode> {
60
60
  return span;
61
61
  }
62
62
 
63
- updateDOM(): boolean {
63
+ override updateDOM(): boolean {
64
64
  return false;
65
65
  }
66
66
 
67
- decorate(): ReactNode {
67
+ override decorate(): ReactNode {
68
68
  return <ImageNodeComponent src={this.__src} alt={this.__altText} />;
69
69
  }
70
70
 
71
- isInline(): boolean {
71
+ override isInline(): boolean {
72
72
  return true;
73
73
  }
74
74
 
75
- static importJSON(serializedNode: SerializedImageNode): ImageNode {
75
+ static override importJSON(serializedNode: SerializedImageNode): ImageNode {
76
76
  return $createImageNode(serializedNode.src, serializedNode.altText);
77
77
  }
78
78
 
79
- exportJSON(): SerializedImageNode {
79
+ override exportJSON(): SerializedImageNode {
80
80
  return {
81
81
  type: 'image',
82
82
  version: 1,
@@ -85,14 +85,14 @@ export class ImageNode extends DecoratorNode<ReactNode> {
85
85
  };
86
86
  }
87
87
 
88
- exportDOM(): DOMExportOutput {
88
+ override exportDOM(): DOMExportOutput {
89
89
  const element = document.createElement('img');
90
90
  element.setAttribute('src', this.__src);
91
91
  element.setAttribute('alt', this.__altText);
92
92
  return { element };
93
93
  }
94
94
 
95
- static importDOM(): DOMConversionMap | null {
95
+ static override importDOM(): DOMConversionMap | null {
96
96
  return {
97
97
  img: () => ({
98
98
  conversion: (domNode: HTMLElement) => {
@@ -10,11 +10,11 @@ type SerializedMermaidNode = Spread<{ code: string }, SerializedLexicalNode>;
10
10
  export class MermaidNode extends DecoratorNode<ReactNode> {
11
11
  __code: string;
12
12
 
13
- static getType(): string {
13
+ static override getType(): string {
14
14
  return 'mermaid';
15
15
  }
16
16
 
17
- static clone(node: MermaidNode): MermaidNode {
17
+ static override clone(node: MermaidNode): MermaidNode {
18
18
  return new MermaidNode(node.__code, node.__key);
19
19
  }
20
20
 
@@ -32,29 +32,29 @@ export class MermaidNode extends DecoratorNode<ReactNode> {
32
32
  writable.__code = code;
33
33
  }
34
34
 
35
- createDOM(): HTMLElement {
35
+ override createDOM(): HTMLElement {
36
36
  const div = document.createElement('div');
37
37
  div.setAttribute('data-lexical-mermaid', 'true');
38
38
  return div;
39
39
  }
40
40
 
41
- updateDOM(): boolean {
41
+ override updateDOM(): boolean {
42
42
  return false;
43
43
  }
44
44
 
45
- decorate(): ReactNode {
45
+ override decorate(): ReactNode {
46
46
  return <MermaidNodeComponent code={this.__code} />;
47
47
  }
48
48
 
49
- isInline(): boolean {
49
+ override isInline(): boolean {
50
50
  return false;
51
51
  }
52
52
 
53
- static importJSON(serializedNode: SerializedMermaidNode): MermaidNode {
53
+ static override importJSON(serializedNode: SerializedMermaidNode): MermaidNode {
54
54
  return $createMermaidNode(serializedNode.code);
55
55
  }
56
56
 
57
- exportJSON(): SerializedMermaidNode {
57
+ override exportJSON(): SerializedMermaidNode {
58
58
  return {
59
59
  type: 'mermaid',
60
60
  version: 1,
@@ -1,18 +1,54 @@
1
1
  import { useEffect, useRef } from 'react';
2
+ import type { RefObject } from 'react';
2
3
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
4
  import { COMMAND_PRIORITY_HIGH, KEY_DOWN_COMMAND } from 'lexical';
4
5
 
5
6
  type KeyboardPluginProps = {
7
+ /** Invoked directly on Cmd/Ctrl+Enter (explicit commit shortcut, no blur). */
6
8
  onSave?: () => void;
9
+ /**
10
+ * Invoked on Escape in inline-edit mode. Per the inline-editable-text pattern spec
11
+ * (evviku/design-system-docs), Escape reverts the in-progress edit — callers reset
12
+ * `value` to the last-committed state inside this handler.
13
+ */
7
14
  onCancel?: () => void;
15
+ /**
16
+ * When true, plain Enter blurs the editor instead of inserting a paragraph break.
17
+ * For `saveOnBlur` callers this commits via blur; for non-saveOnBlur callers Enter
18
+ * just exits edit mode. Shift+Enter still inserts a soft line break.
19
+ */
20
+ singleLine?: boolean;
21
+ /**
22
+ * When true, Escape blurs the editor (exits edit mode). Enable only for inline-edit
23
+ * patterns (`singleLine` or `saveOnBlur`); in full-toolbar mode Esc stays a no-op so
24
+ * existing call sites don't lose focus on a stray keypress.
25
+ */
26
+ blurOnEscape?: boolean;
27
+ /**
28
+ * Set to `true` before the plugin programmatically blurs on Escape so the wrapper's
29
+ * blur handler can skip its `onSave` — otherwise blur-after-cancel would commit the
30
+ * value we just reverted. Reset by the consumer on read.
31
+ */
32
+ skipBlurSaveRef?: RefObject<boolean>;
8
33
  };
9
34
 
10
- export const KeyboardPlugin = ({ onSave, onCancel }: KeyboardPluginProps) => {
35
+ export const KeyboardPlugin = ({
36
+ onSave,
37
+ onCancel,
38
+ singleLine,
39
+ blurOnEscape,
40
+ skipBlurSaveRef,
41
+ }: KeyboardPluginProps) => {
11
42
  const [editor] = useLexicalComposerContext();
12
43
  const onSaveRef = useRef(onSave);
13
44
  onSaveRef.current = onSave;
14
45
  const onCancelRef = useRef(onCancel);
15
46
  onCancelRef.current = onCancel;
47
+ const singleLineRef = useRef(singleLine);
48
+ singleLineRef.current = singleLine;
49
+ const blurOnEscapeRef = useRef(blurOnEscape);
50
+ blurOnEscapeRef.current = blurOnEscape;
51
+ const skipBlurSaveExternalRef = skipBlurSaveRef;
16
52
 
17
53
  useEffect(() => {
18
54
  return editor.registerCommand(
@@ -23,15 +59,23 @@ export const KeyboardPlugin = ({ onSave, onCancel }: KeyboardPluginProps) => {
23
59
  onSaveRef.current?.();
24
60
  return true;
25
61
  }
26
- if (event.key === 'Escape') {
62
+ if (event.key === 'Enter' && !event.shiftKey && !event.altKey && singleLineRef.current) {
63
+ event.preventDefault();
64
+ editor.getRootElement()?.blur();
65
+ return true;
66
+ }
67
+ if (event.key === 'Escape' && blurOnEscapeRef.current) {
68
+ // Suppress blur-driven onSave so the post-blur save doesn't commit the value we just reverted.
69
+ if (skipBlurSaveExternalRef) skipBlurSaveExternalRef.current = true;
27
70
  onCancelRef.current?.();
71
+ editor.getRootElement()?.blur();
28
72
  return true;
29
73
  }
30
74
  return false;
31
75
  },
32
76
  COMMAND_PRIORITY_HIGH,
33
77
  );
34
- }, [editor]);
78
+ }, [editor, skipBlurSaveExternalRef]);
35
79
 
36
80
  return null;
37
81
  };
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useRef } from 'react';
2
+ import type { Transformer } from '@lexical/markdown';
2
3
  import { $convertFromMarkdownString, $convertToMarkdownString } from '@lexical/markdown';
3
4
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
5
 
@@ -10,13 +11,21 @@ type MarkdownSyncPluginProps = {
10
11
  value: string;
11
12
  editable: boolean;
12
13
  onChange?: (markdown: string) => void;
14
+ transformers?: Transformer[];
13
15
  };
14
16
 
15
- export const MarkdownSyncPlugin = ({ value, editable, onChange }: MarkdownSyncPluginProps) => {
17
+ export const MarkdownSyncPlugin = ({
18
+ value,
19
+ editable,
20
+ onChange,
21
+ transformers = CUSTOM_TRANSFORMERS,
22
+ }: MarkdownSyncPluginProps) => {
16
23
  const [editor] = useLexicalComposerContext();
17
24
  const lastExportedRef = useRef(value);
18
25
  const onChangeRef = useRef(onChange);
19
26
  onChangeRef.current = onChange;
27
+ const transformersRef = useRef(transformers);
28
+ transformersRef.current = transformers;
20
29
 
21
30
  // Sync editable state — only when it actually changes
22
31
  useEffect(() => {
@@ -30,7 +39,7 @@ export const MarkdownSyncPlugin = ({ value, editable, onChange }: MarkdownSyncPl
30
39
 
31
40
  editor.update(
32
41
  () => {
33
- $convertFromMarkdownString(preprocessMarkdown(value), CUSTOM_TRANSFORMERS);
42
+ $convertFromMarkdownString(preprocessMarkdown(value), transformersRef.current);
34
43
  },
35
44
  { tag: IMPORT_TAG },
36
45
  );
@@ -42,7 +51,7 @@ export const MarkdownSyncPlugin = ({ value, editable, onChange }: MarkdownSyncPl
42
51
  if (tags.has(IMPORT_TAG)) return;
43
52
 
44
53
  editor.getEditorState().read(() => {
45
- const raw = $convertToMarkdownString(CUSTOM_TRANSFORMERS);
54
+ const raw = $convertToMarkdownString(transformersRef.current);
46
55
  const markdown = postprocessMarkdown(raw);
47
56
  if (markdown !== lastExportedRef.current) {
48
57
  lastExportedRef.current = markdown;
@@ -19,7 +19,17 @@ import {
19
19
  FORMAT_TEXT_COMMAND,
20
20
  } from 'lexical';
21
21
 
22
- import { Icon } from '../../Icon';
22
+ import {
23
+ Bold,
24
+ Code,
25
+ Italic,
26
+ List as ListIcon,
27
+ ListOrdered,
28
+ Quote,
29
+ SquareCode,
30
+ Strikethrough,
31
+ Underline,
32
+ } from '../../../icons';
23
33
 
24
34
  import { AlignmentDropdown } from './toolbar/AlignmentDropdown';
25
35
  import { BlockTypeDropdown } from './toolbar/BlockTypeDropdown';
@@ -164,35 +174,35 @@ export const ToolbarPlugin = () => {
164
174
  onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}
165
175
  tooltip="Bold (Ctrl+B)"
166
176
  >
167
- <Icon icon={['far', 'bold']} />
177
+ <Bold className="tw-size-4" />
168
178
  </ToolbarButton>
169
179
  <ToolbarButton
170
180
  active={isItalic}
171
181
  onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')}
172
182
  tooltip="Italic (Ctrl+I)"
173
183
  >
174
- <Icon icon={['far', 'italic']} />
184
+ <Italic className="tw-size-4" />
175
185
  </ToolbarButton>
176
186
  <ToolbarButton
177
187
  active={isUnderline}
178
188
  onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')}
179
189
  tooltip="Underline (Ctrl+U)"
180
190
  >
181
- <Icon icon={['far', 'underline']} />
191
+ <Underline className="tw-size-4" />
182
192
  </ToolbarButton>
183
193
  <ToolbarButton
184
194
  active={isStrikethrough}
185
195
  onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')}
186
196
  tooltip="Strikethrough"
187
197
  >
188
- <Icon icon={['far', 'strikethrough']} />
198
+ <Strikethrough className="tw-size-4" />
189
199
  </ToolbarButton>
190
200
  <ToolbarButton
191
201
  active={isCode}
192
202
  onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code')}
193
203
  tooltip="Inline Code"
194
204
  >
195
- <Icon icon={['far', 'code']} />
205
+ <Code className="tw-size-4" />
196
206
  </ToolbarButton>
197
207
 
198
208
  <ToolbarDivider />
@@ -212,7 +222,7 @@ export const ToolbarPlugin = () => {
212
222
  }}
213
223
  tooltip="Bullet List"
214
224
  >
215
- <Icon icon={['far', 'list-ul']} />
225
+ <ListIcon className="tw-size-4" />
216
226
  </ToolbarButton>
217
227
  <ToolbarButton
218
228
  active={blockType === 'number'}
@@ -225,7 +235,7 @@ export const ToolbarPlugin = () => {
225
235
  }}
226
236
  tooltip="Numbered List"
227
237
  >
228
- <Icon icon={['far', 'list-ol']} />
238
+ <ListOrdered className="tw-size-4" />
229
239
  </ToolbarButton>
230
240
 
231
241
  <ToolbarDivider />
@@ -249,7 +259,7 @@ export const ToolbarPlugin = () => {
249
259
  }}
250
260
  tooltip="Quote"
251
261
  >
252
- <Icon icon={['far', 'block-quote']} />
262
+ <Quote className="tw-size-4" />
253
263
  </ToolbarButton>
254
264
  <ToolbarButton
255
265
  active={blockType === 'code'}
@@ -266,7 +276,7 @@ export const ToolbarPlugin = () => {
266
276
  }}
267
277
  tooltip="Code Block"
268
278
  >
269
- <Icon icon="square-code" />
279
+ <SquareCode className="tw-size-4" />
270
280
  </ToolbarButton>
271
281
  </div>,
272
282
  document.body,
@@ -1,5 +1,4 @@
1
1
  import { useRef } from 'react';
2
- import type { ComponentProps } from 'react';
3
2
  import {
4
3
  $getSelection,
5
4
  $setSelection,
@@ -9,21 +8,26 @@ import {
9
8
  type LexicalEditor,
10
9
  } from 'lexical';
11
10
 
11
+ import {
12
+ AlignCenter,
13
+ AlignJustify,
14
+ AlignLeft,
15
+ AlignRight,
16
+ ChevronDown,
17
+ type LucideIcon,
18
+ } from '../../../../icons';
12
19
  import { cn } from '../../../../utils';
13
20
  import { DropdownMenu } from '../../../DropdownMenu';
14
- import { Icon } from '../../../Icon';
15
21
  import { Tooltip } from '../../../Tooltip';
16
22
 
17
- type IconProp = ComponentProps<typeof Icon>['icon'];
18
-
19
- const ALIGNMENT_OPTIONS: { value: ElementFormatType; label: string; icon: IconProp }[] = [
20
- { value: 'left', label: 'Left', icon: ['far', 'align-left'] },
21
- { value: 'center', label: 'Center', icon: ['far', 'align-center'] },
22
- { value: 'right', label: 'Right', icon: ['far', 'align-right'] },
23
- { value: 'justify', label: 'Justify', icon: ['far', 'align-justify'] },
23
+ const ALIGNMENT_OPTIONS: { value: ElementFormatType; label: string; icon: LucideIcon }[] = [
24
+ { value: 'left', label: 'Left', icon: AlignLeft },
25
+ { value: 'center', label: 'Center', icon: AlignCenter },
26
+ { value: 'right', label: 'Right', icon: AlignRight },
27
+ { value: 'justify', label: 'Justify', icon: AlignJustify },
24
28
  ];
25
29
 
26
- const ALIGNMENT_ICONS: Record<string, IconProp> = Object.fromEntries(
30
+ const ALIGNMENT_ICONS: Record<string, LucideIcon> = Object.fromEntries(
27
31
  ALIGNMENT_OPTIONS.map(({ value, icon }) => [value, icon]),
28
32
  );
29
33
 
@@ -57,6 +61,8 @@ export const AlignmentDropdown = ({
57
61
  });
58
62
  };
59
63
 
64
+ const CurrentAlignmentIcon = ALIGNMENT_ICONS[alignment] ?? AlignLeft;
65
+
60
66
  return (
61
67
  <DropdownMenu onOpenChange={handleOpenChange}>
62
68
  <Tooltip tooltip="Text alignment" placement="top" triggerAsChild>
@@ -68,13 +74,13 @@ export const AlignmentDropdown = ({
68
74
  'hover:tw-bg-neutral-100',
69
75
  )}
70
76
  >
71
- <Icon icon={ALIGNMENT_ICONS[alignment] ?? ['far', 'align-left']} fixedWidth />
72
- <Icon icon={['fas', 'caret-down']} className="tw-text-[8px]" />
77
+ <CurrentAlignmentIcon className="tw-size-4" />
78
+ <ChevronDown className="tw-size-3" />
73
79
  </button>
74
80
  </DropdownMenu.Trigger>
75
81
  </Tooltip>
76
82
  <DropdownMenu.Content align="start" className="tw-min-w-32">
77
- {ALIGNMENT_OPTIONS.map(({ value, label, icon }) => (
83
+ {ALIGNMENT_OPTIONS.map(({ value, label, icon: IconComp }) => (
78
84
  <DropdownMenu.Item
79
85
  key={value}
80
86
  onSelect={() => handleSelect(value)}
@@ -83,7 +89,7 @@ export const AlignmentDropdown = ({
83
89
  alignment === value && 'tw-bg-secondary-50 tw-text-secondary-500',
84
90
  )}
85
91
  >
86
- <Icon icon={icon} fixedWidth />
92
+ <IconComp className="tw-size-4" />
87
93
  {label}
88
94
  </DropdownMenu.Item>
89
95
  ))}