@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,385 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The brand slash-menu Milkdown plugin (the ProseMirror half).
|
|
5
|
+
*
|
|
6
|
+
* A self-owned raw ProseMirror plugin (`$prose`) — NOT `@milkdown/kit/plugin/slash`
|
|
7
|
+
* — so the editor pulls zero new dependencies and we keep full control of the
|
|
8
|
+
* interaction. It owns the STATE and the KEYBOARD; the React widget
|
|
9
|
+
* (slash-widget.tsx) owns the rendered popup. They share one
|
|
10
|
+
* {@link SlashController} (the command list + a Milkdown `Ctx` getter + the
|
|
11
|
+
* insert/close helpers), built once in `MarkdownEditorView` so both the plugin's
|
|
12
|
+
* Enter handler and the widget's click handler run a command the SAME way.
|
|
13
|
+
*
|
|
14
|
+
* Responsibilities here:
|
|
15
|
+
* 1. Detect a `/` typed at a textblock START or after whitespace; track
|
|
16
|
+
* `{ active, from, query, index }` as the user keeps typing (a literal `/`
|
|
17
|
+
* mid-word is never hijacked; a space ends the query).
|
|
18
|
+
* 2. Render the React `<SlashMenu>` as a ProseMirror WIDGET decoration at the
|
|
19
|
+
* caret (via the `useWidgetViewFactory()` factory passed in).
|
|
20
|
+
* 3. ↑/↓ move the selection, Enter inserts the active command (replacing the
|
|
21
|
+
* `/query` range), Esc / deleting the `/` closes.
|
|
22
|
+
*/
|
|
23
|
+
import type { Ctx, MilkdownPlugin } from "@milkdown/kit/ctx";
|
|
24
|
+
import { $prose } from "@milkdown/kit/utils";
|
|
25
|
+
import type { ReactWidgetViewComponent, useWidgetViewFactory } from "@prosemirror-adapter/react";
|
|
26
|
+
import type { Node as ProseNode } from "@milkdown/kit/prose/model";
|
|
27
|
+
import type { Transaction } from "@milkdown/kit/prose/state";
|
|
28
|
+
import { Plugin, PluginKey } from "@milkdown/kit/prose/state";
|
|
29
|
+
import type { EditorView } from "@milkdown/kit/prose/view";
|
|
30
|
+
import { DecorationSet } from "@milkdown/kit/prose/view";
|
|
31
|
+
|
|
32
|
+
import type { IterationEditHandler } from "../../markdown-iteration/edit-context";
|
|
33
|
+
import { filterSlashCommands, type SlashCommand } from "./brand-slash-commands";
|
|
34
|
+
import { matchesKeyboardEvent } from "./shortcut";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The factory `useWidgetViewFactory()` returns: `(options) => (pos, spec?) =>
|
|
38
|
+
* Decoration`. We borrow the exact type from the hook so we never name the
|
|
39
|
+
* (un-re-exported) `@prosemirror-adapter/core` internals.
|
|
40
|
+
*/
|
|
41
|
+
export type SlashWidgetFactory = ReturnType<typeof useWidgetViewFactory>;
|
|
42
|
+
|
|
43
|
+
/** Plugin state. `active === false` means the menu is closed. */
|
|
44
|
+
export interface SlashPluginState {
|
|
45
|
+
active: boolean;
|
|
46
|
+
/** Document position of the trigger `/` (char mode) or the caret (shortcut mode). */
|
|
47
|
+
from: number;
|
|
48
|
+
/** Text typed after the `/` (the filter query). */
|
|
49
|
+
query: string;
|
|
50
|
+
/** Index into the CURRENTLY-FILTERED list of the highlighted command. */
|
|
51
|
+
index: number;
|
|
52
|
+
/**
|
|
53
|
+
* How the menu was opened:
|
|
54
|
+
* - `"char"` — the user typed the trigger character (default); the `from`
|
|
55
|
+
* position holds the `/`, which is deleted when the range is inserted.
|
|
56
|
+
* - `"shortcut"` — opened programmatically via the keyboard shortcut; `from`
|
|
57
|
+
* is the bare caret position (no leading `/`), so `range` must be `null`
|
|
58
|
+
* on insert (insert-at-caret, delete nothing).
|
|
59
|
+
*/
|
|
60
|
+
triggered: "char" | "shortcut";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The closed (inactive) plugin state. Exported for unit tests; not barreled. */
|
|
64
|
+
export const CLOSED: SlashPluginState = {
|
|
65
|
+
active: false,
|
|
66
|
+
from: 0,
|
|
67
|
+
query: "",
|
|
68
|
+
index: 0,
|
|
69
|
+
triggered: "char",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const slashPluginKey = new PluginKey<SlashPluginState>("brand-slash");
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The shared controller. Built once in React so the plugin (keyboard) and the
|
|
76
|
+
* widget (mouse) run commands identically with the live Milkdown `Ctx`. The
|
|
77
|
+
* `ctx` is captured by the plugin's `$prose((ctx) => …)` callback (see
|
|
78
|
+
* {@link brandSlashPlugin}) and read back through `getCtx()`.
|
|
79
|
+
*/
|
|
80
|
+
export interface SlashController {
|
|
81
|
+
/** The command list driving both the menu and the keyboard. */
|
|
82
|
+
commands: SlashCommand[];
|
|
83
|
+
/** The trigger character (default `"/"`). */
|
|
84
|
+
trigger: string;
|
|
85
|
+
/**
|
|
86
|
+
* Optional keyboard shortcut string (e.g. `"Mod-/"`) that opens the menu
|
|
87
|
+
* programmatically at the caret (shortcut mode, no leading `/` in the doc).
|
|
88
|
+
* Parsed by `matchesKeyboardEvent` in `handleKeyDown`.
|
|
89
|
+
*/
|
|
90
|
+
shortcut?: string;
|
|
91
|
+
/** Resolve the live Milkdown `Ctx` (set by the plugin at build time). */
|
|
92
|
+
getCtx: () => Ctx;
|
|
93
|
+
/** Internal: the plugin assigns the captured `Ctx` here. */
|
|
94
|
+
_ctx?: Ctx;
|
|
95
|
+
/**
|
|
96
|
+
* Programmatically open the slash menu at the current caret position.
|
|
97
|
+
* Dispatches the `"open"` meta — equivalent to pressing the shortcut key.
|
|
98
|
+
* No-op if no ProseMirror view is available yet.
|
|
99
|
+
*/
|
|
100
|
+
openMenu?: (view: EditorView) => void;
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the LIVE `IterationEditContext` handler (#223) — read lazily (a
|
|
103
|
+
* ref-backed getter) so a fresh Provider identity each render never needs the
|
|
104
|
+
* controller (and its editor) to be rebuilt. Set by `MarkdownEditorView`;
|
|
105
|
+
* returns `null`/`undefined` when the consumer hasn't wired one, in which case
|
|
106
|
+
* `runSlashCommand` falls back to a command's plain `run`.
|
|
107
|
+
*/
|
|
108
|
+
getIterationEditHandler?: () => IterationEditHandler | null | undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Create a {@link SlashController} with a settable, lazily-read `Ctx`. */
|
|
112
|
+
export function createSlashController(
|
|
113
|
+
commands: SlashCommand[],
|
|
114
|
+
trigger = "/",
|
|
115
|
+
shortcut?: string,
|
|
116
|
+
getIterationEditHandler?: () => IterationEditHandler | null | undefined,
|
|
117
|
+
): SlashController {
|
|
118
|
+
const controller: SlashController = {
|
|
119
|
+
commands,
|
|
120
|
+
trigger,
|
|
121
|
+
shortcut,
|
|
122
|
+
getIterationEditHandler,
|
|
123
|
+
getCtx: () => {
|
|
124
|
+
if (!controller._ctx) {
|
|
125
|
+
throw new Error("Brand slash controller used before the editor Ctx was captured.");
|
|
126
|
+
}
|
|
127
|
+
return controller._ctx;
|
|
128
|
+
},
|
|
129
|
+
openMenu: (view: EditorView) => {
|
|
130
|
+
const pos = view.state.selection.from;
|
|
131
|
+
view.dispatch(
|
|
132
|
+
view.state.tr.setMeta(slashPluginKey, {
|
|
133
|
+
_open: true,
|
|
134
|
+
from: pos,
|
|
135
|
+
triggered: "shortcut",
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
return controller;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Compute the `[from, to)` range covering the trigger + query (to be replaced). */
|
|
144
|
+
export function slashRange(state: SlashPluginState): { from: number; to: number } {
|
|
145
|
+
return { from: state.from, to: state.from + 1 + state.query.length };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Run a command: insert (replacing the `/query` range or at caret), then close the menu. */
|
|
149
|
+
export function runSlashCommand(
|
|
150
|
+
view: EditorView,
|
|
151
|
+
controller: SlashController,
|
|
152
|
+
state: SlashPluginState,
|
|
153
|
+
command: SlashCommand,
|
|
154
|
+
): void {
|
|
155
|
+
// Char mode: replace the whole `/query` run (slashRange covers the `/` + query).
|
|
156
|
+
// Shortcut mode: there is no leading `/`, but any filter the user typed IS real
|
|
157
|
+
// document text [from, from+query.length) — replace that span so the inserted
|
|
158
|
+
// block lands at `from` with the typed query removed (empty query → insert at
|
|
159
|
+
// caret, delete nothing). NOT range:null, which would leave the query behind.
|
|
160
|
+
const range =
|
|
161
|
+
state.triggered === "shortcut"
|
|
162
|
+
? { from: state.from, to: state.from + state.query.length }
|
|
163
|
+
: slashRange(state);
|
|
164
|
+
// #223: a `guided` command (currently `/iterate` `/pivot`) opens its modal
|
|
165
|
+
// instead of inserting a bare directive — but ONLY when the consumer has
|
|
166
|
+
// actually wired an `IterationEditContext` handler; with none, fall back to
|
|
167
|
+
// the plain `run` (today's direct-insert behaviour, unchanged).
|
|
168
|
+
const iterationHandler = controller.getIterationEditHandler?.();
|
|
169
|
+
if (command.guided && iterationHandler) {
|
|
170
|
+
// The guided modal doesn't dispatch an insert transaction until it SAVES
|
|
171
|
+
// (Cancel must leave the document untouched) — so the typed `/query` range
|
|
172
|
+
// has to be consumed NOW, or it is left behind as literal text forever
|
|
173
|
+
// when the user cancels. Delete it up front (collapsing the selection to
|
|
174
|
+
// `range.from`) and hand the resolver `range: null` so its eventual save
|
|
175
|
+
// inserts at the now-current caret rather than a stale, already-consumed span.
|
|
176
|
+
view.dispatch(view.state.tr.delete(range.from, range.to));
|
|
177
|
+
command.guided({ ctx: controller.getCtx(), range: null }, iterationHandler);
|
|
178
|
+
} else {
|
|
179
|
+
// The command's `run` dispatches its own insert transaction via the view;
|
|
180
|
+
// then close the (now stale) menu. If the command already removed the
|
|
181
|
+
// range, the close meta is harmless.
|
|
182
|
+
command.run({ ctx: controller.getCtx(), range });
|
|
183
|
+
}
|
|
184
|
+
if (slashPluginKey.getState(view.state)?.active) {
|
|
185
|
+
view.dispatch(view.state.tr.setMeta(slashPluginKey, "close"));
|
|
186
|
+
}
|
|
187
|
+
view.focus();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** A `/` is a valid trigger only at a textblock start or right after whitespace. */
|
|
191
|
+
function triggerAllowed(doc: ProseNode, triggerPos: number): boolean {
|
|
192
|
+
const $pos = doc.resolve(triggerPos);
|
|
193
|
+
if (!$pos.parent.isTextblock) return false;
|
|
194
|
+
if ($pos.parent.type.spec.code) return false;
|
|
195
|
+
if ($pos.parentOffset === 0) return true; // start of block
|
|
196
|
+
const before = $pos.parent.textBetween(Math.max(0, $pos.parentOffset - 1), $pos.parentOffset);
|
|
197
|
+
return /\s/.test(before);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Recompute open/query/index from a transaction. Opens when the trigger is typed
|
|
202
|
+
* in an allowed spot; tracks the query; closes when the `/` is removed, the caret
|
|
203
|
+
* leaves the trigger run/block, the selection is non-empty, or a space is typed.
|
|
204
|
+
*
|
|
205
|
+
* Also handles the `_open` meta (programmatic open via shortcut / `openMenu()`):
|
|
206
|
+
* sets `triggered: "shortcut"` so `runSlashCommand` uses `range: null`.
|
|
207
|
+
*/
|
|
208
|
+
export function nextState(
|
|
209
|
+
prev: SlashPluginState,
|
|
210
|
+
tr: Transaction,
|
|
211
|
+
trigger: string,
|
|
212
|
+
): SlashPluginState {
|
|
213
|
+
const meta = tr.getMeta(slashPluginKey) as
|
|
214
|
+
| (Partial<SlashPluginState> & { _open?: boolean })
|
|
215
|
+
| "close"
|
|
216
|
+
| undefined;
|
|
217
|
+
|
|
218
|
+
if (meta === "close") return CLOSED;
|
|
219
|
+
|
|
220
|
+
if (meta && typeof meta === "object") {
|
|
221
|
+
// Programmatic open (shortcut / openMenu): open unconditionally at caret.
|
|
222
|
+
if (meta._open) {
|
|
223
|
+
return {
|
|
224
|
+
active: true,
|
|
225
|
+
from: (meta as { from: number }).from,
|
|
226
|
+
query: "",
|
|
227
|
+
index: 0,
|
|
228
|
+
triggered: "shortcut",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
// Nav meta (index change) only applies while still active.
|
|
232
|
+
return prev.active ? { ...prev, ...meta } : prev;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const sel = tr.selection;
|
|
236
|
+
if (!sel.empty) return prev.active ? CLOSED : prev;
|
|
237
|
+
|
|
238
|
+
const pos = sel.from;
|
|
239
|
+
const $pos = tr.doc.resolve(pos);
|
|
240
|
+
|
|
241
|
+
if (prev.active) {
|
|
242
|
+
if (pos <= prev.from) return CLOSED;
|
|
243
|
+
const start = tr.doc.resolve(prev.from);
|
|
244
|
+
if (start.parent !== $pos.parent) return CLOSED;
|
|
245
|
+
|
|
246
|
+
if (prev.triggered === "shortcut") {
|
|
247
|
+
// In shortcut mode there is no leading trigger char — read the query as
|
|
248
|
+
// text typed AFTER the caret-open position. Skip the triggerChar check.
|
|
249
|
+
const query = $pos.parent.textBetween(start.parentOffset, $pos.parentOffset);
|
|
250
|
+
if (/\s/.test(query)) return CLOSED;
|
|
251
|
+
const index = query === prev.query ? prev.index : 0;
|
|
252
|
+
return { active: true, from: prev.from, query, index, triggered: "shortcut" };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Char mode: the first character at `from` must still be the trigger.
|
|
256
|
+
const triggerChar = start.parent.textBetween(start.parentOffset, start.parentOffset + 1);
|
|
257
|
+
if (triggerChar !== trigger) return CLOSED;
|
|
258
|
+
const query = $pos.parent.textBetween(start.parentOffset + 1, $pos.parentOffset);
|
|
259
|
+
if (/\s/.test(query)) return CLOSED;
|
|
260
|
+
const index = query === prev.query ? prev.index : 0;
|
|
261
|
+
return { active: true, from: prev.from, query, index, triggered: "char" };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!tr.docChanged) return prev;
|
|
265
|
+
const justTyped = tr.doc.textBetween(Math.max(0, pos - 1), pos);
|
|
266
|
+
if (justTyped !== trigger) return prev;
|
|
267
|
+
const triggerPos = pos - 1;
|
|
268
|
+
if (!triggerAllowed(tr.doc, triggerPos)) return prev;
|
|
269
|
+
return { active: true, from: triggerPos, query: "", index: 0, triggered: "char" };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Options for {@link brandSlashPlugin}. */
|
|
273
|
+
export interface BrandSlashPluginOptions {
|
|
274
|
+
/** The widget factory from `useWidgetViewFactory()` (created under the adapter provider). */
|
|
275
|
+
widgetFactory: SlashWidgetFactory;
|
|
276
|
+
/** The React component rendered as the widget (built with the controller). */
|
|
277
|
+
widgetComponent: ReactWidgetViewComponent;
|
|
278
|
+
/** The shared controller (commands + ctx getter + trigger). */
|
|
279
|
+
controller: SlashController;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Build the slash plugin. The widget factory + component + controller are created
|
|
284
|
+
* in React (so the factory lives under `<ProsemirrorAdapterProvider>` and the
|
|
285
|
+
* component closes over the controller). Returns a Milkdown plugin to `.use()`.
|
|
286
|
+
*/
|
|
287
|
+
export function brandSlashPlugin(options: BrandSlashPluginOptions): MilkdownPlugin {
|
|
288
|
+
const { widgetFactory, widgetComponent, controller } = options;
|
|
289
|
+
const trigger = controller.trigger || "/";
|
|
290
|
+
|
|
291
|
+
return $prose((ctx) => {
|
|
292
|
+
// Capture the live editor Ctx so command `run`s (keyboard + mouse) can
|
|
293
|
+
// resolve NodeTypes and the view.
|
|
294
|
+
controller._ctx = ctx;
|
|
295
|
+
return new Plugin<SlashPluginState>({
|
|
296
|
+
key: slashPluginKey,
|
|
297
|
+
state: {
|
|
298
|
+
init: () => CLOSED,
|
|
299
|
+
apply: (tr, value) => nextState(value, tr, trigger),
|
|
300
|
+
},
|
|
301
|
+
props: {
|
|
302
|
+
decorations: (state) => {
|
|
303
|
+
const s = slashPluginKey.getState(state);
|
|
304
|
+
if (!s?.active) return DecorationSet.empty;
|
|
305
|
+
const factory = widgetFactory({ component: widgetComponent, as: "span" });
|
|
306
|
+
// Anchor: in char mode `from + 1` skips the `/`; in shortcut mode
|
|
307
|
+
// anchor at `from` (the bare caret — no `/` to step over).
|
|
308
|
+
const anchor = s.triggered === "shortcut" ? s.from : s.from + 1;
|
|
309
|
+
// `spec` goes on the factory CALL (the Decoration.widget spec), not the
|
|
310
|
+
// factory options. After the caret; don't let the widget disturb the
|
|
311
|
+
// editor selection. Key on from+query+index so ProseMirror re-renders the
|
|
312
|
+
// widget when the filter OR the highlighted row changes (decoration
|
|
313
|
+
// diffing reuses a widget with an unchanged key, which would freeze the
|
|
314
|
+
// ↑/↓ highlight).
|
|
315
|
+
const decoration = factory(anchor, {
|
|
316
|
+
side: 1,
|
|
317
|
+
ignoreSelection: true,
|
|
318
|
+
key: `brand-slash:${s.from}:${s.query}:${s.index}`,
|
|
319
|
+
});
|
|
320
|
+
return DecorationSet.create(state.doc, [decoration]);
|
|
321
|
+
},
|
|
322
|
+
handleKeyDown: (view, event) => {
|
|
323
|
+
// --- Shortcut open (BEFORE the active guard) ---
|
|
324
|
+
// If a shortcut is configured and this event matches it, open the menu
|
|
325
|
+
// at the current caret position in shortcut mode. This runs before keymaps
|
|
326
|
+
// so it beats commonmark/gfm bindings (the exit-keymap.ts precedent).
|
|
327
|
+
if (controller.shortcut && matchesKeyboardEvent(controller.shortcut, event)) {
|
|
328
|
+
const pos = view.state.selection.from;
|
|
329
|
+
view.dispatch(
|
|
330
|
+
view.state.tr.setMeta(slashPluginKey, {
|
|
331
|
+
_open: true,
|
|
332
|
+
from: pos,
|
|
333
|
+
triggered: "shortcut",
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const s = slashPluginKey.getState(view.state);
|
|
341
|
+
if (!s?.active) return false;
|
|
342
|
+
const filtered = filterSlashCommands(controller.commands, s.query);
|
|
343
|
+
|
|
344
|
+
if (event.key === "Escape") {
|
|
345
|
+
view.dispatch(view.state.tr.setMeta(slashPluginKey, "close"));
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
if (filtered.length === 0) return false;
|
|
350
|
+
|
|
351
|
+
if (event.key === "ArrowDown") {
|
|
352
|
+
const index = (s.index + 1) % filtered.length;
|
|
353
|
+
view.dispatch(view.state.tr.setMeta(slashPluginKey, { index }));
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
if (event.key === "ArrowUp") {
|
|
358
|
+
const index = (s.index - 1 + filtered.length) % filtered.length;
|
|
359
|
+
view.dispatch(view.state.tr.setMeta(slashPluginKey, { index }));
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
if (event.key === "Enter") {
|
|
364
|
+
const command = filtered[Math.min(s.index, filtered.length - 1)];
|
|
365
|
+
if (command) {
|
|
366
|
+
runSlashCommand(view, controller, s, command);
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (event.key === "Tab") {
|
|
372
|
+
// Tab also selects the active command (common in Notion-style menus).
|
|
373
|
+
const command = filtered[Math.min(s.index, filtered.length - 1)];
|
|
374
|
+
if (command) {
|
|
375
|
+
runSlashCommand(view, controller, s, command);
|
|
376
|
+
event.preventDefault();
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return false;
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
}) as unknown as MilkdownPlugin;
|
|
385
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Brand slash-menu — public surface for the WYSIWYG markdown editor.
|
|
5
|
+
*
|
|
6
|
+
* Typing `/` at a block start (or after whitespace) opens a branded command menu
|
|
7
|
+
* that inserts the brand `:::` directives + basic blocks live in the editor. The
|
|
8
|
+
* load-bearing pieces:
|
|
9
|
+
* - `brand-slash-commands` — the typed, tree-shakeable command registry.
|
|
10
|
+
* - `insert-directive` — the pure ProseMirror insertion commands (unit-tested).
|
|
11
|
+
* - `brand-slash-plugin` — the `$prose` plugin (state + keyboard).
|
|
12
|
+
* - `slash-widget` / `slash-menu` — the React widget + the branded popup.
|
|
13
|
+
*
|
|
14
|
+
* `brandSlashViewPlugins(widgetFactory, options)` wires it all together — call it
|
|
15
|
+
* with the factory from `useWidgetViewFactory()` (so it runs under
|
|
16
|
+
* `<ProsemirrorAdapterProvider>`), exactly like `directiveViewPlugins`.
|
|
17
|
+
*/
|
|
18
|
+
import type { MilkdownPlugin } from "@milkdown/kit/ctx";
|
|
19
|
+
|
|
20
|
+
import type { IterationEditHandler } from "../../markdown-iteration/edit-context";
|
|
21
|
+
import { BRAND_SLASH_COMMANDS, type SlashCommand } from "./brand-slash-commands";
|
|
22
|
+
import {
|
|
23
|
+
brandSlashPlugin,
|
|
24
|
+
createSlashController,
|
|
25
|
+
type SlashWidgetFactory,
|
|
26
|
+
} from "./brand-slash-plugin";
|
|
27
|
+
import { DEFAULT_SLASH_SHORTCUT } from "./shortcut";
|
|
28
|
+
import { createSlashWidget } from "./slash-widget";
|
|
29
|
+
|
|
30
|
+
/** Options for {@link brandSlashViewPlugins}. */
|
|
31
|
+
export interface BrandSlashViewOptions {
|
|
32
|
+
/** Override the default command list. */
|
|
33
|
+
commands?: SlashCommand[];
|
|
34
|
+
/** The trigger character. Defaults to `"/"`. */
|
|
35
|
+
trigger?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Keyboard shortcut that opens the menu at the caret in BOTH panes (#271).
|
|
38
|
+
* Defaults to `DEFAULT_SLASH_SHORTCUT` (`"Mod-Shift-O"`). Set to `undefined` or
|
|
39
|
+
* an empty string to disable the shortcut binding.
|
|
40
|
+
*/
|
|
41
|
+
shortcut?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the live `IterationEditContext` handler (#223, internal —
|
|
44
|
+
* `MarkdownEditorView` wires this from `useContext`). When present, a
|
|
45
|
+
* `guided` command (`/iterate` `/pivot`) opens its modal instead of a bare
|
|
46
|
+
* insert. Not part of the public `slashMenu` prop surface.
|
|
47
|
+
*/
|
|
48
|
+
getIterationEditHandler?: () => IterationEditHandler | null | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build the slash-menu Milkdown plugins. Call with the widget factory from
|
|
53
|
+
* `useWidgetViewFactory()` (must run inside a `<ProsemirrorAdapterProvider>`),
|
|
54
|
+
* then `.use()` the result on the editor.
|
|
55
|
+
*/
|
|
56
|
+
export function brandSlashViewPlugins(
|
|
57
|
+
widgetFactory: SlashWidgetFactory,
|
|
58
|
+
options: BrandSlashViewOptions = {},
|
|
59
|
+
): MilkdownPlugin[] {
|
|
60
|
+
const commands = options.commands ?? BRAND_SLASH_COMMANDS;
|
|
61
|
+
const shortcut = "shortcut" in options ? options.shortcut : DEFAULT_SLASH_SHORTCUT;
|
|
62
|
+
const controller = createSlashController(
|
|
63
|
+
commands,
|
|
64
|
+
options.trigger ?? "/",
|
|
65
|
+
shortcut,
|
|
66
|
+
options.getIterationEditHandler,
|
|
67
|
+
);
|
|
68
|
+
const widgetComponent = createSlashWidget(controller);
|
|
69
|
+
return [brandSlashPlugin({ widgetFactory, widgetComponent, controller })];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
BRAND_SLASH_COMMANDS,
|
|
74
|
+
filterSlashCommands,
|
|
75
|
+
groupSlashCommands,
|
|
76
|
+
type SlashCommand,
|
|
77
|
+
type SlashInsertContext,
|
|
78
|
+
type SourceSlashContext,
|
|
79
|
+
type BasicBlockId,
|
|
80
|
+
} from "./brand-slash-commands";
|
|
81
|
+
export {
|
|
82
|
+
insertBrandDirective,
|
|
83
|
+
insertBasicBlock,
|
|
84
|
+
insertCalcFence,
|
|
85
|
+
resolveBrandInsert,
|
|
86
|
+
resolveBasicInsert,
|
|
87
|
+
resolveCalcInsert,
|
|
88
|
+
isContainerDirective,
|
|
89
|
+
isLeafDirective,
|
|
90
|
+
CALC_FENCE_SEED,
|
|
91
|
+
type InsertableDirective,
|
|
92
|
+
type SlashRange,
|
|
93
|
+
} from "./insert-directive";
|
|
94
|
+
export { SlashMenu, slashOptionId, type SlashMenuProps } from "./slash-menu";
|
|
95
|
+
export {
|
|
96
|
+
slashPluginKey,
|
|
97
|
+
slashRange,
|
|
98
|
+
createSlashController,
|
|
99
|
+
type SlashController,
|
|
100
|
+
type SlashPluginState,
|
|
101
|
+
type SlashWidgetFactory,
|
|
102
|
+
} from "./brand-slash-plugin";
|
|
103
|
+
// NOTE: `MonacoSlashMenu` is deliberately NOT re-exported here. It pulls the
|
|
104
|
+
// Monaco runtime (via `markdown-commands`), and this barrel is imported by the
|
|
105
|
+
// Milkdown `MarkdownEditor` graph, which must stay Monaco-free. It is exported
|
|
106
|
+
// from the heavy `@elabs-ai/components-editor/markdown` subpath instead (which already pulls
|
|
107
|
+
// Monaco via `MarkdownWorkspace`).
|