@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.
- package/README.md +25 -0
- package/dist/icon-names.cjs +18 -0
- package/dist/icon-names.cjs.map +1 -0
- package/dist/icon-names.js +16 -0
- package/dist/icon-names.js.map +1 -0
- package/dist/icons.cjs +731 -0
- package/dist/icons.cjs.map +1 -0
- package/dist/icons.js +29 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.cjs +14728 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +14553 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown-editor.cjs +4440 -0
- package/dist/markdown-editor.cjs.map +1 -0
- package/dist/markdown-editor.js +4413 -0
- package/dist/markdown-editor.js.map +1 -0
- package/dist/mermaid.cjs +1011 -0
- package/dist/mermaid.cjs.map +1 -0
- package/dist/mermaid.js +1005 -0
- package/dist/mermaid.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +10 -3
- package/src/blocks/AIKit/CollapsibleStepHeader.tsx +7 -8
- package/src/blocks/AIKit/InlineStepLabel.tsx +7 -7
- package/src/blocks/AIKit/MessageBubble.stories.tsx +11 -3
- package/src/blocks/AIKit/MessageBubble.tsx +1 -1
- package/src/blocks/AIKit/PlanApproval.tsx +9 -8
- package/src/blocks/AIKit/PlanRewritingIndicator.tsx +2 -2
- package/src/blocks/AIKit/PromptInput.stories.tsx +10 -2
- package/src/blocks/AIKit/PromptInputSubmit.tsx +12 -8
- package/src/blocks/AIKit/Question.tsx +8 -9
- package/src/blocks/AIKit/SuggestedPrompt.tsx +12 -5
- package/src/blocks/AIKit/ThinkingIndicator.tsx +2 -2
- package/src/blocks/AutoSaveTextarea/AutoSaveTextarea.tsx +4 -4
- package/src/blocks/ClipboardButton/ClipboardButton.tsx +7 -5
- package/src/blocks/ClipboardButton/ClipboardInline.tsx +2 -3
- package/src/blocks/ColorInput/ColorInput.tsx +4 -2
- package/src/blocks/ColorInput/ColorPicker.tsx +5 -2
- package/src/blocks/EmptyState/EmptyState.tsx +16 -0
- package/src/blocks/ErrorState/ErrorState.tsx +15 -0
- package/src/blocks/Form/formHook.ts +15 -0
- package/src/blocks/MultiActionButtonGroup/MultiActionButton.stories.tsx +12 -11
- package/src/blocks/MultiActionButtonGroup/MultiActionButton.tsx +4 -4
- package/src/blocks/MultiCombobox/MultiCombobox.tsx +28 -12
- package/src/blocks/PageHeader/AGENTS.md +63 -3
- package/src/blocks/PageHeader/PageHeader.stories.tsx +124 -28
- package/src/blocks/PageHeader/PageHeader.tsx +15 -0
- package/src/blocks/PageHeader/PageHeaderDetail.tsx +23 -1
- package/src/blocks/PageHeader/PageHeaderIndex.tsx +20 -1
- package/src/blocks/PageLayout/PageLayout.tsx +26 -0
- package/src/blocks/PageToolbar/PageToolbar.stories.tsx +348 -0
- package/src/blocks/PageToolbar/PageToolbar.tsx +119 -0
- package/src/blocks/PageToolbar/index.ts +1 -0
- package/src/blocks/PasswordInput/PasswordInput.tsx +10 -10
- package/src/blocks/SearchInput/SearchInput.tsx +30 -4
- package/src/blocks/SearchInputWithNavigation/SearchInputWithNavigation.tsx +9 -6
- package/src/blocks/SplitButton/SplitButton.stories.tsx +7 -4
- package/src/blocks/SplitButton/SplitButton.tsx +17 -4
- package/src/blocks/Tagger/TagItem.tsx +4 -1
- package/src/blocks/Tagger/TagSelectPopover/TagSelectCreateItem.tsx +3 -1
- package/src/blocks/Tagger/TagSelectPopover/TagSelectList.tsx +2 -2
- package/src/blocks/Tagger/Tagger.tsx +2 -2
- package/src/components/Alert/Alert.stories.tsx +11 -3
- package/src/components/Alert/Alert.tsx +19 -1
- package/src/components/Alert/AlertCollapsibleIcon.tsx +3 -4
- package/src/components/Alert/AlertIcon.tsx +18 -21
- package/src/components/Badge/Badge.stories.tsx +22 -4
- package/src/components/Badge/Badge.tsx +16 -0
- package/src/components/Badge/BadgeButton.tsx +7 -6
- package/src/components/Breadcrumb/BreadcrumbEllipsis.tsx +2 -2
- package/src/components/Breadcrumb/BreadcrumbSeparator.tsx +2 -2
- package/src/components/Button/Button.stories.tsx +14 -2
- package/src/components/Button/Button.tsx +42 -5
- package/src/components/Button/styles.ts +4 -6
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +32 -10
- package/src/components/Card/Card.tsx +17 -0
- package/src/components/Checkbox/Checkbox.tsx +3 -6
- package/src/components/CodeEditor/highlighting.ts +1 -1
- package/src/components/CollapsiblePanel.tsx +2 -5
- package/src/components/Combobox/Combobox.tsx +12 -14
- package/src/components/Combobox/ComboboxChips.tsx +6 -1
- package/src/components/Combobox/ComboboxItem.tsx +3 -4
- package/src/components/Combobox/ComboboxItemIndicator.tsx +6 -4
- package/src/components/Combobox/types.ts +2 -6
- package/src/components/Command/Command.stories.tsx +47 -56
- package/src/components/Command/CommandItem/CheckIndicator.tsx +26 -14
- package/src/components/Command/CommandItem/CommandItem.tsx +6 -3
- package/src/components/DropdownMenu/DropdownMenu.stories.tsx +8 -4
- package/src/components/DropdownMenu/DropdownMenuCheckboxItem.tsx +4 -1
- package/src/components/DropdownMenu/DropdownMenuItem.tsx +12 -6
- package/src/components/DropdownMenu/DropdownMenuItemIndicator.tsx +12 -6
- package/src/components/DropdownMenu/DropdownMenuRadioGroup/DropdownMenuRadioItem.tsx +8 -5
- package/src/components/DropdownMenu/DropdownMenuSub/DropdownMenuSubTrigger.tsx +2 -2
- package/src/components/DropdownMenu/ItemIcon.tsx +16 -8
- package/src/components/FormGroup/FormGroup.tsx +15 -0
- package/src/components/FullScreenModal/FullScreenModalHeader.tsx +4 -1
- package/src/components/Input/DateInput/DateInput.tsx +4 -1
- package/src/components/Input/DateRangeInput/DateRangeInput.tsx +4 -6
- package/src/components/Input/Input.tsx +18 -2
- package/src/components/Input/TextInput/TextInput.tsx +16 -0
- package/src/components/Input/TimeInput/TimeInput.tsx +4 -1
- package/src/components/InputGroup/InputGroup.stories.tsx +11 -3
- package/src/components/MarkdownEditor/MarkdownEditor.stories.tsx +63 -1
- package/src/components/MarkdownEditor/MarkdownEditor.tsx +105 -15
- package/src/components/MarkdownEditor/nodes/ImageNode.tsx +10 -10
- package/src/components/MarkdownEditor/nodes/MermaidNode.tsx +8 -8
- package/src/components/MarkdownEditor/plugins/KeyboardPlugin.tsx +47 -3
- package/src/components/MarkdownEditor/plugins/MarkdownSyncPlugin.tsx +12 -3
- package/src/components/MarkdownEditor/plugins/ToolbarPlugin.tsx +20 -10
- package/src/components/MarkdownEditor/plugins/toolbar/AlignmentDropdown.tsx +20 -14
- package/src/components/MarkdownEditor/plugins/toolbar/BlockTypeDropdown.tsx +25 -16
- package/src/components/MarkdownEditor/plugins/toolbar/LinkPanel.tsx +3 -3
- package/src/components/MarkdownEditor/transformers.ts +10 -0
- package/src/components/MergeEditor/AcceptControls/AcceptButton.tsx +9 -4
- package/src/components/MergeEditor/AcceptControls/AcceptControls.tsx +25 -14
- package/src/components/MergeEditor/ConflictStatus/ConflictStatusIndicator.tsx +2 -2
- package/src/components/MergeEditor/MergeEditor.stories.tsx +15 -0
- package/src/components/MergeEditor/MergeEditor.tsx +9 -0
- package/src/components/MergeEditor/MergePane.tsx +22 -4
- package/src/components/MergeEditor/decorations.ts +2 -2
- package/src/components/Modal/Modal.stories.tsx +6 -4
- package/src/components/Modal/Modal.tsx +26 -0
- package/src/components/Modal/ModalBody.tsx +5 -0
- package/src/components/Modal/ModalCloseIcon.tsx +2 -3
- package/src/components/Modal/ModalContent.tsx +8 -0
- package/src/components/Modal/ModalFooter.tsx +5 -0
- package/src/components/Modal/ModalHeader.tsx +11 -0
- package/src/components/Modal/ModalIcon.tsx +45 -19
- package/src/components/Navbar/Navbar.stories.tsx +26 -13
- package/src/components/Navbar/NavbarMenuButton.tsx +11 -10
- package/src/components/Pagination/PaginationEllipsis.tsx +2 -2
- package/src/components/Pagination/PaginationNext.tsx +2 -1
- package/src/components/Pagination/PaginationPrevious.tsx +4 -1
- package/src/components/PanelWithDetails.tsx +3 -4
- package/src/components/Popconfirm/Popconfirm.stories.tsx +13 -5
- package/src/components/RadioGroup/Radio.tsx +6 -12
- package/src/components/Search/ClearButton.tsx +2 -2
- package/src/components/Search/Search.tsx +3 -3
- package/src/components/Select/Select.tsx +16 -5
- package/src/components/Select/SelectContent.tsx +13 -4
- package/src/components/Select/SelectItem.tsx +6 -5
- package/src/components/Select/SelectTrigger.tsx +2 -2
- package/src/components/Sidebar/Sidebar.stories.tsx +55 -19
- package/src/components/Sidebar/SidebarGroup/SidebarGroupAction.tsx +8 -4
- package/src/components/Sidebar/SidebarMenu/SidebarMenuButton.tsx +4 -2
- package/src/components/Sidebar/SidebarMenu/SidebarMenuCard.tsx +3 -2
- package/src/components/Sidebar/SidebarMenu/SidebarMenuIcon.tsx +12 -12
- package/src/components/Sidebar/SidebarMenu/SidebarMenuSub.tsx +3 -7
- package/src/components/Sidebar/SidebarMenu/SidebarSearchInput.tsx +3 -5
- package/src/components/Skeleton/Skeleton.stories.tsx +52 -0
- package/src/components/Skeleton/Skeleton.tsx +24 -1
- package/src/components/Table/Table.stories.tsx +223 -1
- package/src/components/Table/Table.tsx +17 -0
- package/src/components/Tabs.tsx +25 -2
- package/src/components/Toast/ToastDismiss.tsx +3 -3
- package/src/components/Toast/ToastRoot.tsx +24 -19
- package/src/components/Toggle/Toggle.tsx +11 -6
- package/src/components/Tooltip/Tooltip.tsx +23 -0
- package/src/components/ZoomableSvg/ZoomableSvg.tsx +8 -6
- package/src/icons/Keboola.tsx +28 -0
- package/src/icons/icons.stories.tsx +174 -0
- package/src/icons/index.ts +180 -0
- package/src/index.ts +1 -0
- package/src/patterns/AlertWithAction.stories.tsx +30 -0
- package/src/patterns/CardWithEmptyState.stories.tsx +66 -0
- package/src/patterns/ConfirmDialog.stories.tsx +72 -0
- package/src/patterns/CreatableMultiCombobox.stories.tsx +33 -0
- package/src/patterns/DataCardWithSkeleton.stories.tsx +48 -0
- package/src/patterns/DisabledButtonTooltip.stories.tsx +30 -0
- package/src/patterns/FilterToolbar.stories.tsx +71 -0
- package/src/patterns/InfoTooltip.stories.tsx +56 -0
- package/src/patterns/ListDetail.stories.tsx +65 -0
- package/src/patterns/ModalForm.stories.tsx +59 -0
- package/src/patterns/PageHeaderTabs.stories.tsx +48 -0
- package/src/styles.css +16 -8
- package/src/utils/legacyIcon.tsx +28 -0
|
@@ -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
|
-
/**
|
|
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),
|
|
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
|
|
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-
|
|
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=
|
|
195
|
+
className={cn('tw-outline-none', !compact && 'tw-min-h-[100px]')}
|
|
124
196
|
aria-placeholder={placeholder ?? ''}
|
|
125
197
|
placeholder={
|
|
126
198
|
placeholder ? (
|
|
127
|
-
<div
|
|
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={
|
|
147
|
-
<MarkdownSyncPlugin
|
|
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
|
|
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 = ({
|
|
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 === '
|
|
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 = ({
|
|
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),
|
|
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(
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{ value: '
|
|
21
|
-
{ value: '
|
|
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,
|
|
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
|
-
<
|
|
72
|
-
<
|
|
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
|
-
<
|
|
92
|
+
<IconComp className="tw-size-4" />
|
|
87
93
|
{label}
|
|
88
94
|
</DropdownMenu.Item>
|
|
89
95
|
))}
|