@elabs-ai/components-editor 4.0.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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/chunk-LBC5VJBD.js +3748 -0
- package/dist/chunk-LBC5VJBD.js.map +1 -0
- package/dist/index.css +233 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +362 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/monaco-environment.d.ts +2 -0
- package/dist/lib/monaco-environment.js +44 -0
- package/dist/lib/monaco-environment.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +11 -0
- package/dist/markdown/frontmatter.js +33 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/index.css +233 -0
- package/dist/markdown/index.css.map +1 -0
- package/dist/markdown/index.d.ts +1486 -0
- package/dist/markdown/index.js +4882 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/markdown/parse.d.ts +25 -0
- package/dist/markdown/parse.js +14 -0
- package/dist/markdown/parse.js.map +1 -0
- package/dist/markdown-editor-DfBZibAn.d.ts +702 -0
- package/package.json +101 -0
- package/src/ai-objects/ai-objects-integration.test.tsx +128 -0
- package/src/ai-objects/ai-objects.stories.tsx +233 -0
- package/src/ai-objects/ai-objects.test.tsx +205 -0
- package/src/ai-objects/decision-card.tsx +178 -0
- package/src/ai-objects/directives.ts +136 -0
- package/src/ai-objects/entity.tsx +181 -0
- package/src/ai-objects/index.ts +34 -0
- package/src/ai-objects/knowledge-card.tsx +143 -0
- package/src/calc-block/calc-block.stories.tsx +297 -0
- package/src/calc-block/calc-block.test.tsx +164 -0
- package/src/calc-block/calc-block.tsx +398 -0
- package/src/calc-block/calc-editor-monaco.ts +214 -0
- package/src/calc-block/calc-editor-prose.ts +128 -0
- package/src/calc-block/calc-editor.css +80 -0
- package/src/calc-block/calc-editor.stories.tsx +272 -0
- package/src/calc-block/calc-editor.test.ts +228 -0
- package/src/calc-block/calc-editor.ts +270 -0
- package/src/calc-block/calc-inline.stories.tsx +44 -0
- package/src/calc-block/calc-inline.test.tsx +44 -0
- package/src/calc-block/calc-inline.tsx +75 -0
- package/src/calc-block/index.ts +20 -0
- package/src/calc-block/types.ts +164 -0
- package/src/code-editor/code-editor.stories.tsx +218 -0
- package/src/code-editor/code-editor.test.tsx +134 -0
- package/src/code-editor/code-editor.tsx +250 -0
- package/src/code-editor/index.ts +6 -0
- package/src/code-workspace/code-workspace.stories.tsx +51 -0
- package/src/code-workspace/code-workspace.test.tsx +146 -0
- package/src/code-workspace/code-workspace.tsx +253 -0
- package/src/code-workspace/index.ts +6 -0
- package/src/copy-button/copy-button.tsx +51 -0
- package/src/copy-button/index.ts +1 -0
- package/src/css.d.ts +3 -0
- package/src/diff-editor/diff-editor.stories.tsx +44 -0
- package/src/diff-editor/diff-editor.test.tsx +68 -0
- package/src/diff-editor/diff-editor.tsx +151 -0
- package/src/diff-editor/index.ts +1 -0
- package/src/editor-context-menu/editor-context-menu.tsx +120 -0
- package/src/editor-context-menu/index.ts +1 -0
- package/src/editor-toolbar/editor-toolbar.tsx +83 -0
- package/src/editor-toolbar/index.ts +1 -0
- package/src/index.ts +48 -0
- package/src/lib/editor-completions-monaco.test.ts +206 -0
- package/src/lib/editor-completions-monaco.ts +127 -0
- package/src/lib/editor-completions.test.ts +156 -0
- package/src/lib/editor-completions.ts +153 -0
- package/src/lib/editor-content-access-prose.test.ts +411 -0
- package/src/lib/editor-content-access-prose.ts +172 -0
- package/src/lib/editor-content-access.test.ts +182 -0
- package/src/lib/editor-content-access.ts +109 -0
- package/src/lib/languages.ts +32 -0
- package/src/lib/markdown/diff.test.ts +80 -0
- package/src/lib/markdown/diff.ts +194 -0
- package/src/lib/markdown/directives.ts +342 -0
- package/src/lib/markdown/frontmatter.ts +50 -0
- package/src/lib/markdown/markdown-scale.test.ts +65 -0
- package/src/lib/markdown/markdown-scale.ts +62 -0
- package/src/lib/markdown/merge.test.ts +64 -0
- package/src/lib/markdown/merge.ts +158 -0
- package/src/lib/markdown/slugify.test.ts +156 -0
- package/src/lib/markdown/slugify.ts +47 -0
- package/src/lib/monaco-environment.ts +103 -0
- package/src/lib/monaco-theme-bridge.ts +293 -0
- package/src/lib/use-data-theme.ts +61 -0
- package/src/markdown/frontmatter.ts +14 -0
- package/src/markdown/index.ts +323 -0
- package/src/markdown/parse.test.ts +60 -0
- package/src/markdown/parse.ts +38 -0
- package/src/markdown-academic/citations.test.ts +77 -0
- package/src/markdown-academic/citations.tsx +442 -0
- package/src/markdown-academic/footnotes.tsx +235 -0
- package/src/markdown-academic/index.ts +32 -0
- package/src/markdown-academic/math.tsx +163 -0
- package/src/markdown-academic/toc.tsx +88 -0
- package/src/markdown-editor/completions/completions-menu.tsx +96 -0
- package/src/markdown-editor/completions/completions-prose.test.ts +356 -0
- package/src/markdown-editor/completions/completions-prose.ts +313 -0
- package/src/markdown-editor/completions/completions-widget.tsx +75 -0
- package/src/markdown-editor/completions/index.ts +37 -0
- package/src/markdown-editor/directive-nodes.ts +151 -0
- package/src/markdown-editor/directive-views.test.tsx +189 -0
- package/src/markdown-editor/directive-views.tsx +795 -0
- package/src/markdown-editor/exit-keymap.test.ts +88 -0
- package/src/markdown-editor/exit-keymap.ts +87 -0
- package/src/markdown-editor/index.ts +6 -0
- package/src/markdown-editor/markdown-editor.css +250 -0
- package/src/markdown-editor/markdown-editor.directives.test.tsx +105 -0
- package/src/markdown-editor/markdown-editor.fill.test.ts +34 -0
- package/src/markdown-editor/markdown-editor.paste-embed.test.tsx +297 -0
- package/src/markdown-editor/markdown-editor.stories.tsx +560 -0
- package/src/markdown-editor/markdown-editor.strictmode.test.tsx +38 -0
- package/src/markdown-editor/markdown-editor.table.test.tsx +287 -0
- package/src/markdown-editor/markdown-editor.tsx +560 -0
- package/src/markdown-editor/milkdown-react/editor.tsx +33 -0
- package/src/markdown-editor/milkdown-react/index.ts +8 -0
- package/src/markdown-editor/milkdown-react/types.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-editor.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-get-editor.ts +56 -0
- package/src/markdown-editor/milkdown-react/use-instance.ts +23 -0
- package/src/markdown-editor/paste-embed.ts +355 -0
- package/src/markdown-editor/slash/brand-slash-commands.test.ts +194 -0
- package/src/markdown-editor/slash/brand-slash-commands.ts +324 -0
- package/src/markdown-editor/slash/brand-slash-plugin.test.ts +254 -0
- package/src/markdown-editor/slash/brand-slash-plugin.ts +385 -0
- package/src/markdown-editor/slash/index.ts +107 -0
- package/src/markdown-editor/slash/insert-directive.test.ts +263 -0
- package/src/markdown-editor/slash/insert-directive.ts +345 -0
- package/src/markdown-editor/slash/monaco-slash-menu.test.tsx +198 -0
- package/src/markdown-editor/slash/monaco-slash-menu.tsx +356 -0
- package/src/markdown-editor/slash/shortcut-monaco.ts +69 -0
- package/src/markdown-editor/slash/shortcut.test.ts +157 -0
- package/src/markdown-editor/slash/shortcut.ts +57 -0
- package/src/markdown-editor/slash/slash-menu.stories.tsx +139 -0
- package/src/markdown-editor/slash/slash-menu.tsx +115 -0
- package/src/markdown-editor/slash/slash-scroll.test.tsx +52 -0
- package/src/markdown-editor/slash/slash-widget.tsx +97 -0
- package/src/markdown-editor/slash/source-slash-trigger.test.ts +42 -0
- package/src/markdown-editor/slash/source-slash-trigger.ts +46 -0
- package/src/markdown-editor/table-view.tsx +249 -0
- package/src/markdown-iteration/directive.tsx +92 -0
- package/src/markdown-iteration/edit-context.ts +56 -0
- package/src/markdown-iteration/index.ts +24 -0
- package/src/markdown-iteration/iteration-block.test.tsx +138 -0
- package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +293 -0
- package/src/markdown-iteration/iteration-builder-dialog.tsx +312 -0
- package/src/markdown-iteration/iteration-builder.test.ts +252 -0
- package/src/markdown-iteration/iteration-builder.ts +303 -0
- package/src/markdown-iteration/iteration.test.ts +56 -0
- package/src/markdown-iteration/iteration.tsx +333 -0
- package/src/markdown-iteration/template-dialog.stories.tsx +63 -0
- package/src/markdown-iteration/template-dialog.test.tsx +104 -0
- package/src/markdown-iteration/template-dialog.tsx +116 -0
- package/src/markdown-outline/document-outline.stories.tsx +76 -0
- package/src/markdown-outline/document-outline.tsx +83 -0
- package/src/markdown-outline/index.ts +2 -0
- package/src/markdown-outline/markdown-outline.test.ts +49 -0
- package/src/markdown-outline/markdown-outline.ts +60 -0
- package/src/markdown-preview/code-fence.tsx +193 -0
- package/src/markdown-preview/index.ts +8 -0
- package/src/markdown-preview/markdown-preview-academic.stories.tsx +203 -0
- package/src/markdown-preview/markdown-preview-academic.test.tsx +257 -0
- package/src/markdown-preview/markdown-preview-iteration-calc.test.tsx +71 -0
- package/src/markdown-preview/markdown-preview-iteration-nested.test.tsx +58 -0
- package/src/markdown-preview/markdown-preview-iteration.stories.tsx +187 -0
- package/src/markdown-preview/markdown-preview-iteration.test.tsx +90 -0
- package/src/markdown-preview/markdown-preview-linking.test.tsx +61 -0
- package/src/markdown-preview/markdown-preview-transclusion.test.tsx +33 -0
- package/src/markdown-preview/markdown-preview.stories.tsx +108 -0
- package/src/markdown-preview/markdown-preview.test.tsx +411 -0
- package/src/markdown-preview/markdown-preview.tsx +1541 -0
- package/src/markdown-toolbar/index.ts +8 -0
- package/src/markdown-toolbar/markdown-commands.test.ts +84 -0
- package/src/markdown-toolbar/markdown-commands.ts +104 -0
- package/src/markdown-toolbar/markdown-toolbar.stories.tsx +56 -0
- package/src/markdown-toolbar/markdown-toolbar.tsx +263 -0
- package/src/markdown-workspace/focus-writing.test.ts +52 -0
- package/src/markdown-workspace/focus-writing.ts +38 -0
- package/src/markdown-workspace/index.ts +6 -0
- package/src/markdown-workspace/markdown-workspace.stories.tsx +688 -0
- package/src/markdown-workspace/markdown-workspace.test.tsx +326 -0
- package/src/markdown-workspace/markdown-workspace.tsx +787 -0
- package/src/mermaid-diagram/index.ts +1 -0
- package/src/mermaid-diagram/mermaid-diagram.stories.tsx +42 -0
- package/src/mermaid-diagram/mermaid-diagram.test.tsx +86 -0
- package/src/mermaid-diagram/mermaid-diagram.tsx +318 -0
- package/src/mermaid-diagram/mermaid-viewer.test.tsx +89 -0
- package/src/mermaid-diagram/mermaid-viewer.tsx +367 -0
- package/src/mermaid-diagram/real-parse.test.ts +42 -0
- package/src/mermaid-diagram/remediate.test.ts +60 -0
- package/src/mermaid-diagram/remediate.ts +124 -0
- package/src/mermaid-workspace/index.ts +1 -0
- package/src/mermaid-workspace/mermaid-workspace.stories.tsx +28 -0
- package/src/mermaid-workspace/mermaid-workspace.tsx +71 -0
- package/src/metric-block/index.ts +1 -0
- package/src/metric-block/metric-block.stories.tsx +40 -0
- package/src/metric-block/metric-block.tsx +14 -0
- package/src/monaco-workers.d.ts +10 -0
- package/src/prose/index.ts +14 -0
- package/src/prose/prose.stories.tsx +44 -0
- package/src/prose/prose.test.tsx +44 -0
- package/src/prose/prose.tsx +21 -0
- package/src/timeline/index.ts +18 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MarkdownEditor — a headless Milkdown (ProseMirror) WYSIWYG markdown surface,
|
|
5
|
+
* vendored onto brand-ui. Companion to the Monaco-based CodeEditor: same package,
|
|
6
|
+
* different engine (Monaco = source/diff; Milkdown = direct-manipulation markdown).
|
|
7
|
+
*
|
|
8
|
+
* - Engine dep: only `@milkdown/kit` (headless). React glue is vendored under
|
|
9
|
+
* ./milkdown-react so we never pull `@milkdown/react` → `@milkdown/crepe` → Vue.
|
|
10
|
+
* - Theming: token-driven via markdown-editor.css (all three themes, no raw color).
|
|
11
|
+
* - Controlled (`value` + `onChange`) or uncontrolled (`defaultValue`). Mirrors the
|
|
12
|
+
* platform: `isControlled = value !== undefined`; never flips between modes.
|
|
13
|
+
* - StrictMode-safe (see markdown-editor.strictmode.test.tsx).
|
|
14
|
+
* - The brand `:::` directives render as live @brand React components inside the
|
|
15
|
+
* editor (real <Card>/<Alert>/<MetricBlock>, with inline-editable titles +
|
|
16
|
+
* metric label/value) via @prosemirror-adapter/react — see directive-views.tsx.
|
|
17
|
+
* The shared remark pipeline keeps the editor and the Streamdown preview on one
|
|
18
|
+
* markdown dialect.
|
|
19
|
+
*/
|
|
20
|
+
import "@milkdown/kit/prose/view/style/prosemirror.css";
|
|
21
|
+
import "./markdown-editor.css";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
Editor,
|
|
25
|
+
defaultValueCtx,
|
|
26
|
+
editorViewCtx,
|
|
27
|
+
editorViewOptionsCtx,
|
|
28
|
+
parserCtx,
|
|
29
|
+
rootCtx,
|
|
30
|
+
serializerCtx,
|
|
31
|
+
} from "@milkdown/kit/core";
|
|
32
|
+
import { commonmark } from "@milkdown/kit/preset/commonmark";
|
|
33
|
+
import { gfm } from "@milkdown/kit/preset/gfm";
|
|
34
|
+
import { history } from "@milkdown/kit/plugin/history";
|
|
35
|
+
import { listener, listenerCtx } from "@milkdown/kit/plugin/listener";
|
|
36
|
+
import type { Node as ProseNode } from "@milkdown/kit/prose/model";
|
|
37
|
+
import { TextSelection } from "@milkdown/kit/prose/state";
|
|
38
|
+
import type { EditorView } from "@milkdown/kit/prose/view";
|
|
39
|
+
import { getMarkdown, replaceAll } from "@milkdown/kit/utils";
|
|
40
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
41
|
+
import {
|
|
42
|
+
ProsemirrorAdapterProvider,
|
|
43
|
+
useNodeViewFactory,
|
|
44
|
+
usePluginViewFactory,
|
|
45
|
+
useWidgetViewFactory,
|
|
46
|
+
} from "@prosemirror-adapter/react";
|
|
47
|
+
import {
|
|
48
|
+
forwardRef,
|
|
49
|
+
useContext,
|
|
50
|
+
useEffect,
|
|
51
|
+
useImperativeHandle,
|
|
52
|
+
useRef,
|
|
53
|
+
type HTMLAttributes,
|
|
54
|
+
} from "react";
|
|
55
|
+
|
|
56
|
+
import { calcProsePlugins } from "../calc-block/calc-editor-prose";
|
|
57
|
+
import type { CalcEditorHooks } from "../calc-block/types";
|
|
58
|
+
import { completionsViewPlugins } from "./completions";
|
|
59
|
+
import type { EditorCompletionProvider } from "../lib/editor-completions";
|
|
60
|
+
import type { EditorContentAccess, EditorSelection } from "../lib/editor-content-access";
|
|
61
|
+
import { proseMirrorContentAccess, selectionWatchPlugin } from "../lib/editor-content-access-prose";
|
|
62
|
+
import { markdownScaleVars } from "../lib/markdown/markdown-scale";
|
|
63
|
+
import { plainText, slugifyHeading, uniqueSlug } from "../lib/markdown/slugify";
|
|
64
|
+
import {
|
|
65
|
+
IterationEditContext,
|
|
66
|
+
type IterationEditHandler,
|
|
67
|
+
} from "../markdown-iteration/edit-context";
|
|
68
|
+
import { parseMarkdownOutline } from "../markdown-outline";
|
|
69
|
+
import { directivePlugins } from "./directive-nodes";
|
|
70
|
+
import { directiveViewPlugins } from "./directive-views";
|
|
71
|
+
import { exitKeymapPlugins } from "./exit-keymap";
|
|
72
|
+
import { Milkdown, MilkdownProvider, useEditor, useInstance } from "./milkdown-react";
|
|
73
|
+
import { pasteEmbedPlugin, type EmbedAssetFn } from "./paste-embed";
|
|
74
|
+
import { brandSlashViewPlugins, type SlashCommand } from "./slash";
|
|
75
|
+
import { tableViewPlugins } from "./table-view";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Imperative handle exposed via `ref`.
|
|
79
|
+
*
|
|
80
|
+
* Extends {@link EditorContentAccess} so any consumer of a `MarkdownEditor`
|
|
81
|
+
* ref can drive AI editing operations (insert, replace, subscribe to selection)
|
|
82
|
+
* through the engine-agnostic interface, alongside the markdown-specific helpers.
|
|
83
|
+
*
|
|
84
|
+
* `getText()` is an alias for `getMarkdown()` (same output; both return the full
|
|
85
|
+
* document serialized to markdown). The duplication is intentional: `getText`
|
|
86
|
+
* satisfies the `EditorContentAccess` interface contract; `getMarkdown` is the
|
|
87
|
+
* historically-named markdown-specific method. JSDoc notes the equivalence.
|
|
88
|
+
*/
|
|
89
|
+
export interface MarkdownEditorHandle extends EditorContentAccess {
|
|
90
|
+
/**
|
|
91
|
+
* Serialize the current document to a markdown string.
|
|
92
|
+
* Equivalent to `getText()` — both return the full document as markdown.
|
|
93
|
+
*/
|
|
94
|
+
getMarkdown: () => string;
|
|
95
|
+
/**
|
|
96
|
+
* Serialize the current document, or `null` while the engine is still
|
|
97
|
+
* booting. Used to capture the pre-edit normalization BASELINE for the
|
|
98
|
+
* lossless-edit merge (WI-1) — unlike `getMarkdown` it never falls back to
|
|
99
|
+
* the raw input, so a non-null result is always the editor's own output.
|
|
100
|
+
*/
|
|
101
|
+
serialized: () => string | null;
|
|
102
|
+
/**
|
|
103
|
+
* Best-effort: scroll the heading whose outline slug matches into view (#273).
|
|
104
|
+
* Walks the ProseMirror doc for `heading` nodes, slugifies their text with the
|
|
105
|
+
* same algorithm as `parseMarkdownOutline`, and scrolls the first match into
|
|
106
|
+
* view. No-op (never throws) while the engine is booting or if no match.
|
|
107
|
+
*/
|
|
108
|
+
scrollToHeading: (slug: string) => void;
|
|
109
|
+
/**
|
|
110
|
+
* Best-effort: scroll toward FULL-SOURCE 1-based `line` by resolving the
|
|
111
|
+
* nearest preceding heading in the ProseMirror doc (no exact source map in
|
|
112
|
+
* WYSIWYG — Milkdown keeps no `data-sourcepos`). No-op when no preceding
|
|
113
|
+
* heading is found or while booting. (#273)
|
|
114
|
+
*
|
|
115
|
+
* For precise navigation, prefer `scrollToHeading(slug)` — the workspace
|
|
116
|
+
* resolves the slug from the line via `parseMarkdownOutline` + `fmOffset`
|
|
117
|
+
* before forwarding here.
|
|
118
|
+
*/
|
|
119
|
+
revealLine: (line: number, opts?: { center?: boolean }) => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface MarkdownEditorProps extends Omit<
|
|
123
|
+
HTMLAttributes<HTMLDivElement>,
|
|
124
|
+
"onChange" | "defaultValue"
|
|
125
|
+
> {
|
|
126
|
+
/** Controlled markdown value. Pair with `onChange`. */
|
|
127
|
+
value?: string;
|
|
128
|
+
/** Initial markdown for uncontrolled use. */
|
|
129
|
+
defaultValue?: string;
|
|
130
|
+
/** Fires on every edit with the full markdown document. */
|
|
131
|
+
onChange?: (markdown: string) => void;
|
|
132
|
+
/** Render the editor read-only (still selectable, not editable). */
|
|
133
|
+
readOnly?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Accessible name for the editable surface. Milkdown/ProseMirror renders an ARIA
|
|
136
|
+
* `textbox` (`contenteditable`); without a name, screen readers announce an
|
|
137
|
+
* unlabeled field. Set onto the ProseMirror view's `attributes`. Default
|
|
138
|
+
* `"Markdown editor"`.
|
|
139
|
+
*/
|
|
140
|
+
ariaLabel?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Enable the `/` command menu (insert brand `:::` directives + basic blocks
|
|
143
|
+
* live at the caret). `true` (default) uses the built-in
|
|
144
|
+
* {@link BRAND_SLASH_COMMANDS}; pass a `commands` array to extend/replace them,
|
|
145
|
+
* or `false` to disable. The `trigger` defaults to `"/"`.
|
|
146
|
+
*
|
|
147
|
+
* Pass `shortcut` (default `"Mod-Shift-O"`) to control the keyboard shortcut that
|
|
148
|
+
* opens the menu at the caret without typing the trigger character — works in
|
|
149
|
+
* BOTH the WYSIWYG pane and the Monaco source pane (#271).
|
|
150
|
+
*/
|
|
151
|
+
slashMenu?: boolean | { commands?: SlashCommand[]; trigger?: string; shortcut?: string };
|
|
152
|
+
/**
|
|
153
|
+
* Opt-in calc authoring inside ```calc fences (off by default). Supply the
|
|
154
|
+
* consumer's hooks — `tokenize` (highlight), `evaluate` (result inlays), and/or
|
|
155
|
+
* `complete` (autocomplete; Monaco surface). The library DECORATES, the consumer
|
|
156
|
+
* COMPUTES — no calc engine is bundled. Mirrors `MarkdownPreview`'s `evaluate`.
|
|
157
|
+
*/
|
|
158
|
+
calc?: CalcEditorHooks;
|
|
159
|
+
/**
|
|
160
|
+
* Declarative completion providers (#283) — e.g. `[[wikilink]]` autocomplete.
|
|
161
|
+
* Off by default; mirrors the `slashMenu`/`calc` opt-in pattern. Each provider
|
|
162
|
+
* registers a `triggerCharacters` set + a `provide(ctx)` that returns
|
|
163
|
+
* candidates; the library owns detecting the trigger and rendering/inserting.
|
|
164
|
+
*
|
|
165
|
+
* This is a DELIBERATELY MINIMAL, best-effort mirror of the Monaco source-pane
|
|
166
|
+
* behavior (see `markdown-editor/completions/completions-prose.ts` for the
|
|
167
|
+
* exact gaps — no real cross-block line/column, a plain listbox instead of a
|
|
168
|
+
* native suggest widget). Forwarded from `MarkdownWorkspace`'s `completions`
|
|
169
|
+
* prop, which also wires the FULL Monaco/source-pane path.
|
|
170
|
+
*/
|
|
171
|
+
completions?: EditorCompletionProvider[];
|
|
172
|
+
/**
|
|
173
|
+
* Host-provided callback for image paste/drop embedding.
|
|
174
|
+
*
|
|
175
|
+
* When set, the editor intercepts paste and drop events that contain image
|
|
176
|
+
* files, shows an inline "uploading…" placeholder, calls this function with
|
|
177
|
+
* the `File`, and on resolve inserts `` in the
|
|
178
|
+
* document. On reject the placeholder is removed and an inline error chip
|
|
179
|
+
* (+ toast) is shown — the document never contains a broken image.
|
|
180
|
+
*
|
|
181
|
+
* If not set, the editor's default paste/drop behavior is unchanged.
|
|
182
|
+
*
|
|
183
|
+
* The library NEVER stores assets — all persistence is the host's responsibility.
|
|
184
|
+
*/
|
|
185
|
+
onEmbedAsset?: EmbedAssetFn;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type { EmbedAssetFn };
|
|
189
|
+
|
|
190
|
+
interface ViewProps {
|
|
191
|
+
initialValue: string;
|
|
192
|
+
value?: string;
|
|
193
|
+
onChange?: (markdown: string) => void;
|
|
194
|
+
readOnly: boolean;
|
|
195
|
+
ariaLabel: string;
|
|
196
|
+
slashMenu: boolean | { commands?: SlashCommand[]; trigger?: string; shortcut?: string };
|
|
197
|
+
calc?: CalcEditorHooks;
|
|
198
|
+
completions?: EditorCompletionProvider[];
|
|
199
|
+
onEmbedAsset?: EmbedAssetFn;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Walk the ProseMirror doc for a `heading` node whose slug (slugified with the
|
|
204
|
+
* SAME algorithm as `parseMarkdownOutline`) matches `slug`, and scroll it into
|
|
205
|
+
* view. Best-effort: a no-op (never throws) when no match / out of range. Shared
|
|
206
|
+
* by `scrollToHeading` and the best-effort `revealLine` (#273).
|
|
207
|
+
*/
|
|
208
|
+
function scrollHeadingBySlug(editor: Editor, slug: string): void {
|
|
209
|
+
editor.action((ctx) => {
|
|
210
|
+
const view = ctx.get(editorViewCtx);
|
|
211
|
+
const { doc } = view.state;
|
|
212
|
+
const used = new Map<string, number>();
|
|
213
|
+
let targetPos: number | null = null;
|
|
214
|
+
doc.forEach((node, offset) => {
|
|
215
|
+
if (targetPos !== null) return;
|
|
216
|
+
if (node.type.name === "heading") {
|
|
217
|
+
const id = uniqueSlug(slugifyHeading(plainText(node.textContent)), used);
|
|
218
|
+
if (id === slug) targetPos = offset + 1; // +1 steps inside the heading node
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
if (targetPos === null) return;
|
|
222
|
+
try {
|
|
223
|
+
const resolved = doc.resolve(targetPos);
|
|
224
|
+
view.dispatch(view.state.tr.setSelection(TextSelection.near(resolved)).scrollIntoView());
|
|
225
|
+
} catch {
|
|
226
|
+
// Guard against out-of-range positions — never throw.
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const MarkdownEditorView = forwardRef<MarkdownEditorHandle, ViewProps>(function MarkdownEditorView(
|
|
232
|
+
{
|
|
233
|
+
initialValue,
|
|
234
|
+
value,
|
|
235
|
+
onChange,
|
|
236
|
+
readOnly,
|
|
237
|
+
ariaLabel,
|
|
238
|
+
slashMenu,
|
|
239
|
+
calc,
|
|
240
|
+
completions,
|
|
241
|
+
onEmbedAsset,
|
|
242
|
+
},
|
|
243
|
+
ref,
|
|
244
|
+
) {
|
|
245
|
+
// Latest onChange via a ref so the create effect can run once per `readOnly`.
|
|
246
|
+
const onChangeRef = useRef(onChange);
|
|
247
|
+
onChangeRef.current = onChange;
|
|
248
|
+
// Tracks the editor's current markdown — both the "preserve across recreate"
|
|
249
|
+
// value and the echo-guard for controlled `value` syncing.
|
|
250
|
+
const lastMarkdown = useRef(initialValue);
|
|
251
|
+
|
|
252
|
+
// Renders the brand `:::` directives as live @brand React components inside the
|
|
253
|
+
// editor (must run under <ProsemirrorAdapterProvider>). The factory is stable.
|
|
254
|
+
const nodeViewFactory = useNodeViewFactory();
|
|
255
|
+
// The widget factory backs the `/` command menu (rendered as a ProseMirror
|
|
256
|
+
// widget at the caret). Same adapter, also stable.
|
|
257
|
+
const widgetViewFactory = useWidgetViewFactory();
|
|
258
|
+
// The plugin-view factory mounts the table controls toolbar as a ProseMirror
|
|
259
|
+
// plugin view (outside contentEditable, no focus conflicts).
|
|
260
|
+
const pluginViewFactory = usePluginViewFactory();
|
|
261
|
+
|
|
262
|
+
// Resolve the slash config through a ref so a fresh `commands` array identity
|
|
263
|
+
// each render never rebuilds the editor — only enabling/disabling does.
|
|
264
|
+
const slashEnabled = slashMenu !== false;
|
|
265
|
+
const slashConfigRef = useRef<{ commands?: SlashCommand[]; trigger?: string; shortcut?: string }>(
|
|
266
|
+
{},
|
|
267
|
+
);
|
|
268
|
+
slashConfigRef.current = typeof slashMenu === "object" ? slashMenu : {};
|
|
269
|
+
|
|
270
|
+
// Calc authoring hooks read through a ref — like the slash config, a fresh
|
|
271
|
+
// `calc` object identity each render must NOT rebuild the editor; only
|
|
272
|
+
// toggling the feature on/off does (the plugin reads the latest hooks lazily).
|
|
273
|
+
const calcEnabled = calc != null;
|
|
274
|
+
const calcRef = useRef<CalcEditorHooks | undefined>(calc);
|
|
275
|
+
calcRef.current = calc;
|
|
276
|
+
|
|
277
|
+
// Completion providers (#283) read through a ref — a fresh `completions`
|
|
278
|
+
// array identity each render must NOT rebuild the editor (or re-register the
|
|
279
|
+
// Monaco-side global provider); only enabling/disabling the feature does. The
|
|
280
|
+
// plugin's async `provide()` fetch reads the LATEST list via this getter.
|
|
281
|
+
const completionsEnabled = completions != null;
|
|
282
|
+
const completionsRef = useRef<EditorCompletionProvider[] | undefined>(completions);
|
|
283
|
+
completionsRef.current = completions;
|
|
284
|
+
|
|
285
|
+
// Keep onEmbedAsset in a ref so the editor factory captures it by reference —
|
|
286
|
+
// a new function identity each render never causes an editor rebuild.
|
|
287
|
+
const onEmbedAssetRef = useRef(onEmbedAsset);
|
|
288
|
+
onEmbedAssetRef.current = onEmbedAsset;
|
|
289
|
+
|
|
290
|
+
// #223: the live `IterationEditContext` handler (set by a consumer's
|
|
291
|
+
// `IterationBuilderProvider` / `IterationTemplateProvider` ABOVE this editor),
|
|
292
|
+
// read through a ref like the other slash/calc hooks — a fresh Provider value
|
|
293
|
+
// each render must NOT rebuild the editor; the plugin resolves it lazily via
|
|
294
|
+
// the getter passed to `brandSlashViewPlugins`.
|
|
295
|
+
const iterationEditHandler = useContext(IterationEditContext);
|
|
296
|
+
const iterationEditHandlerRef = useRef<IterationEditHandler | null>(iterationEditHandler);
|
|
297
|
+
iterationEditHandlerRef.current = iterationEditHandler;
|
|
298
|
+
|
|
299
|
+
// Shared selection-change listener set: the selectionWatchPlugin writes to it;
|
|
300
|
+
// proseMirrorContentAccess reads/writes subscriptions. Stable across re-renders
|
|
301
|
+
// (created once) — the plugin reads it via the thunk each transaction.
|
|
302
|
+
const selectionListeners = useRef(new Set<(sel: EditorSelection) => void>()).current;
|
|
303
|
+
|
|
304
|
+
// The serializeSlice closure is built lazily in useImperativeHandle (needs
|
|
305
|
+
// getInstance), but the plugin reads the LATEST version via a thunk so it never
|
|
306
|
+
// captures a stale closure. We hold a ref that useImperativeHandle fills in.
|
|
307
|
+
const serializeSliceRef = useRef<(view: EditorView) => string>(() => "");
|
|
308
|
+
|
|
309
|
+
useEditor(
|
|
310
|
+
(root) => {
|
|
311
|
+
// Build the paste-embed plugin with an indirection through the ref so the
|
|
312
|
+
// plugin always calls the latest host callback without rebuilding.
|
|
313
|
+
const embedPlugin = pasteEmbedPlugin(
|
|
314
|
+
onEmbedAssetRef.current ? (file: File) => onEmbedAssetRef.current!(file) : undefined,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
let editor = Editor.make()
|
|
318
|
+
.config((ctx) => {
|
|
319
|
+
ctx.set(rootCtx, root);
|
|
320
|
+
ctx.set(defaultValueCtx, lastMarkdown.current);
|
|
321
|
+
ctx.update(editorViewOptionsCtx, (prev) => ({
|
|
322
|
+
...prev,
|
|
323
|
+
editable: () => !readOnly,
|
|
324
|
+
// Name the ProseMirror `role="textbox"` surface so AT announces it.
|
|
325
|
+
// Spread prev.attributes so we never clobber Milkdown's own view attrs.
|
|
326
|
+
attributes: { ...prev.attributes, "aria-label": ariaLabel },
|
|
327
|
+
}));
|
|
328
|
+
ctx.get(listenerCtx).markdownUpdated((_, markdown) => {
|
|
329
|
+
lastMarkdown.current = markdown;
|
|
330
|
+
onChangeRef.current?.(markdown);
|
|
331
|
+
});
|
|
332
|
+
})
|
|
333
|
+
.use(commonmark)
|
|
334
|
+
.use(gfm)
|
|
335
|
+
.use(tableViewPlugins(pluginViewFactory))
|
|
336
|
+
.use(exitKeymapPlugins())
|
|
337
|
+
.use(history)
|
|
338
|
+
.use(listener)
|
|
339
|
+
.use(directivePlugins)
|
|
340
|
+
.use(directiveViewPlugins(nodeViewFactory))
|
|
341
|
+
.use(embedPlugin)
|
|
342
|
+
// Always-present selection-watch plugin — notifies selectionListeners when
|
|
343
|
+
// the ProseMirror selection changes. Listeners subscribe/unsubscribe via the
|
|
344
|
+
// proseMirrorContentAccess adapter; the plugin itself is stateless per-sub.
|
|
345
|
+
.use(
|
|
346
|
+
selectionWatchPlugin(
|
|
347
|
+
() => selectionListeners,
|
|
348
|
+
() => serializeSliceRef.current,
|
|
349
|
+
),
|
|
350
|
+
);
|
|
351
|
+
if (slashEnabled) {
|
|
352
|
+
editor = editor.use(
|
|
353
|
+
brandSlashViewPlugins(widgetViewFactory, {
|
|
354
|
+
...slashConfigRef.current,
|
|
355
|
+
getIterationEditHandler: () => iterationEditHandlerRef.current,
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
if (calcEnabled) {
|
|
360
|
+
editor = editor.use(calcProsePlugins(() => calcRef.current));
|
|
361
|
+
}
|
|
362
|
+
if (completionsEnabled) {
|
|
363
|
+
editor = editor.use(
|
|
364
|
+
completionsViewPlugins(widgetViewFactory, () => completionsRef.current),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return editor;
|
|
368
|
+
},
|
|
369
|
+
[readOnly, ariaLabel, slashEnabled, calcEnabled, completionsEnabled],
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const [loading, getInstance] = useInstance();
|
|
373
|
+
|
|
374
|
+
// Controlled sync: push external `value` into the editor only when it diverges
|
|
375
|
+
// from what the editor last emitted (avoids clobbering on our own echo).
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
if (loading || value === undefined) return;
|
|
378
|
+
if (value === lastMarkdown.current) return;
|
|
379
|
+
const editor = getInstance();
|
|
380
|
+
if (!editor) return;
|
|
381
|
+
lastMarkdown.current = value;
|
|
382
|
+
editor.action(replaceAll(value));
|
|
383
|
+
}, [loading, value, getInstance]);
|
|
384
|
+
|
|
385
|
+
useImperativeHandle(
|
|
386
|
+
ref,
|
|
387
|
+
() => {
|
|
388
|
+
// Build serialize/parse closures via getInstance().action(ctx => ...) —
|
|
389
|
+
// the exact technique used by directive-views.tsx (readBodyMarkdown /
|
|
390
|
+
// writeBodyMarkdown). These are re-built whenever getInstance changes
|
|
391
|
+
// (i.e. when the editor is recreated).
|
|
392
|
+
|
|
393
|
+
/** Serialize the current selection slice to markdown ("" when collapsed). */
|
|
394
|
+
const serializeSlice = (view: EditorView): string => {
|
|
395
|
+
const editor = getInstance();
|
|
396
|
+
if (!editor) return "";
|
|
397
|
+
const { selection } = view.state;
|
|
398
|
+
if (selection.empty) return "";
|
|
399
|
+
try {
|
|
400
|
+
return editor
|
|
401
|
+
.action((ctx) => {
|
|
402
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
403
|
+
const serialize = (ctx as any).get(serializerCtx) as (node: ProseNode) => string;
|
|
404
|
+
const doc = selection.content().content;
|
|
405
|
+
// Wrap in a top-level doc node so the serializer can emit all block types.
|
|
406
|
+
const wrapper = view.state.doc.type.schema.topNodeType.create(null, doc);
|
|
407
|
+
return serialize(wrapper);
|
|
408
|
+
})
|
|
409
|
+
.trim();
|
|
410
|
+
} catch {
|
|
411
|
+
// Serializer unavailable (e.g. booting) — fall back to plain text.
|
|
412
|
+
return view.state.doc.textBetween(selection.from, selection.to, "\n");
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// Update the ref so the selection-watch plugin always reads the latest closure.
|
|
417
|
+
serializeSliceRef.current = serializeSlice;
|
|
418
|
+
|
|
419
|
+
/** Parse `md` as markdown and replace the current selection; plain-text fallback. */
|
|
420
|
+
const parseAndReplace = (view: EditorView, md: string): void => {
|
|
421
|
+
const editor = getInstance();
|
|
422
|
+
if (!editor) return;
|
|
423
|
+
try {
|
|
424
|
+
editor.action((ctx) => {
|
|
425
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
426
|
+
const parse = (ctx as any).get(parserCtx) as (md: string) => ProseNode | null;
|
|
427
|
+
const parsed = parse(md);
|
|
428
|
+
if (!parsed) {
|
|
429
|
+
// Null parse result → plain-text insert.
|
|
430
|
+
view.dispatch(view.state.tr.insertText(md));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
// Replace the current selection with the parsed content.
|
|
434
|
+
const tr = view.state.tr.replaceSelectionWith(parsed);
|
|
435
|
+
view.dispatch(tr);
|
|
436
|
+
});
|
|
437
|
+
} catch {
|
|
438
|
+
// Parse failure → plain-text fallback (never blanks the doc, never throws).
|
|
439
|
+
try {
|
|
440
|
+
view.dispatch(view.state.tr.insertText(md));
|
|
441
|
+
} catch {
|
|
442
|
+
// Guard: if even insertText fails (e.g. read-only), silently no-op.
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const getView = (): EditorView | null => {
|
|
448
|
+
const editor = getInstance();
|
|
449
|
+
if (!editor) return null;
|
|
450
|
+
try {
|
|
451
|
+
return editor.action((ctx) => ctx.get(editorViewCtx));
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const access = proseMirrorContentAccess({
|
|
458
|
+
getView,
|
|
459
|
+
getText: () => getInstance()?.action(getMarkdown()) ?? lastMarkdown.current,
|
|
460
|
+
serializeSlice,
|
|
461
|
+
parseAndReplace,
|
|
462
|
+
listeners: selectionListeners,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
// EditorContentAccess methods (via proseMirrorContentAccess).
|
|
467
|
+
getText: access.getText,
|
|
468
|
+
getSelection: access.getSelection,
|
|
469
|
+
replaceSelection: access.replaceSelection,
|
|
470
|
+
insertAtCursor: access.insertAtCursor,
|
|
471
|
+
focus: access.focus,
|
|
472
|
+
onSelectionChange: access.onSelectionChange,
|
|
473
|
+
|
|
474
|
+
// Markdown-specific methods.
|
|
475
|
+
getMarkdown: () => getInstance()?.action(getMarkdown()) ?? lastMarkdown.current,
|
|
476
|
+
serialized: () => getInstance()?.action(getMarkdown()) ?? null,
|
|
477
|
+
|
|
478
|
+
scrollToHeading: (slug: string) => {
|
|
479
|
+
const editor = getInstance();
|
|
480
|
+
if (editor) scrollHeadingBySlug(editor, slug);
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
revealLine: (line: number, _opts?: { center?: boolean }) => {
|
|
484
|
+
// WYSIWYG has no source map (Milkdown keeps no data-sourcepos), so this is
|
|
485
|
+
// best-effort: serialize the doc, resolve the nearest preceding heading via
|
|
486
|
+
// the shared outline parser, and scroll to it. No-op when none. (#273)
|
|
487
|
+
const editor = getInstance();
|
|
488
|
+
if (!editor) return;
|
|
489
|
+
const md = editor.action(getMarkdown());
|
|
490
|
+
const preceding = parseMarkdownOutline(md)
|
|
491
|
+
.filter((item) => item.line <= line)
|
|
492
|
+
.at(-1);
|
|
493
|
+
if (preceding) scrollHeadingBySlug(editor, preceding.id);
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
498
|
+
[getInstance],
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
return <Milkdown />;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorProps>(
|
|
505
|
+
function MarkdownEditor(
|
|
506
|
+
{
|
|
507
|
+
value,
|
|
508
|
+
defaultValue,
|
|
509
|
+
onChange,
|
|
510
|
+
readOnly = false,
|
|
511
|
+
ariaLabel = "Markdown editor",
|
|
512
|
+
slashMenu = true,
|
|
513
|
+
calc,
|
|
514
|
+
completions,
|
|
515
|
+
onEmbedAsset,
|
|
516
|
+
className,
|
|
517
|
+
style,
|
|
518
|
+
...props
|
|
519
|
+
},
|
|
520
|
+
ref,
|
|
521
|
+
) {
|
|
522
|
+
// Capture the initial value once; later `value` changes flow through the
|
|
523
|
+
// controlled-sync effect, not a remount.
|
|
524
|
+
const initialValue = useRef(value ?? defaultValue ?? "").current;
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<div
|
|
528
|
+
data-testid="markdown-editor"
|
|
529
|
+
className={cn(
|
|
530
|
+
// A 1px hairline focus ring (not a heavy 2px ring) — the editable is a
|
|
531
|
+
// large surface, so a thinner edit-mode ring reads calmer while still
|
|
532
|
+
// meeting the visible-focus requirement (same `ring` token). (A7)
|
|
533
|
+
"milkdown-host overflow-auto rounded-md border border-border bg-background text-foreground focus-within:ring-1 focus-within:ring-ring",
|
|
534
|
+
className,
|
|
535
|
+
)}
|
|
536
|
+
// Publish the shared markdown scale as CSS vars the editor CSS reads, so the
|
|
537
|
+
// WYSIWYG headings/measure match the preview (single source of truth, #18).
|
|
538
|
+
style={{ ...markdownScaleVars(), ...style }}
|
|
539
|
+
{...props}
|
|
540
|
+
>
|
|
541
|
+
<MilkdownProvider>
|
|
542
|
+
<ProsemirrorAdapterProvider>
|
|
543
|
+
<MarkdownEditorView
|
|
544
|
+
ref={ref}
|
|
545
|
+
initialValue={initialValue}
|
|
546
|
+
value={value}
|
|
547
|
+
onChange={onChange}
|
|
548
|
+
readOnly={readOnly}
|
|
549
|
+
ariaLabel={ariaLabel}
|
|
550
|
+
slashMenu={slashMenu}
|
|
551
|
+
calc={calc}
|
|
552
|
+
completions={completions}
|
|
553
|
+
onEmbedAsset={onEmbedAsset}
|
|
554
|
+
/>
|
|
555
|
+
</ProsemirrorAdapterProvider>
|
|
556
|
+
</MilkdownProvider>
|
|
557
|
+
</div>
|
|
558
|
+
);
|
|
559
|
+
},
|
|
560
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vendored + adapted from `@milkdown/react` (MIT — © 2020-present Mirone).
|
|
5
|
+
*
|
|
6
|
+
* `<MilkdownProvider>` holds the editor instance/loading state in context;
|
|
7
|
+
* `<Milkdown />` renders the root node the ProseMirror view mounts into.
|
|
8
|
+
*/
|
|
9
|
+
import type { Editor } from "@milkdown/kit/core";
|
|
10
|
+
import { type FC, type ReactNode, useMemo, useRef, useState } from "react";
|
|
11
|
+
|
|
12
|
+
import type { EditorInfoCtx, GetEditor } from "./types";
|
|
13
|
+
import { editorInfoContext, useGetEditor } from "./use-get-editor";
|
|
14
|
+
|
|
15
|
+
export const Milkdown: FC = () => {
|
|
16
|
+
const domRef = useGetEditor();
|
|
17
|
+
|
|
18
|
+
return <div data-milkdown-root ref={domRef} />;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const MilkdownProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
|
22
|
+
const dom = useRef<HTMLDivElement | undefined>(undefined);
|
|
23
|
+
const [editorFactory, setEditorFactory] = useState<GetEditor | undefined>(undefined);
|
|
24
|
+
const editor = useRef<Editor | undefined>(undefined);
|
|
25
|
+
const [loading, setLoading] = useState(true);
|
|
26
|
+
|
|
27
|
+
const editorInfoCtx = useMemo<EditorInfoCtx>(
|
|
28
|
+
() => ({ loading, dom, editor, setLoading, editorFactory, setEditorFactory }),
|
|
29
|
+
[loading, editorFactory],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return <editorInfoContext.Provider value={editorInfoCtx}>{children}</editorInfoContext.Provider>;
|
|
33
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored Milkdown ⇄ React integration (MIT — adapted from @milkdown/react,
|
|
3
|
+
* © 2020-present Mirone). Depends only on the headless `@milkdown/kit`.
|
|
4
|
+
*/
|
|
5
|
+
export { Milkdown, MilkdownProvider } from "./editor";
|
|
6
|
+
export { useEditor } from "./use-editor";
|
|
7
|
+
export { useInstance, type Instance } from "./use-instance";
|
|
8
|
+
export type { GetEditor, UseEditorReturn, EditorInfoCtx } from "./types";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored + adapted from `@milkdown/react` (MIT — Copyright (c) 2020-present
|
|
3
|
+
* Mirone, https://github.com/Milkdown/milkdown).
|
|
4
|
+
*
|
|
5
|
+
* We vendor the ~120-line React integration so `@elabs-ai/components-editor` depends only on
|
|
6
|
+
* the headless `@milkdown/kit` — NOT `@milkdown/react`, which hard-depends on
|
|
7
|
+
* `@milkdown/crepe` (and therefore Vue 3). The upstream `GetEditor` union with
|
|
8
|
+
* `CrepeBuilder` is intentionally dropped: we never use Crepe.
|
|
9
|
+
*/
|
|
10
|
+
import type { Editor } from "@milkdown/kit/core";
|
|
11
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
12
|
+
|
|
13
|
+
export type GetEditor = (container: HTMLElement) => Editor | undefined;
|
|
14
|
+
|
|
15
|
+
export interface UseEditorReturn {
|
|
16
|
+
readonly loading: boolean;
|
|
17
|
+
readonly get: () => Editor | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface EditorInfoCtx {
|
|
21
|
+
loading: boolean;
|
|
22
|
+
setLoading: Dispatch<SetStateAction<boolean>>;
|
|
23
|
+
dom: RefObject<HTMLDivElement | undefined>;
|
|
24
|
+
editor: RefObject<Editor | undefined>;
|
|
25
|
+
editorFactory: GetEditor | undefined;
|
|
26
|
+
setEditorFactory: Dispatch<SetStateAction<GetEditor | undefined>>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored + adapted from `@milkdown/react` (MIT — © 2020-present Mirone).
|
|
3
|
+
*
|
|
4
|
+
* `useEditor(getEditor, deps)` registers the editor factory with the provider.
|
|
5
|
+
* `getEditor` is memoized on `deps`, so the mount effect runs exactly once per
|
|
6
|
+
* dependency change (re-creating the editor when `deps` change).
|
|
7
|
+
*/
|
|
8
|
+
import { type DependencyList, useCallback, useContext, useLayoutEffect } from "react";
|
|
9
|
+
|
|
10
|
+
import type { GetEditor, UseEditorReturn } from "./types";
|
|
11
|
+
import { editorInfoContext } from "./use-get-editor";
|
|
12
|
+
|
|
13
|
+
export function useEditor(getEditor: GetEditor, deps: DependencyList = []): UseEditorReturn {
|
|
14
|
+
const editorInfo = useContext(editorInfoContext);
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
17
|
+
const factory = useCallback(getEditor, deps);
|
|
18
|
+
|
|
19
|
+
useLayoutEffect(() => {
|
|
20
|
+
editorInfo.setEditorFactory(() => factory);
|
|
21
|
+
}, [editorInfo, factory]);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
loading: editorInfo.loading,
|
|
25
|
+
get: () => editorInfo.editor.current,
|
|
26
|
+
};
|
|
27
|
+
}
|