open-mcp-app-ui 0.0.3 → 0.0.6

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.
@@ -1,22 +1,94 @@
1
+ import react__default from 'react';
2
+
1
3
  /**
2
- * open-mcp-app-ui/editor
4
+ * Editor — Markdown + rich text editor.
5
+ *
6
+ * Built on Milkdown (ProseMirror + Remark) for markdown-first editing
7
+ * with perfect round-trip fidelity. Supports WYSIWYG, raw markdown,
8
+ * and split (side-by-side) editing modes.
3
9
  *
4
- * Markdown + rich text editor built on Milkdown (ProseMirror + Remark).
5
- * This is an optional import apps that don't use it pay zero bundle cost.
10
+ * Styled exclusively with MCP Apps spec CSS variables. All typography,
11
+ * colors, borders, and spacing use the host's theme tokens.
6
12
  *
7
- * Phase 2: Placeholder export. Implementation coming soon.
13
+ * Imported separately from the core library to keep apps that don't
14
+ * need it from paying any bundle cost:
8
15
  *
9
- * Planned usage:
10
16
  * import { Editor } from "open-mcp-app-ui/editor";
17
+ */
18
+
19
+ /** Available toolbar action items. */
20
+ type ToolbarItem = "bold" | "italic" | "strikethrough" | "heading" | "bulletList" | "orderedList" | "taskList" | "code" | "codeBlock" | "blockquote" | "link" | "divider" | "undo" | "redo";
21
+ /** Editing mode. */
22
+ type EditorMode = "wysiwyg" | "markdown" | "split";
23
+ interface EditorProps {
24
+ /** Markdown string (source of truth). */
25
+ value?: string;
26
+ /** Called when content changes. Receives the updated markdown string. */
27
+ onChange?: (markdown: string) => void;
28
+ /**
29
+ * Editing mode:
30
+ * - "wysiwyg" — Rich text rendering with formatting (default)
31
+ * - "markdown" — Raw markdown text editing
32
+ * - "split" — Side-by-side WYSIWYG and markdown
33
+ *
34
+ * Omit to show a mode toggle in the toolbar so users can switch freely.
35
+ */
36
+ mode?: EditorMode;
37
+ /** Placeholder text when the editor is empty. */
38
+ placeholder?: string;
39
+ /**
40
+ * Toolbar buttons to show, or `false` to hide the toolbar entirely.
41
+ * Defaults to a sensible set of common formatting actions.
42
+ */
43
+ toolbar?: ToolbarItem[] | false;
44
+ /** View-only mode. Renders markdown as styled content without editing. */
45
+ readOnly?: boolean;
46
+ /**
47
+ * Show a border and rounded corners around the editor.
48
+ * When false (default), the editor sits flat within its container,
49
+ * making it easy to embed inside cards or other bordered elements.
50
+ */
51
+ bordered?: boolean;
52
+ /** Minimum editor height in pixels. */
53
+ minHeight?: number;
54
+ /** Maximum editor height in pixels before scrolling. */
55
+ maxHeight?: number;
56
+ /** Focus the editor on mount. */
57
+ autoFocus?: boolean;
58
+ /** Additional CSS classes on the outermost wrapper. */
59
+ className?: string;
60
+ }
61
+ interface EditorRef {
62
+ /** Get the current markdown content. */
63
+ getMarkdown: () => string;
64
+ /** Imperatively set editor content without triggering onChange. */
65
+ setMarkdown: (markdown: string) => void;
66
+ /** Focus the editor. */
67
+ focus: () => void;
68
+ }
69
+ /**
70
+ * Markdown + rich text editor with toolbar and mode switching.
71
+ *
72
+ * Built on Milkdown (ProseMirror + Remark) for markdown-first editing.
73
+ * Supports WYSIWYG, raw markdown, and split editing modes. Styled with
74
+ * MCP Apps spec CSS variables for automatic host theming.
75
+ *
76
+ * By default the editor has no border or rounded corners so it sits
77
+ * flat inside a containing element. Set `bordered` to add them.
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * import { Editor } from "open-mcp-app-ui/editor";
82
+ *
83
+ * <Editor
84
+ * value={markdown}
85
+ * onChange={setMarkdown}
86
+ * placeholder="Start writing..."
87
+ * />
11
88
  *
12
- * <Editor
13
- * value={markdownString}
14
- * onChange={setMarkdownString}
15
- * mode="wysiwyg"
16
- * toolbar={["bold", "italic", "heading", "list", "code", "link"]}
17
- * />
89
+ * <Editor value={markdown} onChange={setMarkdown} bordered />
90
+ * ```
18
91
  */
19
- declare const EDITOR_VERSION = "0.0.1";
20
- declare const EDITOR_STATUS: "planned";
92
+ declare const Editor: react__default.ForwardRefExoticComponent<EditorProps & react__default.RefAttributes<EditorRef>>;
21
93
 
22
- export { EDITOR_STATUS, EDITOR_VERSION };
94
+ export { Editor, type EditorMode, type EditorProps, type EditorRef, type ToolbarItem };
@@ -1,8 +1,536 @@
1
- // src/editor/index.ts
2
- var EDITOR_VERSION = "0.0.1";
3
- var EDITOR_STATUS = "planned";
1
+ // src/editor/Editor.tsx
2
+ import {
3
+ useState,
4
+ useRef,
5
+ useCallback,
6
+ useEffect,
7
+ forwardRef,
8
+ useImperativeHandle
9
+ } from "react";
10
+ import {
11
+ Editor as MilkdownEditor,
12
+ rootCtx,
13
+ defaultValueCtx,
14
+ commandsCtx
15
+ } from "@milkdown/kit/core";
16
+ import {
17
+ commonmark,
18
+ toggleStrongCommand,
19
+ toggleEmphasisCommand,
20
+ wrapInHeadingCommand,
21
+ wrapInBulletListCommand,
22
+ wrapInOrderedListCommand,
23
+ toggleInlineCodeCommand,
24
+ createCodeBlockCommand,
25
+ wrapInBlockquoteCommand,
26
+ toggleLinkCommand
27
+ } from "@milkdown/kit/preset/commonmark";
28
+ import {
29
+ gfm,
30
+ toggleStrikethroughCommand
31
+ } from "@milkdown/kit/preset/gfm";
32
+ import {
33
+ history,
34
+ undoCommand,
35
+ redoCommand
36
+ } from "@milkdown/kit/plugin/history";
37
+ import { clipboard } from "@milkdown/kit/plugin/clipboard";
38
+ import { listener, listenerCtx } from "@milkdown/plugin-listener";
39
+ import { replaceAll } from "@milkdown/utils";
40
+ import { Milkdown, MilkdownProvider, useEditor } from "@milkdown/react";
41
+ import { jsx, jsxs } from "react/jsx-runtime";
42
+ var DEFAULT_TOOLBAR = [
43
+ "bold",
44
+ "italic",
45
+ "strikethrough",
46
+ "divider",
47
+ "heading",
48
+ "bulletList",
49
+ "orderedList",
50
+ "taskList",
51
+ "divider",
52
+ "code",
53
+ "codeBlock",
54
+ "blockquote",
55
+ "link",
56
+ "divider",
57
+ "undo",
58
+ "redo"
59
+ ];
60
+ var TOOLBAR_MARKDOWN = {
61
+ bold: "**bold**",
62
+ italic: "*italic*",
63
+ strikethrough: "~~strikethrough~~",
64
+ heading: "## ",
65
+ bulletList: "- ",
66
+ orderedList: "1. ",
67
+ taskList: "- [ ] ",
68
+ code: "`code`",
69
+ codeBlock: "```\n\n```",
70
+ blockquote: "> ",
71
+ link: "[text](url)",
72
+ undo: "",
73
+ redo: ""
74
+ };
75
+ var ToolbarIcons = {
76
+ bold: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
77
+ /* @__PURE__ */ jsx("path", { d: "M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z" }),
78
+ /* @__PURE__ */ jsx("path", { d: "M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z" })
79
+ ] }),
80
+ italic: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
81
+ /* @__PURE__ */ jsx("line", { x1: "19", y1: "4", x2: "10", y2: "4" }),
82
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "20", x2: "5", y2: "20" }),
83
+ /* @__PURE__ */ jsx("line", { x1: "15", y1: "4", x2: "9", y2: "20" })
84
+ ] }),
85
+ strikethrough: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
86
+ /* @__PURE__ */ jsx("path", { d: "M16 4H9a3 3 0 0 0-3 3c0 2 1.5 3 3 3h6" }),
87
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
88
+ /* @__PURE__ */ jsx("path", { d: "M15 12c1.5 0 3 1 3 3a3 3 0 0 1-3 3H8" })
89
+ ] }),
90
+ heading: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
91
+ /* @__PURE__ */ jsx("path", { d: "M6 4v16" }),
92
+ /* @__PURE__ */ jsx("path", { d: "M18 4v16" }),
93
+ /* @__PURE__ */ jsx("path", { d: "M6 12h12" })
94
+ ] }),
95
+ bulletList: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
96
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "6", x2: "20", y2: "6" }),
97
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "12", x2: "20", y2: "12" }),
98
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "18", x2: "20", y2: "18" }),
99
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "6", r: "1", fill: "currentColor" }),
100
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "12", r: "1", fill: "currentColor" }),
101
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "18", r: "1", fill: "currentColor" })
102
+ ] }),
103
+ orderedList: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
104
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "6", x2: "21", y2: "6" }),
105
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "12", x2: "21", y2: "12" }),
106
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "18", x2: "21", y2: "18" }),
107
+ /* @__PURE__ */ jsx("text", { x: "3", y: "7", fontSize: "7", fill: "currentColor", stroke: "none", fontFamily: "sans-serif", children: "1" }),
108
+ /* @__PURE__ */ jsx("text", { x: "3", y: "13", fontSize: "7", fill: "currentColor", stroke: "none", fontFamily: "sans-serif", children: "2" }),
109
+ /* @__PURE__ */ jsx("text", { x: "3", y: "19", fontSize: "7", fill: "currentColor", stroke: "none", fontFamily: "sans-serif", children: "3" })
110
+ ] }),
111
+ taskList: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
112
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "5", width: "6", height: "6", rx: "1" }),
113
+ /* @__PURE__ */ jsx("path", { d: "M5 8l1.5 1.5L9 7" }),
114
+ /* @__PURE__ */ jsx("line", { x1: "13", y1: "8", x2: "21", y2: "8" }),
115
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "14", width: "6", height: "6", rx: "1" }),
116
+ /* @__PURE__ */ jsx("line", { x1: "13", y1: "17", x2: "21", y2: "17" })
117
+ ] }),
118
+ code: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
119
+ /* @__PURE__ */ jsx("polyline", { points: "16 18 22 12 16 6" }),
120
+ /* @__PURE__ */ jsx("polyline", { points: "8 6 2 12 8 18" })
121
+ ] }),
122
+ codeBlock: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
123
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
124
+ /* @__PURE__ */ jsx("polyline", { points: "9 8 5 12 9 16" }),
125
+ /* @__PURE__ */ jsx("polyline", { points: "15 8 19 12 15 16" })
126
+ ] }),
127
+ blockquote: () => /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M10 8V6a6 6 0 0 0-6 6v4h4v-4H6a4 4 0 0 1 4-4zm10 0V6a6 6 0 0 0-6 6v4h4v-4h-2a4 4 0 0 1 4-4z" }) }),
128
+ link: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
129
+ /* @__PURE__ */ jsx("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
130
+ /* @__PURE__ */ jsx("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
131
+ ] }),
132
+ undo: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
133
+ /* @__PURE__ */ jsx("polyline", { points: "1 4 1 10 7 10" }),
134
+ /* @__PURE__ */ jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })
135
+ ] }),
136
+ redo: () => /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
137
+ /* @__PURE__ */ jsx("polyline", { points: "23 4 23 10 17 10" }),
138
+ /* @__PURE__ */ jsx("path", { d: "M20.49 15a9 9 0 1 1-2.12-9.36L23 10" })
139
+ ] })
140
+ };
141
+ var TOOLBAR_LABELS = {
142
+ bold: "Bold",
143
+ italic: "Italic",
144
+ strikethrough: "Strikethrough",
145
+ heading: "Heading",
146
+ bulletList: "Bullet List",
147
+ orderedList: "Ordered List",
148
+ taskList: "Task List",
149
+ code: "Inline Code",
150
+ codeBlock: "Code Block",
151
+ blockquote: "Blockquote",
152
+ link: "Link",
153
+ undo: "Undo",
154
+ redo: "Redo"
155
+ };
156
+ var EditorToolbar = ({
157
+ items,
158
+ onAction
159
+ }) => /* @__PURE__ */ jsx("div", { className: "omu-editor-toolbar flex items-center gap-0.5 px-2 py-1.5 overflow-x-auto", children: items.map((item, i) => {
160
+ if (item === "divider") {
161
+ return /* @__PURE__ */ jsx(
162
+ "div",
163
+ {
164
+ className: "w-px h-4 bg-bdr-secondary mx-1 shrink-0"
165
+ },
166
+ `divider-${i}`
167
+ );
168
+ }
169
+ const Icon = ToolbarIcons[item];
170
+ const label = TOOLBAR_LABELS[item];
171
+ return /* @__PURE__ */ jsx(
172
+ "button",
173
+ {
174
+ type: "button",
175
+ className: "inline-flex items-center justify-center w-7 h-7 rounded-sm text-txt-secondary hover:text-txt-primary hover:bg-bg-tertiary transition-colors shrink-0 cursor-pointer",
176
+ onMouseDown: (e) => {
177
+ e.preventDefault();
178
+ },
179
+ onClick: () => onAction(item),
180
+ title: label,
181
+ "aria-label": label,
182
+ children: /* @__PURE__ */ jsx(Icon, {})
183
+ },
184
+ item
185
+ );
186
+ }) });
187
+ var ModeToggle = ({
188
+ mode,
189
+ onModeChange
190
+ }) => {
191
+ const modes = [
192
+ { value: "wysiwyg", label: "Rich" },
193
+ { value: "markdown", label: "MD" },
194
+ { value: "split", label: "Split" }
195
+ ];
196
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-0.5 ml-auto pl-2 pr-2", children: modes.map((m) => /* @__PURE__ */ jsx(
197
+ "button",
198
+ {
199
+ type: "button",
200
+ className: [
201
+ "px-2 py-0.5 rounded-sm text-xs font-medium transition-colors cursor-pointer",
202
+ mode === m.value ? "bg-bg-tertiary text-txt-primary" : "text-txt-tertiary hover:text-txt-secondary hover:bg-bg-tertiary"
203
+ ].join(" "),
204
+ onClick: () => onModeChange(m.value),
205
+ children: m.label
206
+ },
207
+ m.value
208
+ )) });
209
+ };
210
+ var MilkdownInner = ({
211
+ defaultValue,
212
+ onChange,
213
+ readOnly,
214
+ placeholder,
215
+ autoFocus,
216
+ skipRef,
217
+ actionsRef
218
+ }) => {
219
+ const contentRef = useRef(defaultValue);
220
+ const wrapperRef = useRef(null);
221
+ const { get } = useEditor(
222
+ (root) => MilkdownEditor.make().config((ctx) => {
223
+ ctx.set(rootCtx, root);
224
+ ctx.set(defaultValueCtx, defaultValue);
225
+ ctx.get(listenerCtx).markdownUpdated((_, markdown) => {
226
+ contentRef.current = markdown;
227
+ if (skipRef.current) {
228
+ skipRef.current = false;
229
+ return;
230
+ }
231
+ onChange(markdown);
232
+ });
233
+ }).use(commonmark).use(gfm).use(history).use(clipboard).use(listener)
234
+ );
235
+ useEffect(() => {
236
+ actionsRef.current = {
237
+ getMarkdown: () => contentRef.current,
238
+ setMarkdown: (md) => {
239
+ const editor = get();
240
+ if (!editor) return;
241
+ try {
242
+ skipRef.current = true;
243
+ editor.action(replaceAll(md));
244
+ contentRef.current = md;
245
+ } catch (e) {
246
+ skipRef.current = false;
247
+ console.warn("[Editor] setMarkdown failed:", e);
248
+ }
249
+ },
250
+ /**
251
+ * Execute a Milkdown command by its key.
252
+ * This is how toolbar buttons apply formatting — they call
253
+ * real ProseMirror commands through Milkdown's command manager,
254
+ * which toggles marks/wraps on the current selection.
255
+ */
256
+ runCommand: (key, payload) => {
257
+ const editor = get();
258
+ if (!editor) return false;
259
+ try {
260
+ editor.action((ctx) => {
261
+ const commands = ctx.get(commandsCtx);
262
+ commands.call(key, payload);
263
+ });
264
+ return true;
265
+ } catch (e) {
266
+ console.warn("[Editor] Command failed:", e);
267
+ return false;
268
+ }
269
+ },
270
+ focus: () => {
271
+ const el = wrapperRef.current?.querySelector(".ProseMirror");
272
+ if (el instanceof HTMLElement) el.focus();
273
+ }
274
+ };
275
+ }, [get, skipRef, actionsRef]);
276
+ useEffect(() => {
277
+ if (!placeholder) return;
278
+ const trySet = () => {
279
+ const el = wrapperRef.current?.querySelector(".ProseMirror");
280
+ if (el) {
281
+ el.setAttribute("data-placeholder", placeholder);
282
+ }
283
+ };
284
+ trySet();
285
+ const t = setTimeout(trySet, 100);
286
+ return () => clearTimeout(t);
287
+ }, [placeholder]);
288
+ useEffect(() => {
289
+ if (autoFocus) {
290
+ requestAnimationFrame(() => {
291
+ const el = wrapperRef.current?.querySelector(".ProseMirror");
292
+ if (el instanceof HTMLElement) el.focus();
293
+ });
294
+ }
295
+ }, [autoFocus]);
296
+ const handleWrapperClick = useCallback(() => {
297
+ const el = wrapperRef.current?.querySelector(".ProseMirror");
298
+ if (el instanceof HTMLElement) el.focus();
299
+ }, []);
300
+ return /* @__PURE__ */ jsx(
301
+ "div",
302
+ {
303
+ ref: wrapperRef,
304
+ className: "omu-editor-content flex-1 overflow-y-auto px-4 py-3 flex flex-col min-h-0 cursor-text",
305
+ "data-placeholder": placeholder,
306
+ "data-readonly": readOnly || void 0,
307
+ onClick: readOnly ? void 0 : handleWrapperClick,
308
+ children: /* @__PURE__ */ jsx(Milkdown, {})
309
+ }
310
+ );
311
+ };
312
+ var MarkdownTextarea = ({
313
+ value,
314
+ onChange,
315
+ readOnly,
316
+ placeholder,
317
+ autoFocus,
318
+ minHeight,
319
+ maxHeight
320
+ }) => /* @__PURE__ */ jsx(
321
+ "textarea",
322
+ {
323
+ className: [
324
+ "omu-editor-textarea flex-1 w-full px-4 py-3 resize-none",
325
+ "bg-bg-primary text-txt-primary font-mono text-sm",
326
+ "outline-none border-none",
327
+ "placeholder:text-txt-tertiary"
328
+ ].join(" "),
329
+ value,
330
+ onChange: (e) => onChange(e.target.value),
331
+ readOnly,
332
+ placeholder,
333
+ autoFocus,
334
+ style: {
335
+ minHeight: minHeight ? `${minHeight}px` : void 0,
336
+ maxHeight: maxHeight ? `${maxHeight}px` : void 0
337
+ },
338
+ spellCheck: false
339
+ }
340
+ );
341
+ var Editor = forwardRef(
342
+ ({
343
+ value = "",
344
+ onChange,
345
+ mode: modeProp,
346
+ placeholder,
347
+ toolbar: toolbarProp,
348
+ readOnly = false,
349
+ bordered = false,
350
+ minHeight = 120,
351
+ maxHeight,
352
+ autoFocus = false,
353
+ className = ""
354
+ }, ref) => {
355
+ const [internalMode, setInternalMode] = useState(modeProp ?? "wysiwyg");
356
+ const mode = modeProp ?? internalMode;
357
+ const showModeToggle = modeProp == null;
358
+ const [rawValue, setRawValue] = useState(value);
359
+ const [milkdownKey, setMilkdownKey] = useState(0);
360
+ const skipRef = useRef(false);
361
+ const actionsRef = useRef(null);
362
+ useEffect(() => {
363
+ setRawValue(value);
364
+ }, [value]);
365
+ useImperativeHandle(ref, () => ({
366
+ getMarkdown: () => {
367
+ if (mode === "markdown") return rawValue;
368
+ return actionsRef.current?.getMarkdown() ?? rawValue;
369
+ },
370
+ setMarkdown: (md) => {
371
+ setRawValue(md);
372
+ actionsRef.current?.setMarkdown(md);
373
+ },
374
+ focus: () => {
375
+ actionsRef.current?.focus();
376
+ }
377
+ }), [mode, rawValue]);
378
+ const handleWysiwygChange = useCallback(
379
+ (markdown) => {
380
+ setRawValue(markdown);
381
+ onChange?.(markdown);
382
+ },
383
+ [onChange]
384
+ );
385
+ const handleRawChange = useCallback(
386
+ (markdown) => {
387
+ setRawValue(markdown);
388
+ onChange?.(markdown);
389
+ if (mode === "split") {
390
+ actionsRef.current?.setMarkdown(markdown);
391
+ }
392
+ },
393
+ [onChange, mode]
394
+ );
395
+ const handleToolbarAction = useCallback(
396
+ (item) => {
397
+ if (item === "divider") return;
398
+ if (mode === "markdown") {
399
+ const syntax = TOOLBAR_MARKDOWN[item];
400
+ if (syntax) {
401
+ const newValue = rawValue + syntax;
402
+ setRawValue(newValue);
403
+ onChange?.(newValue);
404
+ }
405
+ return;
406
+ }
407
+ if (!actionsRef.current) return;
408
+ switch (item) {
409
+ case "bold":
410
+ actionsRef.current.runCommand(toggleStrongCommand.key);
411
+ break;
412
+ case "italic":
413
+ actionsRef.current.runCommand(toggleEmphasisCommand.key);
414
+ break;
415
+ case "strikethrough":
416
+ actionsRef.current.runCommand(toggleStrikethroughCommand.key);
417
+ break;
418
+ case "heading": {
419
+ const md = actionsRef.current.getMarkdown();
420
+ const lines = md.split("\n");
421
+ const lastHeadingMatch = lines[lines.length - 1]?.match(/^(#{1,6})\s/);
422
+ const currentLevel = lastHeadingMatch ? lastHeadingMatch[1].length : 0;
423
+ if (currentLevel >= 4 || currentLevel === 0) {
424
+ actionsRef.current.runCommand(wrapInHeadingCommand.key, 2);
425
+ } else {
426
+ actionsRef.current.runCommand(wrapInHeadingCommand.key, currentLevel + 1);
427
+ }
428
+ break;
429
+ }
430
+ case "bulletList":
431
+ actionsRef.current.runCommand(wrapInBulletListCommand.key);
432
+ break;
433
+ case "orderedList":
434
+ actionsRef.current.runCommand(wrapInOrderedListCommand.key);
435
+ break;
436
+ case "taskList": {
437
+ const current = actionsRef.current.getMarkdown();
438
+ const suffix = current.endsWith("\n") ? "- [ ] " : "\n- [ ] ";
439
+ actionsRef.current.setMarkdown(current + suffix);
440
+ break;
441
+ }
442
+ case "code":
443
+ actionsRef.current.runCommand(toggleInlineCodeCommand.key);
444
+ break;
445
+ case "codeBlock":
446
+ actionsRef.current.runCommand(createCodeBlockCommand.key);
447
+ break;
448
+ case "blockquote":
449
+ actionsRef.current.runCommand(wrapInBlockquoteCommand.key);
450
+ break;
451
+ case "link": {
452
+ actionsRef.current.runCommand(toggleLinkCommand.key, { href: "https://" });
453
+ break;
454
+ }
455
+ case "undo":
456
+ actionsRef.current.runCommand(undoCommand.key);
457
+ break;
458
+ case "redo":
459
+ actionsRef.current.runCommand(redoCommand.key);
460
+ break;
461
+ default:
462
+ break;
463
+ }
464
+ },
465
+ [mode, rawValue, onChange]
466
+ );
467
+ const handleModeChange = useCallback(
468
+ (newMode) => {
469
+ if (mode === "wysiwyg" || mode === "split") {
470
+ const current = actionsRef.current?.getMarkdown() ?? rawValue;
471
+ setRawValue(current);
472
+ }
473
+ setInternalMode(newMode);
474
+ if (newMode === "wysiwyg" || newMode === "split") {
475
+ setMilkdownKey((k) => k + 1);
476
+ }
477
+ },
478
+ [mode, rawValue]
479
+ );
480
+ const toolbarItems = toolbarProp === false ? null : toolbarProp ?? DEFAULT_TOOLBAR;
481
+ const showToolbar = toolbarItems != null && !readOnly;
482
+ const wrapperStyle = {
483
+ minHeight: `${minHeight}px`,
484
+ ...maxHeight ? { maxHeight: `${maxHeight}px` } : {}
485
+ };
486
+ return /* @__PURE__ */ jsxs(
487
+ "div",
488
+ {
489
+ className: [
490
+ "omu-editor flex flex-col overflow-hidden",
491
+ "bg-bg-primary text-txt-primary",
492
+ bordered ? "border border-bdr-primary rounded-md focus-within:outline focus-within:outline-2 focus-within:outline-offset-0 focus-within:outline-ring-primary" : "",
493
+ className
494
+ ].join(" "),
495
+ style: wrapperStyle,
496
+ children: [
497
+ (showToolbar || showModeToggle) && /* @__PURE__ */ jsxs("div", { className: "flex items-center bg-bg-secondary border-b border-bdr-secondary", children: [
498
+ showToolbar && /* @__PURE__ */ jsx(EditorToolbar, { items: toolbarItems, onAction: handleToolbarAction }),
499
+ showModeToggle && !readOnly && /* @__PURE__ */ jsx(ModeToggle, { mode, onModeChange: handleModeChange })
500
+ ] }),
501
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 min-h-0 overflow-hidden", children: [
502
+ (mode === "wysiwyg" || mode === "split") && /* @__PURE__ */ jsx("div", { className: `flex flex-col flex-1 min-h-0 ${mode === "split" ? "border-r border-bdr-secondary" : ""}`, children: /* @__PURE__ */ jsx(MilkdownProvider, { children: /* @__PURE__ */ jsx(
503
+ MilkdownInner,
504
+ {
505
+ defaultValue: rawValue,
506
+ onChange: handleWysiwygChange,
507
+ readOnly,
508
+ placeholder,
509
+ autoFocus: autoFocus && mode === "wysiwyg",
510
+ skipRef,
511
+ actionsRef
512
+ }
513
+ ) }, milkdownKey) }),
514
+ (mode === "markdown" || mode === "split") && /* @__PURE__ */ jsx("div", { className: "flex flex-col flex-1 min-h-0", children: /* @__PURE__ */ jsx(
515
+ MarkdownTextarea,
516
+ {
517
+ value: rawValue,
518
+ onChange: handleRawChange,
519
+ readOnly,
520
+ placeholder: mode === "markdown" ? placeholder : "Raw markdown...",
521
+ autoFocus: autoFocus && mode === "markdown",
522
+ minHeight: minHeight - 40,
523
+ maxHeight: maxHeight ? maxHeight - 40 : void 0
524
+ }
525
+ ) })
526
+ ] })
527
+ ]
528
+ }
529
+ );
530
+ }
531
+ );
532
+ Editor.displayName = "Editor";
4
533
  export {
5
- EDITOR_STATUS,
6
- EDITOR_VERSION
534
+ Editor
7
535
  };
8
536
  //# sourceMappingURL=index.js.map