@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,3748 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/editor-context-menu/editor-context-menu.tsx
|
|
4
|
+
import {
|
|
5
|
+
ContextMenu,
|
|
6
|
+
ContextMenuContent,
|
|
7
|
+
ContextMenuItem,
|
|
8
|
+
ContextMenuSeparator,
|
|
9
|
+
ContextMenuShortcut,
|
|
10
|
+
ContextMenuTrigger
|
|
11
|
+
} from "@elabs-ai/components-ui";
|
|
12
|
+
import "react";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
function EditorContextMenu({ editor: editor2, readOnly = false, children }) {
|
|
15
|
+
const selection = () => {
|
|
16
|
+
if (!editor2) return null;
|
|
17
|
+
const model = editor2.getModel();
|
|
18
|
+
const sel = editor2.getSelection();
|
|
19
|
+
return model && sel ? { editor: editor2, model, sel } : null;
|
|
20
|
+
};
|
|
21
|
+
const copy = async () => {
|
|
22
|
+
const ctx2 = selection();
|
|
23
|
+
if (!ctx2) return;
|
|
24
|
+
const text = ctx2.model.getValueInRange(ctx2.sel);
|
|
25
|
+
if (text && typeof navigator !== "undefined" && navigator.clipboard) {
|
|
26
|
+
await navigator.clipboard.writeText(text);
|
|
27
|
+
}
|
|
28
|
+
ctx2.editor.focus();
|
|
29
|
+
};
|
|
30
|
+
const cut = async () => {
|
|
31
|
+
const ctx2 = selection();
|
|
32
|
+
if (!ctx2) return;
|
|
33
|
+
const text = ctx2.model.getValueInRange(ctx2.sel);
|
|
34
|
+
if (text && typeof navigator !== "undefined" && navigator.clipboard) {
|
|
35
|
+
await navigator.clipboard.writeText(text);
|
|
36
|
+
ctx2.editor.executeEdits("brand-cut", [{ range: ctx2.sel, text: "", forceMoveMarkers: true }]);
|
|
37
|
+
}
|
|
38
|
+
ctx2.editor.focus();
|
|
39
|
+
};
|
|
40
|
+
const paste = async () => {
|
|
41
|
+
const ctx2 = selection();
|
|
42
|
+
if (!ctx2 || typeof navigator === "undefined" || !navigator.clipboard) return;
|
|
43
|
+
const text = await navigator.clipboard.readText();
|
|
44
|
+
ctx2.editor.executeEdits("brand-paste", [{ range: ctx2.sel, text, forceMoveMarkers: true }]);
|
|
45
|
+
ctx2.editor.focus();
|
|
46
|
+
};
|
|
47
|
+
const selectAll = () => {
|
|
48
|
+
const ctx2 = selection();
|
|
49
|
+
if (!ctx2) return;
|
|
50
|
+
ctx2.editor.setSelection(ctx2.model.getFullModelRange());
|
|
51
|
+
ctx2.editor.focus();
|
|
52
|
+
};
|
|
53
|
+
const runAction = (id) => {
|
|
54
|
+
editor2?.focus();
|
|
55
|
+
void editor2?.getAction(id)?.run();
|
|
56
|
+
};
|
|
57
|
+
return /* @__PURE__ */ jsxs(ContextMenu, { children: [
|
|
58
|
+
/* @__PURE__ */ jsx(ContextMenuTrigger, { asChild: true, children }),
|
|
59
|
+
/* @__PURE__ */ jsxs(ContextMenuContent, { className: "w-52", children: [
|
|
60
|
+
!readOnly ? /* @__PURE__ */ jsxs(ContextMenuItem, { onSelect: () => void cut(), children: [
|
|
61
|
+
"Cut",
|
|
62
|
+
/* @__PURE__ */ jsx(ContextMenuShortcut, { children: "\u2318X" })
|
|
63
|
+
] }) : null,
|
|
64
|
+
/* @__PURE__ */ jsxs(ContextMenuItem, { onSelect: () => void copy(), children: [
|
|
65
|
+
"Copy",
|
|
66
|
+
/* @__PURE__ */ jsx(ContextMenuShortcut, { children: "\u2318C" })
|
|
67
|
+
] }),
|
|
68
|
+
!readOnly ? /* @__PURE__ */ jsxs(ContextMenuItem, { onSelect: () => void paste(), children: [
|
|
69
|
+
"Paste",
|
|
70
|
+
/* @__PURE__ */ jsx(ContextMenuShortcut, { children: "\u2318V" })
|
|
71
|
+
] }) : null,
|
|
72
|
+
/* @__PURE__ */ jsx(ContextMenuSeparator, {}),
|
|
73
|
+
/* @__PURE__ */ jsxs(ContextMenuItem, { onSelect: selectAll, children: [
|
|
74
|
+
"Select all",
|
|
75
|
+
/* @__PURE__ */ jsx(ContextMenuShortcut, { children: "\u2318A" })
|
|
76
|
+
] }),
|
|
77
|
+
!readOnly ? /* @__PURE__ */ jsxs(ContextMenuItem, { onSelect: () => runAction("editor.action.formatDocument"), children: [
|
|
78
|
+
"Format document",
|
|
79
|
+
/* @__PURE__ */ jsx(ContextMenuShortcut, { children: "\u21E7\u2325F" })
|
|
80
|
+
] }) : null,
|
|
81
|
+
/* @__PURE__ */ jsx(ContextMenuSeparator, {}),
|
|
82
|
+
/* @__PURE__ */ jsxs(ContextMenuItem, { onSelect: () => runAction("editor.action.quickCommand"), children: [
|
|
83
|
+
"Command palette",
|
|
84
|
+
/* @__PURE__ */ jsx(ContextMenuShortcut, { children: "F1" })
|
|
85
|
+
] })
|
|
86
|
+
] })
|
|
87
|
+
] });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/lib/monaco-theme-bridge.ts
|
|
91
|
+
import { oklchToHex, resolveThemeIsDark } from "@elabs-ai/components-tokens";
|
|
92
|
+
var hexCache = /* @__PURE__ */ new Map();
|
|
93
|
+
var ctx = null;
|
|
94
|
+
function resolveCssColor(value, fallback = "#000000") {
|
|
95
|
+
const raw = value.trim();
|
|
96
|
+
if (!raw) return fallback;
|
|
97
|
+
if (/^#([0-9a-f]{3,8})$/i.test(raw)) return raw;
|
|
98
|
+
const cached = hexCache.get(raw);
|
|
99
|
+
if (cached) return cached;
|
|
100
|
+
const viaOklch = oklchToHex(raw);
|
|
101
|
+
if (viaOklch) {
|
|
102
|
+
hexCache.set(raw, viaOklch);
|
|
103
|
+
return viaOklch;
|
|
104
|
+
}
|
|
105
|
+
if (typeof document === "undefined") return fallback;
|
|
106
|
+
if (!ctx) {
|
|
107
|
+
const canvas = document.createElement("canvas");
|
|
108
|
+
canvas.width = 1;
|
|
109
|
+
canvas.height = 1;
|
|
110
|
+
ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
111
|
+
}
|
|
112
|
+
if (!ctx) return fallback;
|
|
113
|
+
ctx.fillStyle = "#000000";
|
|
114
|
+
ctx.fillStyle = fallback;
|
|
115
|
+
ctx.fillStyle = raw;
|
|
116
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
117
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
118
|
+
const data = ctx.getImageData(0, 0, 1, 1).data;
|
|
119
|
+
const r = data[0] ?? 0;
|
|
120
|
+
const g = data[1] ?? 0;
|
|
121
|
+
const b = data[2] ?? 0;
|
|
122
|
+
const a = data[3] ?? 255;
|
|
123
|
+
const hex = a < 255 ? `#${byte(r)}${byte(g)}${byte(b)}${byte(a)}` : `#${byte(r)}${byte(g)}${byte(b)}`;
|
|
124
|
+
hexCache.set(raw, hex);
|
|
125
|
+
return hex;
|
|
126
|
+
}
|
|
127
|
+
var clamp01 = (n) => Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 1;
|
|
128
|
+
var byte = (n) => n.toString(16).padStart(2, "0");
|
|
129
|
+
function withAlpha(hex, alpha) {
|
|
130
|
+
const base = hex.slice(0, 7);
|
|
131
|
+
return `${base}${byte(Math.round(clamp01(alpha) * 255))}`;
|
|
132
|
+
}
|
|
133
|
+
function bare(hex) {
|
|
134
|
+
return hex.replace("#", "").slice(0, 6).padEnd(6, "0");
|
|
135
|
+
}
|
|
136
|
+
var channel = (hex, i) => parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) || 0;
|
|
137
|
+
var toLinear = (c) => {
|
|
138
|
+
const s = c / 255;
|
|
139
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
140
|
+
};
|
|
141
|
+
function luminance(hex) {
|
|
142
|
+
return 0.2126 * toLinear(channel(hex, 0)) + 0.7152 * toLinear(channel(hex, 1)) + 0.0722 * toLinear(channel(hex, 2));
|
|
143
|
+
}
|
|
144
|
+
function contrast(a, b) {
|
|
145
|
+
const la = luminance(a);
|
|
146
|
+
const lb = luminance(b);
|
|
147
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
148
|
+
}
|
|
149
|
+
function mixHex(hex, target, t) {
|
|
150
|
+
const lerp = (i) => Math.round(channel(hex, i) + (channel(target, i) - channel(hex, i)) * t);
|
|
151
|
+
return `#${byte(lerp(0))}${byte(lerp(1))}${byte(lerp(2))}`;
|
|
152
|
+
}
|
|
153
|
+
function ensureReadable(hex, bg, minRatio) {
|
|
154
|
+
const base = hex.slice(0, 7);
|
|
155
|
+
if (contrast(base, bg) >= minRatio) return base;
|
|
156
|
+
const target = luminance(bg) > 0.5 ? "#000000" : "#ffffff";
|
|
157
|
+
let out = base;
|
|
158
|
+
for (let t = 0.1; t <= 1.0001; t += 0.1) {
|
|
159
|
+
out = mixHex(base, target, t);
|
|
160
|
+
if (contrast(out, bg) >= minRatio) break;
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
function builtinBase(rootEl) {
|
|
165
|
+
return resolveThemeIsDark(rootEl) ? "vs-dark" : "vs";
|
|
166
|
+
}
|
|
167
|
+
function buildBrandThemeData(rootEl) {
|
|
168
|
+
const el = rootEl ?? (typeof document !== "undefined" ? document.documentElement : null);
|
|
169
|
+
const read = (name, fallback) => resolveCssColor(el ? getComputedStyle(el).getPropertyValue(name) : "", fallback);
|
|
170
|
+
const background = read("--background", "#ffffff");
|
|
171
|
+
const foreground = read("--foreground", "#000000");
|
|
172
|
+
const muted = read("--muted", background);
|
|
173
|
+
const mutedFg = read("--muted-foreground", foreground);
|
|
174
|
+
const border = read("--border", muted);
|
|
175
|
+
const primary = read("--primary", foreground);
|
|
176
|
+
const ring = read("--ring", primary);
|
|
177
|
+
const popover = read("--popover", background);
|
|
178
|
+
const popoverFg = read("--popover-foreground", foreground);
|
|
179
|
+
const input = read("--input", border);
|
|
180
|
+
const chart1 = read("--chart-1", primary);
|
|
181
|
+
const chart2 = read("--chart-2", primary);
|
|
182
|
+
const chart3 = read("--chart-3", primary);
|
|
183
|
+
const chart4 = read("--chart-4", primary);
|
|
184
|
+
const success = read("--success", chart2);
|
|
185
|
+
const destructive = read("--destructive", "#ff0000");
|
|
186
|
+
const calcResult = ensureReadable(read("--calc-result", primary), background, 4.5);
|
|
187
|
+
const colors = {
|
|
188
|
+
"editor.background": background,
|
|
189
|
+
"editor.foreground": foreground,
|
|
190
|
+
"editorGutter.background": background,
|
|
191
|
+
"editorLineNumber.foreground": withAlpha(mutedFg, 0.6),
|
|
192
|
+
"editorLineNumber.activeForeground": foreground,
|
|
193
|
+
"editorCursor.foreground": primary,
|
|
194
|
+
"editor.selectionBackground": withAlpha(primary, 0.28),
|
|
195
|
+
"editor.inactiveSelectionBackground": withAlpha(primary, 0.14),
|
|
196
|
+
"editor.selectionHighlightBackground": withAlpha(primary, 0.14),
|
|
197
|
+
"editor.lineHighlightBackground": withAlpha(foreground, 0.05),
|
|
198
|
+
"editor.lineHighlightBorder": "#00000000",
|
|
199
|
+
"editorIndentGuide.background1": withAlpha(border, 0.6),
|
|
200
|
+
"editorIndentGuide.activeBackground1": mutedFg,
|
|
201
|
+
"editorWhitespace.foreground": withAlpha(mutedFg, 0.35),
|
|
202
|
+
"editorBracketMatch.background": withAlpha(primary, 0.2),
|
|
203
|
+
"editorBracketMatch.border": withAlpha(primary, 0.45),
|
|
204
|
+
// Widgets — this is what makes Monaco's "built-in components" match brand-ui.
|
|
205
|
+
// A shadow lets the popover detach from the editor even on light themes where
|
|
206
|
+
// the popover surface and editor background are nearly identical.
|
|
207
|
+
"widget.shadow": "#0000002e",
|
|
208
|
+
"editorWidget.background": popover,
|
|
209
|
+
"editorWidget.foreground": popoverFg,
|
|
210
|
+
"editorWidget.border": border,
|
|
211
|
+
"editorHoverWidget.background": popover,
|
|
212
|
+
"editorHoverWidget.foreground": popoverFg,
|
|
213
|
+
"editorHoverWidget.border": border,
|
|
214
|
+
"editorSuggestWidget.background": popover,
|
|
215
|
+
"editorSuggestWidget.foreground": popoverFg,
|
|
216
|
+
"editorSuggestWidget.border": border,
|
|
217
|
+
"editorSuggestWidget.selectedBackground": withAlpha(primary, 0.18),
|
|
218
|
+
"editorSuggestWidget.selectedForeground": popoverFg,
|
|
219
|
+
"editorSuggestWidget.highlightForeground": primary,
|
|
220
|
+
"input.background": input,
|
|
221
|
+
"input.foreground": foreground,
|
|
222
|
+
"input.border": border,
|
|
223
|
+
focusBorder: ring,
|
|
224
|
+
"dropdown.background": popover,
|
|
225
|
+
"dropdown.foreground": popoverFg,
|
|
226
|
+
"dropdown.border": border,
|
|
227
|
+
"list.hoverBackground": withAlpha(mutedFg, 0.12),
|
|
228
|
+
"list.focusBackground": withAlpha(primary, 0.16),
|
|
229
|
+
// Context menu (Monaco's built-in, used when contextMenu="monaco").
|
|
230
|
+
"menu.background": popover,
|
|
231
|
+
"menu.foreground": popoverFg,
|
|
232
|
+
"menu.border": border,
|
|
233
|
+
"menu.selectionBackground": withAlpha(primary, 0.18),
|
|
234
|
+
"menu.selectionForeground": popoverFg,
|
|
235
|
+
"menu.separatorBackground": withAlpha(border, 0.8),
|
|
236
|
+
"scrollbarSlider.background": withAlpha(mutedFg, 0.2),
|
|
237
|
+
"scrollbarSlider.hoverBackground": withAlpha(mutedFg, 0.35),
|
|
238
|
+
"scrollbarSlider.activeBackground": withAlpha(mutedFg, 0.5),
|
|
239
|
+
// Minimap (off by default; themed for when it's enabled via `options`).
|
|
240
|
+
"minimap.background": background,
|
|
241
|
+
"minimapSlider.background": withAlpha(mutedFg, 0.18),
|
|
242
|
+
"minimapSlider.hoverBackground": withAlpha(mutedFg, 0.3),
|
|
243
|
+
"minimapSlider.activeBackground": withAlpha(mutedFg, 0.45),
|
|
244
|
+
"editorError.foreground": destructive,
|
|
245
|
+
"editorWarning.foreground": read("--warning", chart4),
|
|
246
|
+
// Inlay hints (#220 calc result inlays) — calm, legible, themed from the calc
|
|
247
|
+
// result token; transparent plate so it reads as an annotation, not a chip.
|
|
248
|
+
"editorInlayHint.foreground": calcResult,
|
|
249
|
+
"editorInlayHint.background": "#00000000",
|
|
250
|
+
"editorInlayHint.typeForeground": calcResult,
|
|
251
|
+
"editorInlayHint.parameterForeground": calcResult,
|
|
252
|
+
// Diff editor — brand the add/remove bands from success/destructive tokens
|
|
253
|
+
// (instead of Monaco's default green/red) at low alpha so syntax reads on top.
|
|
254
|
+
"diffEditor.insertedTextBackground": withAlpha(success, 0.16),
|
|
255
|
+
"diffEditor.removedTextBackground": withAlpha(destructive, 0.16),
|
|
256
|
+
"diffEditor.insertedLineBackground": withAlpha(success, 0.08),
|
|
257
|
+
"diffEditor.removedLineBackground": withAlpha(destructive, 0.08),
|
|
258
|
+
"diffEditorGutter.insertedLineBackground": withAlpha(success, 0.12),
|
|
259
|
+
"diffEditorGutter.removedLineBackground": withAlpha(destructive, 0.12),
|
|
260
|
+
"diffEditorOverview.insertedForeground": withAlpha(success, 0.6),
|
|
261
|
+
"diffEditorOverview.removedForeground": withAlpha(destructive, 0.6),
|
|
262
|
+
"diffEditor.border": border
|
|
263
|
+
};
|
|
264
|
+
const ink = (hex, ratio = 4.5) => bare(ensureReadable(hex, background, ratio));
|
|
265
|
+
const rules = [
|
|
266
|
+
{ token: "", foreground: bare(foreground), background: bare(background) },
|
|
267
|
+
{ token: "comment", foreground: ink(mutedFg, 3.2), fontStyle: "italic" },
|
|
268
|
+
{ token: "keyword", foreground: ink(primary) },
|
|
269
|
+
{ token: "operator", foreground: ink(primary) },
|
|
270
|
+
{ token: "string", foreground: ink(chart2) },
|
|
271
|
+
{ token: "number", foreground: ink(chart4) },
|
|
272
|
+
{ token: "regexp", foreground: ink(chart4) },
|
|
273
|
+
{ token: "constant", foreground: ink(chart4) },
|
|
274
|
+
{ token: "type", foreground: ink(chart1) },
|
|
275
|
+
{ token: "type.identifier", foreground: ink(chart1) },
|
|
276
|
+
{ token: "function", foreground: ink(chart3) },
|
|
277
|
+
{ token: "identifier", foreground: bare(foreground) },
|
|
278
|
+
{ token: "variable", foreground: bare(foreground) },
|
|
279
|
+
{ token: "variable.predefined", foreground: ink(chart3) },
|
|
280
|
+
{ token: "delimiter", foreground: ink(mutedFg, 3.2) },
|
|
281
|
+
{ token: "tag", foreground: ink(primary) },
|
|
282
|
+
{ token: "attribute.name", foreground: ink(chart3) },
|
|
283
|
+
{ token: "attribute.value", foreground: ink(chart2) },
|
|
284
|
+
{ token: "key", foreground: ink(chart1) },
|
|
285
|
+
// JSON keys
|
|
286
|
+
{ token: "string.key", foreground: ink(chart1) },
|
|
287
|
+
{ token: "string.value", foreground: ink(chart2) },
|
|
288
|
+
{ token: "invalid", foreground: ink(destructive) },
|
|
289
|
+
{ token: "namespace", foreground: ink(success) }
|
|
290
|
+
];
|
|
291
|
+
return { base: "vs", inherit: true, colors, rules };
|
|
292
|
+
}
|
|
293
|
+
function brandThemeId(theme) {
|
|
294
|
+
return `brand-${theme}`;
|
|
295
|
+
}
|
|
296
|
+
function applyBrandTheme(monaco2, theme, rootEl) {
|
|
297
|
+
const id = brandThemeId(theme);
|
|
298
|
+
const data = buildBrandThemeData(rootEl);
|
|
299
|
+
data.base = builtinBase(rootEl);
|
|
300
|
+
monaco2.editor.defineTheme(id, data);
|
|
301
|
+
monaco2.editor.setTheme(id);
|
|
302
|
+
return id;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/lib/use-data-theme.ts
|
|
306
|
+
import { useEffect, useState } from "react";
|
|
307
|
+
import { DEFAULT_THEME } from "@elabs-ai/components-tokens";
|
|
308
|
+
function useDataTheme(target) {
|
|
309
|
+
const [state, setState] = useState({ theme: DEFAULT_THEME, revision: 0 });
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
if (typeof document === "undefined") return;
|
|
312
|
+
const el = target ?? document.documentElement;
|
|
313
|
+
const read = () => {
|
|
314
|
+
const next = el.getAttribute("data-theme") ?? document.documentElement.getAttribute("data-theme");
|
|
315
|
+
setState((prev) => ({
|
|
316
|
+
// Any non-empty attribute value is a theme (ADR 0029 — names are open).
|
|
317
|
+
// Only "no attribute anywhere" keeps the previous value, which is what
|
|
318
|
+
// makes a late ThemeProvider write a no-op rather than a flash.
|
|
319
|
+
theme: next || prev.theme,
|
|
320
|
+
revision: prev.revision + 1
|
|
321
|
+
}));
|
|
322
|
+
};
|
|
323
|
+
read();
|
|
324
|
+
const observer = new MutationObserver(read);
|
|
325
|
+
observer.observe(el, { attributes: true, attributeFilter: ["data-theme"] });
|
|
326
|
+
if (el !== document.documentElement) {
|
|
327
|
+
observer.observe(document.documentElement, {
|
|
328
|
+
attributes: true,
|
|
329
|
+
attributeFilter: ["data-theme"]
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return () => observer.disconnect();
|
|
333
|
+
}, [target]);
|
|
334
|
+
return state;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/code-editor/code-editor.tsx
|
|
338
|
+
import * as monaco from "monaco-editor";
|
|
339
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
340
|
+
import {
|
|
341
|
+
forwardRef,
|
|
342
|
+
useEffect as useEffect2,
|
|
343
|
+
useImperativeHandle,
|
|
344
|
+
useRef,
|
|
345
|
+
useState as useState2
|
|
346
|
+
} from "react";
|
|
347
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
348
|
+
var BASE_OPTIONS = {
|
|
349
|
+
automaticLayout: true,
|
|
350
|
+
minimap: { enabled: false },
|
|
351
|
+
scrollBeyondLastLine: false,
|
|
352
|
+
smoothScrolling: true,
|
|
353
|
+
fontLigatures: true,
|
|
354
|
+
fontSize: 13,
|
|
355
|
+
lineNumbersMinChars: 3,
|
|
356
|
+
padding: { top: 12, bottom: 12 },
|
|
357
|
+
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 }
|
|
358
|
+
};
|
|
359
|
+
var CodeEditor = forwardRef(function CodeEditor2({
|
|
360
|
+
value,
|
|
361
|
+
defaultValue,
|
|
362
|
+
onChange,
|
|
363
|
+
language = "typescript",
|
|
364
|
+
path,
|
|
365
|
+
readOnly = false,
|
|
366
|
+
height = "100%",
|
|
367
|
+
options,
|
|
368
|
+
ariaLabel,
|
|
369
|
+
ariaInvalid,
|
|
370
|
+
ariaDescribedBy,
|
|
371
|
+
contextMenu = "brand",
|
|
372
|
+
onMount,
|
|
373
|
+
actions,
|
|
374
|
+
className,
|
|
375
|
+
style,
|
|
376
|
+
...props
|
|
377
|
+
}, ref) {
|
|
378
|
+
const containerRef = useRef(null);
|
|
379
|
+
const [editor2, setEditor] = useState2(null);
|
|
380
|
+
const { theme, revision } = useDataTheme();
|
|
381
|
+
const onChangeRef = useRef(onChange);
|
|
382
|
+
onChangeRef.current = onChange;
|
|
383
|
+
const onMountRef = useRef(onMount);
|
|
384
|
+
onMountRef.current = onMount;
|
|
385
|
+
useImperativeHandle(ref, () => editor2, [
|
|
386
|
+
editor2
|
|
387
|
+
]);
|
|
388
|
+
useEffect2(() => {
|
|
389
|
+
const container = containerRef.current;
|
|
390
|
+
if (!container) return;
|
|
391
|
+
const model = monaco.editor.createModel(
|
|
392
|
+
value ?? defaultValue ?? "",
|
|
393
|
+
language,
|
|
394
|
+
path ? monaco.Uri.parse(`inmemory://brand/${path}`) : void 0
|
|
395
|
+
);
|
|
396
|
+
const instance = monaco.editor.create(container, {
|
|
397
|
+
...BASE_OPTIONS,
|
|
398
|
+
readOnly,
|
|
399
|
+
// Disable Monaco's own menu unless explicitly opted into; "brand" renders
|
|
400
|
+
// brand-ui's ContextMenu around the editor instead.
|
|
401
|
+
contextmenu: contextMenu === "monaco",
|
|
402
|
+
// Monaco's accessible name comes from this construction option (it writes it
|
|
403
|
+
// onto its inner screen-reader <textarea>), not from a wrapper-div attribute.
|
|
404
|
+
...ariaLabel !== void 0 ? { ariaLabel } : null,
|
|
405
|
+
model,
|
|
406
|
+
...options
|
|
407
|
+
});
|
|
408
|
+
const sub = instance.onDidChangeModelContent(() => {
|
|
409
|
+
onChangeRef.current?.(instance.getValue());
|
|
410
|
+
});
|
|
411
|
+
setEditor(instance);
|
|
412
|
+
onMountRef.current?.(instance, monaco);
|
|
413
|
+
return () => {
|
|
414
|
+
sub.dispose();
|
|
415
|
+
instance.dispose();
|
|
416
|
+
model.dispose();
|
|
417
|
+
setEditor(null);
|
|
418
|
+
};
|
|
419
|
+
}, []);
|
|
420
|
+
useEffect2(() => {
|
|
421
|
+
if (!editor2 || value === void 0) return;
|
|
422
|
+
if (value !== editor2.getValue()) editor2.setValue(value);
|
|
423
|
+
}, [editor2, value]);
|
|
424
|
+
useEffect2(() => {
|
|
425
|
+
const model = editor2?.getModel();
|
|
426
|
+
if (model) monaco.editor.setModelLanguage(model, language);
|
|
427
|
+
}, [editor2, language]);
|
|
428
|
+
useEffect2(() => {
|
|
429
|
+
editor2?.updateOptions({ readOnly, contextmenu: contextMenu === "monaco" });
|
|
430
|
+
}, [editor2, readOnly, contextMenu]);
|
|
431
|
+
useEffect2(() => {
|
|
432
|
+
if (!editor2) return;
|
|
433
|
+
try {
|
|
434
|
+
applyBrandTheme(monaco, theme);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.error("[@elabs-ai/components-editor] failed to apply brand theme", err);
|
|
437
|
+
}
|
|
438
|
+
}, [editor2, theme, revision]);
|
|
439
|
+
useEffect2(() => {
|
|
440
|
+
if (!editor2) return;
|
|
441
|
+
if (ariaLabel !== void 0) editor2.updateOptions({ ariaLabel });
|
|
442
|
+
const textarea = editor2.getDomNode()?.querySelector("textarea");
|
|
443
|
+
if (!textarea) return;
|
|
444
|
+
if (ariaLabel === void 0) textarea.removeAttribute("aria-label");
|
|
445
|
+
else textarea.setAttribute("aria-label", ariaLabel);
|
|
446
|
+
if (ariaInvalid === void 0) textarea.removeAttribute("aria-invalid");
|
|
447
|
+
else textarea.setAttribute("aria-invalid", String(ariaInvalid));
|
|
448
|
+
if (ariaDescribedBy === void 0) textarea.removeAttribute("aria-describedby");
|
|
449
|
+
else textarea.setAttribute("aria-describedby", ariaDescribedBy);
|
|
450
|
+
}, [editor2, ariaLabel, ariaInvalid, ariaDescribedBy]);
|
|
451
|
+
useEffect2(() => {
|
|
452
|
+
if (!editor2 || !actions || actions.length === 0) return;
|
|
453
|
+
const disposables = actions.map((a) => editor2.addAction(a));
|
|
454
|
+
return () => disposables.forEach((d) => d.dispose());
|
|
455
|
+
}, [editor2, actions]);
|
|
456
|
+
const resolvedStyle = {
|
|
457
|
+
height: typeof height === "number" ? `${height}px` : height,
|
|
458
|
+
...style
|
|
459
|
+
};
|
|
460
|
+
const editorEl = /* @__PURE__ */ jsx2(
|
|
461
|
+
"div",
|
|
462
|
+
{
|
|
463
|
+
ref: containerRef,
|
|
464
|
+
"data-testid": "code-editor",
|
|
465
|
+
className: cn("h-full w-full overflow-hidden bg-background text-foreground", className),
|
|
466
|
+
style: resolvedStyle,
|
|
467
|
+
...props
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
if (contextMenu === "brand") {
|
|
471
|
+
return /* @__PURE__ */ jsx2(EditorContextMenu, { editor: editor2, readOnly, children: editorEl });
|
|
472
|
+
}
|
|
473
|
+
return editorEl;
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// src/calc-block/calc-editor.ts
|
|
477
|
+
var CALC_FENCE_LANG = "calc";
|
|
478
|
+
var OPEN_FENCE = /^(\s*)(`{3,}|~{3,})[ \t]*calc\b[ \t]*$/i;
|
|
479
|
+
function findCalcFences(text) {
|
|
480
|
+
const lines = text.split("\n");
|
|
481
|
+
const fences = [];
|
|
482
|
+
let i = 0;
|
|
483
|
+
while (i < lines.length) {
|
|
484
|
+
const open = OPEN_FENCE.exec(lines[i] ?? "");
|
|
485
|
+
if (!open) {
|
|
486
|
+
i++;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const run = open[2] ?? "```";
|
|
490
|
+
const marker = run[0] ?? "`";
|
|
491
|
+
const closeRe = new RegExp(`^\\s*\\${marker}{${run.length},}\\s*$`);
|
|
492
|
+
let j = i + 1;
|
|
493
|
+
while (j < lines.length && !closeRe.test(lines[j] ?? "")) j++;
|
|
494
|
+
const openLine = i + 1;
|
|
495
|
+
const closeLine = j < lines.length ? j + 1 : lines.length + 1;
|
|
496
|
+
fences.push({
|
|
497
|
+
openLine,
|
|
498
|
+
closeLine,
|
|
499
|
+
bodyStartLine: openLine + 1,
|
|
500
|
+
bodyEndLine: closeLine - 1,
|
|
501
|
+
source: lines.slice(i + 1, j).join("\n")
|
|
502
|
+
});
|
|
503
|
+
i = j + 1;
|
|
504
|
+
}
|
|
505
|
+
return fences;
|
|
506
|
+
}
|
|
507
|
+
function calcLineLayout(source) {
|
|
508
|
+
const spans = [];
|
|
509
|
+
let offset = 0;
|
|
510
|
+
for (const text of source.split("\n")) {
|
|
511
|
+
spans.push({ text, offset });
|
|
512
|
+
offset += text.length + 1;
|
|
513
|
+
}
|
|
514
|
+
return spans;
|
|
515
|
+
}
|
|
516
|
+
function resolveResults(hooks, source, ctx2) {
|
|
517
|
+
const map = /* @__PURE__ */ new Map();
|
|
518
|
+
if (!hooks.evaluate) return map;
|
|
519
|
+
let sheet;
|
|
520
|
+
try {
|
|
521
|
+
sheet = hooks.evaluate(source, ctx2);
|
|
522
|
+
} catch {
|
|
523
|
+
return map;
|
|
524
|
+
}
|
|
525
|
+
for (const r of sheet.results ?? []) map.set(r.line, r);
|
|
526
|
+
return map;
|
|
527
|
+
}
|
|
528
|
+
function resolveHighlight(hooks, source, ctx2) {
|
|
529
|
+
const lines = source.split("\n");
|
|
530
|
+
if (hooks.tokenize) {
|
|
531
|
+
const { tokenize } = hooks;
|
|
532
|
+
return lines.map((line) => {
|
|
533
|
+
try {
|
|
534
|
+
return tokenize(line, ctx2) ?? [];
|
|
535
|
+
} catch {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
if (hooks.evaluate) {
|
|
541
|
+
const byLine = resolveResults(hooks, source, ctx2);
|
|
542
|
+
return lines.map((_line, i) => byLine.get(i + 1)?.tokens ?? []);
|
|
543
|
+
}
|
|
544
|
+
return lines.map(() => []);
|
|
545
|
+
}
|
|
546
|
+
function inlayTextForResult(result) {
|
|
547
|
+
if (!result) return null;
|
|
548
|
+
if (result.error) return { text: `error: ${result.error.message}`, isError: true };
|
|
549
|
+
if (result.value) return { text: `= ${result.value.display}`, isError: false };
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
function resolveInlays(hooks, source, ctx2) {
|
|
553
|
+
const byLine = resolveResults(hooks, source, ctx2);
|
|
554
|
+
const out = [];
|
|
555
|
+
for (const [lineNumber, result] of byLine) {
|
|
556
|
+
const inlay = inlayTextForResult(result);
|
|
557
|
+
if (inlay) out.push({ lineNumber, text: inlay.text, isError: inlay.isError });
|
|
558
|
+
}
|
|
559
|
+
out.sort((a, b) => a.lineNumber - b.lineNumber);
|
|
560
|
+
return out;
|
|
561
|
+
}
|
|
562
|
+
function calcTokenClassName(kind, resolved) {
|
|
563
|
+
const base = `brand-calc-tok brand-calc-tok--${kind}`;
|
|
564
|
+
return resolved ? base : `${base} brand-calc-tok--unresolved`;
|
|
565
|
+
}
|
|
566
|
+
function identifierPrefix(line, column) {
|
|
567
|
+
const upto = line.slice(0, Math.max(0, column));
|
|
568
|
+
const m = /[A-Za-z_][A-Za-z0-9_]*$/.exec(upto);
|
|
569
|
+
return m ? m[0] : "";
|
|
570
|
+
}
|
|
571
|
+
var clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
572
|
+
function calcDecorationSpecs(hooks, bodyStartLine, bodyLineTexts, ctx2) {
|
|
573
|
+
const tokensByLine = resolveHighlight(hooks, bodyLineTexts.join("\n"), ctx2);
|
|
574
|
+
const specs = [];
|
|
575
|
+
for (let i = 0; i < bodyLineTexts.length; i++) {
|
|
576
|
+
const lineText = bodyLineTexts[i] ?? "";
|
|
577
|
+
for (const t of tokensByLine[i] ?? []) {
|
|
578
|
+
const start = clamp(t.start, 0, lineText.length);
|
|
579
|
+
const end = clamp(t.end, start, lineText.length);
|
|
580
|
+
if (end <= start) continue;
|
|
581
|
+
specs.push({
|
|
582
|
+
lineNumber: bodyStartLine + i,
|
|
583
|
+
startColumn: start + 1,
|
|
584
|
+
endColumn: end + 1,
|
|
585
|
+
className: calcTokenClassName(t.kind, t.resolved)
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return specs;
|
|
590
|
+
}
|
|
591
|
+
function calcInlaySpecs(hooks, bodyStartLine, bodyLineTexts, ctx2) {
|
|
592
|
+
return resolveInlays(hooks, bodyLineTexts.join("\n"), ctx2).map((inlay) => {
|
|
593
|
+
const i = inlay.lineNumber - 1;
|
|
594
|
+
const lineText = bodyLineTexts[i] ?? "";
|
|
595
|
+
return {
|
|
596
|
+
lineNumber: bodyStartLine + i,
|
|
597
|
+
column: lineText.length + 1,
|
|
598
|
+
text: inlay.text,
|
|
599
|
+
isError: inlay.isError
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/calc-block/calc-editor-prose.ts
|
|
605
|
+
import { Plugin, PluginKey } from "@milkdown/kit/prose/state";
|
|
606
|
+
import { Decoration, DecorationSet } from "@milkdown/kit/prose/view";
|
|
607
|
+
import { $prose } from "@milkdown/kit/utils";
|
|
608
|
+
var clamp2 = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
609
|
+
var calcDecorationKey = new PluginKey("brand-calc-decorations");
|
|
610
|
+
function inlayWidget(inlay) {
|
|
611
|
+
const span = document.createElement("span");
|
|
612
|
+
span.className = inlay.isError ? "brand-calc-inlay brand-calc-inlay--error" : "brand-calc-inlay";
|
|
613
|
+
span.textContent = inlay.text;
|
|
614
|
+
span.setAttribute("contenteditable", "false");
|
|
615
|
+
span.setAttribute("title", inlay.isError ? "Calc error" : "Calc result");
|
|
616
|
+
return span;
|
|
617
|
+
}
|
|
618
|
+
function buildCalcDecorations(doc, hooks) {
|
|
619
|
+
if (!hooks || !hooks.tokenize && !hooks.evaluate) return DecorationSet.empty;
|
|
620
|
+
const decorations = [];
|
|
621
|
+
doc.descendants((node, pos) => {
|
|
622
|
+
if (node.type.name !== "code_block" || node.attrs.language !== CALC_FENCE_LANG) {
|
|
623
|
+
return void 0;
|
|
624
|
+
}
|
|
625
|
+
const source = node.textContent;
|
|
626
|
+
const contentStart = pos + 1;
|
|
627
|
+
const layout = calcLineLayout(source);
|
|
628
|
+
const tokensByLine = resolveHighlight(hooks, source);
|
|
629
|
+
layout.forEach((span, i) => {
|
|
630
|
+
const lineStart = contentStart + span.offset;
|
|
631
|
+
for (const t of tokensByLine[i] ?? []) {
|
|
632
|
+
const start = clamp2(t.start, 0, span.text.length);
|
|
633
|
+
const end = clamp2(t.end, start, span.text.length);
|
|
634
|
+
if (end <= start) continue;
|
|
635
|
+
decorations.push(
|
|
636
|
+
Decoration.inline(lineStart + start, lineStart + end, {
|
|
637
|
+
class: calcTokenClassName(t.kind, t.resolved)
|
|
638
|
+
})
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
for (const inlay of resolveInlays(hooks, source)) {
|
|
643
|
+
const span = layout[inlay.lineNumber - 1];
|
|
644
|
+
if (!span) continue;
|
|
645
|
+
const at = contentStart + span.offset + span.text.length;
|
|
646
|
+
decorations.push(
|
|
647
|
+
Decoration.widget(at, () => inlayWidget(inlay), {
|
|
648
|
+
side: 1,
|
|
649
|
+
ignoreSelection: true,
|
|
650
|
+
key: `brand-calc-inlay:${String(inlay.lineNumber)}:${inlay.text}`
|
|
651
|
+
})
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
return false;
|
|
655
|
+
});
|
|
656
|
+
return DecorationSet.create(doc, decorations);
|
|
657
|
+
}
|
|
658
|
+
function calcProsePlugins(getHooks) {
|
|
659
|
+
const plugin = $prose(
|
|
660
|
+
() => new Plugin({
|
|
661
|
+
key: calcDecorationKey,
|
|
662
|
+
state: {
|
|
663
|
+
init: (_config, state) => buildCalcDecorations(state.doc, getHooks()),
|
|
664
|
+
apply: (tr, value, _old, newState) => tr.docChanged ? buildCalcDecorations(newState.doc, getHooks()) : value
|
|
665
|
+
},
|
|
666
|
+
props: {
|
|
667
|
+
decorations(state) {
|
|
668
|
+
return calcDecorationKey.getState(state);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
return [plugin];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/lib/editor-completions.ts
|
|
677
|
+
function triggerQueryStart(lineText, column, triggerCharacters) {
|
|
678
|
+
if (!triggerCharacters || triggerCharacters.length === 0) return null;
|
|
679
|
+
const before = lineText.slice(0, Math.max(0, column - 1));
|
|
680
|
+
let bestIndex = -1;
|
|
681
|
+
for (const ch of triggerCharacters) {
|
|
682
|
+
if (!ch) continue;
|
|
683
|
+
const idx = before.lastIndexOf(ch);
|
|
684
|
+
if (idx > bestIndex) bestIndex = idx;
|
|
685
|
+
}
|
|
686
|
+
if (bestIndex === -1) return null;
|
|
687
|
+
return bestIndex + 2;
|
|
688
|
+
}
|
|
689
|
+
function resolveReplaceRange(item, position, lineText, triggerCharacters) {
|
|
690
|
+
const start = item.replaceFrom ?? triggerQueryStart(lineText, position.column, triggerCharacters) ?? position.column;
|
|
691
|
+
return {
|
|
692
|
+
startLineNumber: position.lineNumber,
|
|
693
|
+
// Clamp into [1, column] — a bad/stale `replaceFrom` degrades to "insert at
|
|
694
|
+
// caret" rather than producing a backwards/out-of-range edit.
|
|
695
|
+
startColumn: Math.min(Math.max(1, start), position.column),
|
|
696
|
+
endLineNumber: position.lineNumber,
|
|
697
|
+
endColumn: position.column
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
async function collectCompletions(providers, ctx2) {
|
|
701
|
+
const perProvider = await Promise.all(
|
|
702
|
+
providers.map(async (provider) => {
|
|
703
|
+
try {
|
|
704
|
+
const items = await provider.provide(ctx2) ?? [];
|
|
705
|
+
return items.map((item) => ({ provider, item }));
|
|
706
|
+
} catch {
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
return perProvider.flat();
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/lib/editor-content-access-prose.ts
|
|
715
|
+
import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@milkdown/kit/prose/state";
|
|
716
|
+
import { $prose as $prose2 } from "@milkdown/kit/utils";
|
|
717
|
+
var SELECTION_WATCH_KEY = new PluginKey2("editorContentAccess_selectionWatch");
|
|
718
|
+
function selectionWatchPlugin(getListeners, getSerializeSlice) {
|
|
719
|
+
const plugin = $prose2(
|
|
720
|
+
() => new Plugin2({
|
|
721
|
+
key: SELECTION_WATCH_KEY,
|
|
722
|
+
view() {
|
|
723
|
+
return {
|
|
724
|
+
update(view, prevState) {
|
|
725
|
+
const ls = getListeners();
|
|
726
|
+
if (ls.size === 0) return;
|
|
727
|
+
if (prevState.selection.eq(view.state.selection)) return;
|
|
728
|
+
const { selection } = view.state;
|
|
729
|
+
const text = selection.empty ? "" : getSerializeSlice()(view);
|
|
730
|
+
const sel = { text, empty: selection.empty };
|
|
731
|
+
ls.forEach((l) => l(sel));
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
);
|
|
737
|
+
return plugin;
|
|
738
|
+
}
|
|
739
|
+
function proseMirrorContentAccess(deps, options = {}) {
|
|
740
|
+
const { getView, getText, serializeSlice, parseAndReplace, listeners } = deps;
|
|
741
|
+
const plainText2 = options.fidelity === "plainText";
|
|
742
|
+
function readSelection(view) {
|
|
743
|
+
const { selection } = view.state;
|
|
744
|
+
if (selection.empty) return { text: "", empty: true };
|
|
745
|
+
const text = plainText2 ? view.state.doc.textBetween(selection.from, selection.to, "\n") : serializeSlice(view);
|
|
746
|
+
return { text, empty: false };
|
|
747
|
+
}
|
|
748
|
+
const apply = (text) => {
|
|
749
|
+
const view = getView();
|
|
750
|
+
if (!view) return;
|
|
751
|
+
try {
|
|
752
|
+
if (plainText2) {
|
|
753
|
+
view.dispatch(view.state.tr.insertText(text).scrollIntoView());
|
|
754
|
+
} else {
|
|
755
|
+
parseAndReplace(view, text);
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
return {
|
|
761
|
+
getText,
|
|
762
|
+
getSelection() {
|
|
763
|
+
const view = getView();
|
|
764
|
+
if (!view) return { text: "", empty: true };
|
|
765
|
+
return readSelection(view);
|
|
766
|
+
},
|
|
767
|
+
replaceSelection: apply,
|
|
768
|
+
insertAtCursor: apply,
|
|
769
|
+
focus() {
|
|
770
|
+
getView()?.focus();
|
|
771
|
+
},
|
|
772
|
+
onSelectionChange(listener2) {
|
|
773
|
+
listeners.add(listener2);
|
|
774
|
+
return () => listeners.delete(listener2);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/lib/markdown/markdown-scale.ts
|
|
780
|
+
import {
|
|
781
|
+
PROSE_HEADING_REM,
|
|
782
|
+
PROSE_HEADING_TRACKING,
|
|
783
|
+
PROSE_HEADING_WEIGHT
|
|
784
|
+
} from "@elabs-ai/components-ui";
|
|
785
|
+
var MARKDOWN_HEADING_REM = PROSE_HEADING_REM;
|
|
786
|
+
var MARKDOWN_HEADING_WEIGHT = PROSE_HEADING_WEIGHT;
|
|
787
|
+
var MARKDOWN_HEADING_TRACKING = PROSE_HEADING_TRACKING;
|
|
788
|
+
var MARKDOWN_MEASURE = "48rem";
|
|
789
|
+
function markdownScaleVars() {
|
|
790
|
+
return {
|
|
791
|
+
"--md-h1": `${MARKDOWN_HEADING_REM[1]}rem`,
|
|
792
|
+
"--md-h2": `${MARKDOWN_HEADING_REM[2]}rem`,
|
|
793
|
+
"--md-h3": `${MARKDOWN_HEADING_REM[3]}rem`,
|
|
794
|
+
"--md-h4": `${MARKDOWN_HEADING_REM[4]}rem`,
|
|
795
|
+
"--md-h5": `${MARKDOWN_HEADING_REM[5]}rem`,
|
|
796
|
+
"--md-h6": `${MARKDOWN_HEADING_REM[6]}rem`,
|
|
797
|
+
"--md-heading-weight": String(MARKDOWN_HEADING_WEIGHT),
|
|
798
|
+
"--md-heading-tracking": MARKDOWN_HEADING_TRACKING,
|
|
799
|
+
"--md-measure": MARKDOWN_MEASURE
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/markdown-iteration/edit-context.ts
|
|
804
|
+
import { createContext } from "react";
|
|
805
|
+
var IterationEditContext = createContext(null);
|
|
806
|
+
|
|
807
|
+
// src/lib/markdown/frontmatter.ts
|
|
808
|
+
import { dump, load } from "js-yaml";
|
|
809
|
+
var FRONTMATTER_RE = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
810
|
+
function parseFrontmatter(source) {
|
|
811
|
+
const match = source.match(FRONTMATTER_RE);
|
|
812
|
+
if (!match) {
|
|
813
|
+
return { frontmatter: {}, content: source, hasFrontmatter: false };
|
|
814
|
+
}
|
|
815
|
+
const parsed = load(match[1] ?? "");
|
|
816
|
+
const frontmatter = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
817
|
+
return {
|
|
818
|
+
frontmatter,
|
|
819
|
+
content: source.slice(match[0].length),
|
|
820
|
+
hasFrontmatter: true
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function serializeFrontmatter(frontmatter, content) {
|
|
824
|
+
const body = content.replace(/^\s+/, "");
|
|
825
|
+
if (!frontmatter || Object.keys(frontmatter).length === 0) {
|
|
826
|
+
return body;
|
|
827
|
+
}
|
|
828
|
+
const yaml = dump(frontmatter, { lineWidth: -1, noRefs: true }).trimEnd();
|
|
829
|
+
return `---
|
|
830
|
+
${yaml}
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
${body}`;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/lib/markdown/slugify.ts
|
|
837
|
+
function plainText(raw) {
|
|
838
|
+
return raw.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[`*_~]/g, "").trim();
|
|
839
|
+
}
|
|
840
|
+
function slugifyHeading(text) {
|
|
841
|
+
return text.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-") || "section";
|
|
842
|
+
}
|
|
843
|
+
function uniqueSlug(base, used) {
|
|
844
|
+
const seen = used.get(base) ?? 0;
|
|
845
|
+
used.set(base, seen + 1);
|
|
846
|
+
return seen === 0 ? base : `${base}-${seen}`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/markdown-outline/markdown-outline.ts
|
|
850
|
+
var HEADING_RE = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
851
|
+
var FENCE_RE = /^(```|~~~)/;
|
|
852
|
+
function parseMarkdownOutline(markdown) {
|
|
853
|
+
let body = markdown;
|
|
854
|
+
try {
|
|
855
|
+
body = parseFrontmatter(markdown).content;
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
const lines = body.split("\n");
|
|
859
|
+
const items = [];
|
|
860
|
+
const used = /* @__PURE__ */ new Map();
|
|
861
|
+
let inFence = false;
|
|
862
|
+
for (let i = 0; i < lines.length; i++) {
|
|
863
|
+
const line = lines[i];
|
|
864
|
+
if (FENCE_RE.test(line.trimStart())) {
|
|
865
|
+
inFence = !inFence;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (inFence) continue;
|
|
869
|
+
const match = HEADING_RE.exec(line);
|
|
870
|
+
if (!match) continue;
|
|
871
|
+
const text = plainText(match[2] ?? "");
|
|
872
|
+
if (!text) continue;
|
|
873
|
+
const base = slugifyHeading(text);
|
|
874
|
+
const id = uniqueSlug(base, used);
|
|
875
|
+
items.push({
|
|
876
|
+
id,
|
|
877
|
+
text,
|
|
878
|
+
level: match[1].length,
|
|
879
|
+
line: i + 1
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
return items;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/markdown-outline/document-outline.tsx
|
|
886
|
+
import { cn as cn2 } from "@elabs-ai/components-ui/lib/cn";
|
|
887
|
+
import { forwardRef as forwardRef2, useMemo } from "react";
|
|
888
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
889
|
+
function useMarkdownOutline(markdown) {
|
|
890
|
+
return useMemo(() => parseMarkdownOutline(markdown), [markdown]);
|
|
891
|
+
}
|
|
892
|
+
var DocumentOutline = forwardRef2(
|
|
893
|
+
function DocumentOutline2({ items, activeId, onSelect, itemActions, className, ...props }, ref) {
|
|
894
|
+
const minLevel = items.reduce((min, it) => Math.min(min, it.level), 6);
|
|
895
|
+
return /* @__PURE__ */ jsxs2(
|
|
896
|
+
"nav",
|
|
897
|
+
{
|
|
898
|
+
ref,
|
|
899
|
+
"aria-label": "Document outline",
|
|
900
|
+
className: cn2("text-body", className),
|
|
901
|
+
...props,
|
|
902
|
+
children: [
|
|
903
|
+
/* @__PURE__ */ jsx3("ul", { className: "m-0 list-none border-s border-border p-0", children: items.map((item) => {
|
|
904
|
+
const actions = itemActions?.(item);
|
|
905
|
+
return /* @__PURE__ */ jsxs2("li", { className: cn2(actions && "group/outline-item relative"), children: [
|
|
906
|
+
/* @__PURE__ */ jsx3(
|
|
907
|
+
"button",
|
|
908
|
+
{
|
|
909
|
+
type: "button",
|
|
910
|
+
"aria-current": item.id === activeId ? "true" : void 0,
|
|
911
|
+
onClick: () => onSelect?.(item),
|
|
912
|
+
className: cn2(
|
|
913
|
+
"-ms-px block w-full truncate border-s-2 py-1 pe-2 text-start text-caption",
|
|
914
|
+
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
915
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
|
916
|
+
item.id === activeId ? "border-s-primary font-medium text-foreground" : "border-s-transparent text-muted-foreground hover:border-s-border-strong hover:text-foreground",
|
|
917
|
+
actions && "pe-8"
|
|
918
|
+
),
|
|
919
|
+
style: { paddingInlineStart: `${(item.level - minLevel) * 0.875 + 0.75}rem` },
|
|
920
|
+
children: item.text
|
|
921
|
+
}
|
|
922
|
+
),
|
|
923
|
+
actions ? /* @__PURE__ */ jsx3("span", { className: "absolute end-0.5 top-1/2 -translate-y-1/2 opacity-0 transition-opacity duration-fast ease-standard focus-within:opacity-100 group-hover/outline-item:opacity-100 has-[[aria-pressed=true]]:opacity-100 motion-reduce:transition-none", children: actions }) : null
|
|
924
|
+
] }, item.id);
|
|
925
|
+
}) }),
|
|
926
|
+
items.length === 0 ? /* @__PURE__ */ jsx3("p", { className: "px-2 py-1 text-caption text-muted-foreground", children: "No headings yet." }) : null
|
|
927
|
+
]
|
|
928
|
+
}
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
// src/markdown-iteration/iteration.tsx
|
|
934
|
+
import {
|
|
935
|
+
BentoGrid,
|
|
936
|
+
BentoGridItem,
|
|
937
|
+
Table,
|
|
938
|
+
TableBody,
|
|
939
|
+
TableCell,
|
|
940
|
+
TableHead,
|
|
941
|
+
TableHeader,
|
|
942
|
+
TableRow
|
|
943
|
+
} from "@elabs-ai/components-ui";
|
|
944
|
+
import { cn as cn3 } from "@elabs-ai/components-ui/lib/cn";
|
|
945
|
+
import { Repeat2 } from "lucide-react";
|
|
946
|
+
import { forwardRef as forwardRef3 } from "react";
|
|
947
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
948
|
+
var TOKEN_RE = /\{\{\s*([\w.$]+)\s*\}\}/g;
|
|
949
|
+
function defaultInterpolate(template, context) {
|
|
950
|
+
return template.replace(TOKEN_RE, (match, path) => {
|
|
951
|
+
const value = path.split(".").reduce((obj, key) => {
|
|
952
|
+
if (obj && typeof obj === "object") return obj[key];
|
|
953
|
+
return void 0;
|
|
954
|
+
}, context);
|
|
955
|
+
return value == null ? match : String(value);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
function cellScope(spec, cell, index) {
|
|
959
|
+
return {
|
|
960
|
+
...cell.context,
|
|
961
|
+
[spec.as]: cell.context,
|
|
962
|
+
index,
|
|
963
|
+
row: cell.row,
|
|
964
|
+
col: cell.col
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function safeEvaluate(evaluate, spec) {
|
|
968
|
+
try {
|
|
969
|
+
return evaluate(spec);
|
|
970
|
+
} catch {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function chunk(arr, size) {
|
|
975
|
+
if (size <= 0) return [arr];
|
|
976
|
+
const out = [];
|
|
977
|
+
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
|
978
|
+
return out;
|
|
979
|
+
}
|
|
980
|
+
function bentoSize(index) {
|
|
981
|
+
const m = index % 6;
|
|
982
|
+
if (m === 0) return "lg";
|
|
983
|
+
if (m === 3) return "md";
|
|
984
|
+
return "sm";
|
|
985
|
+
}
|
|
986
|
+
var IterationBlock = forwardRef3(
|
|
987
|
+
function IterationBlock2({
|
|
988
|
+
spec,
|
|
989
|
+
evaluate,
|
|
990
|
+
interpolate = defaultInterpolate,
|
|
991
|
+
render,
|
|
992
|
+
layout,
|
|
993
|
+
emptyLabel,
|
|
994
|
+
className,
|
|
995
|
+
...props
|
|
996
|
+
}, ref) {
|
|
997
|
+
const data = safeEvaluate(evaluate, spec);
|
|
998
|
+
const cells = data?.cells ?? [];
|
|
999
|
+
const resolvedLayout = layout ?? spec.layout;
|
|
1000
|
+
const renderCell = (cell, index) => {
|
|
1001
|
+
const md = cell.markdown ?? interpolate(spec.template, cellScope(spec, cell, index));
|
|
1002
|
+
return render(md);
|
|
1003
|
+
};
|
|
1004
|
+
const label = spec.kind === "pivot" ? "Pivot" : "Iteration";
|
|
1005
|
+
if (cells.length === 0) {
|
|
1006
|
+
return /* @__PURE__ */ jsxs3(
|
|
1007
|
+
"div",
|
|
1008
|
+
{
|
|
1009
|
+
ref,
|
|
1010
|
+
role: "group",
|
|
1011
|
+
"aria-label": label,
|
|
1012
|
+
"data-iteration": spec.kind,
|
|
1013
|
+
className: cn3("my-4 flex items-center gap-2 text-meta text-muted-foreground", className),
|
|
1014
|
+
...props,
|
|
1015
|
+
children: [
|
|
1016
|
+
/* @__PURE__ */ jsx4(Repeat2, { className: "size-4 shrink-0", "aria-hidden": "true" }),
|
|
1017
|
+
/* @__PURE__ */ jsx4("span", { children: emptyLabel ?? `Nothing to ${spec.kind === "pivot" ? "pivot" : "iterate"} yet.` })
|
|
1018
|
+
]
|
|
1019
|
+
}
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
const isMatrix = resolvedLayout === "matrix" && !!data?.rowHeaders?.length && !!data?.colHeaders?.length;
|
|
1023
|
+
let body;
|
|
1024
|
+
if (resolvedLayout === "stacked") {
|
|
1025
|
+
body = /* @__PURE__ */ jsx4("ol", { className: "space-y-6", children: cells.map((cell, i) => /* @__PURE__ */ jsx4("li", { className: "[&>*:first-child]:mt-0 [&>*:last-child]:mb-0", children: renderCell(cell, i) }, cell.key ?? i)) });
|
|
1026
|
+
} else if (resolvedLayout === "bento") {
|
|
1027
|
+
body = /* @__PURE__ */ jsx4(BentoGrid, { children: cells.map((cell, i) => /* @__PURE__ */ jsx4(BentoGridItem, { size: cell.size ?? bentoSize(i), children: /* @__PURE__ */ jsx4("div", { className: "h-full overflow-auto p-4 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", children: renderCell(cell, i) }) }, cell.key ?? i)) });
|
|
1028
|
+
} else if (isMatrix) {
|
|
1029
|
+
const rowHeaders = data.rowHeaders;
|
|
1030
|
+
const colHeaders = data.colHeaders;
|
|
1031
|
+
const at = /* @__PURE__ */ new Map();
|
|
1032
|
+
cells.forEach((cell, i) => {
|
|
1033
|
+
if (cell.row != null && cell.col != null)
|
|
1034
|
+
at.set(JSON.stringify([cell.row, cell.col]), { cell, index: i });
|
|
1035
|
+
});
|
|
1036
|
+
body = /* @__PURE__ */ jsxs3(Table, { children: [
|
|
1037
|
+
/* @__PURE__ */ jsx4(TableHeader, { children: /* @__PURE__ */ jsxs3(TableRow, { children: [
|
|
1038
|
+
/* @__PURE__ */ jsx4(TableHead, { "aria-hidden": "true" }),
|
|
1039
|
+
colHeaders.map((c) => /* @__PURE__ */ jsx4(TableHead, { scope: "col", children: c }, c))
|
|
1040
|
+
] }) }),
|
|
1041
|
+
/* @__PURE__ */ jsx4(TableBody, { children: rowHeaders.map((r) => /* @__PURE__ */ jsxs3(TableRow, { children: [
|
|
1042
|
+
/* @__PURE__ */ jsx4(TableHead, { scope: "row", className: "font-medium text-foreground", children: r }),
|
|
1043
|
+
colHeaders.map((c) => {
|
|
1044
|
+
const hit = at.get(JSON.stringify([r, c]));
|
|
1045
|
+
return /* @__PURE__ */ jsx4(TableCell, { className: "align-top", children: hit ? renderCell(hit.cell, hit.index) : null }, c);
|
|
1046
|
+
})
|
|
1047
|
+
] }, r)) })
|
|
1048
|
+
] });
|
|
1049
|
+
} else {
|
|
1050
|
+
const headers = data?.columns;
|
|
1051
|
+
const colCount = (headers?.length ?? spec.columns ?? Math.min(cells.length, 3)) || 1;
|
|
1052
|
+
const rows = chunk(cells, colCount);
|
|
1053
|
+
body = /* @__PURE__ */ jsxs3(Table, { children: [
|
|
1054
|
+
headers ? /* @__PURE__ */ jsx4(TableHeader, { children: /* @__PURE__ */ jsx4(TableRow, { children: headers.map((h) => /* @__PURE__ */ jsx4(TableHead, { scope: "col", children: h }, h)) }) }) : null,
|
|
1055
|
+
/* @__PURE__ */ jsx4(TableBody, { children: rows.map((row, r) => /* @__PURE__ */ jsx4(TableRow, { children: row.map((cell, c) => /* @__PURE__ */ jsx4(TableCell, { className: "align-top", children: renderCell(cell, r * colCount + c) }, cell.key ?? c)) }, r)) })
|
|
1056
|
+
] });
|
|
1057
|
+
}
|
|
1058
|
+
return /* @__PURE__ */ jsx4(
|
|
1059
|
+
"div",
|
|
1060
|
+
{
|
|
1061
|
+
ref,
|
|
1062
|
+
role: "group",
|
|
1063
|
+
"aria-label": label,
|
|
1064
|
+
"data-iteration": spec.kind,
|
|
1065
|
+
"data-iteration-layout": resolvedLayout,
|
|
1066
|
+
className: cn3("my-4", className),
|
|
1067
|
+
...props,
|
|
1068
|
+
children: body
|
|
1069
|
+
}
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
// src/markdown-iteration/iteration-builder.ts
|
|
1075
|
+
var DEFAULT_LAYOUT = {
|
|
1076
|
+
iterate: "stacked",
|
|
1077
|
+
pivot: "matrix"
|
|
1078
|
+
};
|
|
1079
|
+
var ITERATION_LAYOUTS = {
|
|
1080
|
+
iterate: ["stacked", "grid", "bento"],
|
|
1081
|
+
pivot: ["matrix", "grid", "bento"]
|
|
1082
|
+
};
|
|
1083
|
+
var DEFAULT_TEMPLATE = {
|
|
1084
|
+
iterate: "{{item.name}}",
|
|
1085
|
+
pivot: "{{row}} \xB7 {{col}}"
|
|
1086
|
+
};
|
|
1087
|
+
function splitList(value) {
|
|
1088
|
+
return (value ?? "").split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
1089
|
+
}
|
|
1090
|
+
function parseAttributes(attrString2) {
|
|
1091
|
+
const out = {};
|
|
1092
|
+
const re = /([\w-]+)\s*=\s*"([^"]*)"/g;
|
|
1093
|
+
let m;
|
|
1094
|
+
while (m = re.exec(attrString2)) {
|
|
1095
|
+
const key = m[1];
|
|
1096
|
+
if (key) out[key] = m[2] ?? "";
|
|
1097
|
+
}
|
|
1098
|
+
return out;
|
|
1099
|
+
}
|
|
1100
|
+
function attrString(attrs) {
|
|
1101
|
+
return Object.entries(attrs).filter(([, v]) => v != null && v !== "").map(([k, v]) => `${k}="${String(v).replace(/"/g, "'")}"`).join(" ");
|
|
1102
|
+
}
|
|
1103
|
+
function directivePartsFromValue(value) {
|
|
1104
|
+
const attributes = value.kind === "pivot" ? {
|
|
1105
|
+
layout: value.layout,
|
|
1106
|
+
rows: value.values.join(", "),
|
|
1107
|
+
cols: (value.cols ?? []).join(", ")
|
|
1108
|
+
} : {
|
|
1109
|
+
as: value.as || "item",
|
|
1110
|
+
layout: value.layout,
|
|
1111
|
+
values: value.values.join(", ")
|
|
1112
|
+
};
|
|
1113
|
+
return { attributes, template: value.template.trim() };
|
|
1114
|
+
}
|
|
1115
|
+
function maxColonRun(template) {
|
|
1116
|
+
let max = 0;
|
|
1117
|
+
const re = /^ {0,3}(:{3,})/;
|
|
1118
|
+
for (const line of template.split(/\r?\n/)) {
|
|
1119
|
+
const m = re.exec(line);
|
|
1120
|
+
if (m && m[1].length > max) max = m[1].length;
|
|
1121
|
+
}
|
|
1122
|
+
return max;
|
|
1123
|
+
}
|
|
1124
|
+
function outerDirectiveFence(template) {
|
|
1125
|
+
return ":".repeat(Math.max(3, maxColonRun(template) + 1));
|
|
1126
|
+
}
|
|
1127
|
+
function serializeIterationDirective(value) {
|
|
1128
|
+
const { attributes, template } = directivePartsFromValue(value);
|
|
1129
|
+
const fence = outerDirectiveFence(template);
|
|
1130
|
+
return `${fence}${value.kind}{${attrString(attributes)}}
|
|
1131
|
+
${template}
|
|
1132
|
+
${fence}`;
|
|
1133
|
+
}
|
|
1134
|
+
function builderValueFromParts(kind, attributes, template) {
|
|
1135
|
+
const layout = attributes.layout || DEFAULT_LAYOUT[kind];
|
|
1136
|
+
if (kind === "pivot") {
|
|
1137
|
+
return {
|
|
1138
|
+
kind,
|
|
1139
|
+
as: attributes.as?.trim() || "item",
|
|
1140
|
+
layout,
|
|
1141
|
+
values: splitList(attributes.rows),
|
|
1142
|
+
cols: splitList(attributes.cols),
|
|
1143
|
+
template: template.trim() || DEFAULT_TEMPLATE.pivot
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
return {
|
|
1147
|
+
kind,
|
|
1148
|
+
as: attributes.as?.trim() || "item",
|
|
1149
|
+
layout,
|
|
1150
|
+
values: splitList(attributes.values),
|
|
1151
|
+
template: template.trim() || DEFAULT_TEMPLATE.iterate
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function parseIterationDirective(markdown) {
|
|
1155
|
+
const m = markdown.match(/^(:{3,})(iterate|pivot)\{([^}]*)\}\n?([\s\S]*?)\n?\1\s*$/);
|
|
1156
|
+
if (!m) return null;
|
|
1157
|
+
return builderValueFromParts(
|
|
1158
|
+
m[2],
|
|
1159
|
+
parseAttributes(m[3] ?? ""),
|
|
1160
|
+
m[4] ?? ""
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
function emptyBuilderValue(kind) {
|
|
1164
|
+
return {
|
|
1165
|
+
kind,
|
|
1166
|
+
as: "item",
|
|
1167
|
+
layout: DEFAULT_LAYOUT[kind],
|
|
1168
|
+
values: [],
|
|
1169
|
+
cols: kind === "pivot" ? [] : void 0,
|
|
1170
|
+
template: DEFAULT_TEMPLATE[kind]
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
function evaluateEmbedded(spec) {
|
|
1174
|
+
if (spec.kind === "pivot") {
|
|
1175
|
+
const rows = splitList(spec.rows ?? spec.attributes.rows);
|
|
1176
|
+
const cols = splitList(spec.cols ?? spec.attributes.cols);
|
|
1177
|
+
if (rows.length === 0 || cols.length === 0) return { cells: [] };
|
|
1178
|
+
const cells = rows.flatMap(
|
|
1179
|
+
(row) => cols.map((col) => ({ context: { row, col }, row, col, key: `${row}|${col}` }))
|
|
1180
|
+
);
|
|
1181
|
+
return { cells, rowHeaders: rows, colHeaders: cols };
|
|
1182
|
+
}
|
|
1183
|
+
const values = splitList(spec.attributes.values ?? spec.source);
|
|
1184
|
+
if (values.length === 0) return { cells: [] };
|
|
1185
|
+
return {
|
|
1186
|
+
cells: values.map((value, i) => ({ context: { value, name: value }, key: `${i}:${value}` }))
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
function transposeIterationValue(value) {
|
|
1190
|
+
if (value.kind !== "pivot") return value;
|
|
1191
|
+
return { ...value, values: value.cols ?? [], cols: value.values };
|
|
1192
|
+
}
|
|
1193
|
+
function cellScope2(as, cell, index) {
|
|
1194
|
+
return { ...cell.context, [as]: cell.context, index, row: cell.row, col: cell.col };
|
|
1195
|
+
}
|
|
1196
|
+
function escapeTableCell(markdown) {
|
|
1197
|
+
return markdown.replace(/\|/g, "\\|").replace(/\r?\n+/g, " ").trim();
|
|
1198
|
+
}
|
|
1199
|
+
function staticMarkdownFromValue(value, interpolate = defaultInterpolate) {
|
|
1200
|
+
const { attributes } = directivePartsFromValue(value);
|
|
1201
|
+
const spec = {
|
|
1202
|
+
kind: value.kind,
|
|
1203
|
+
layout: value.layout,
|
|
1204
|
+
template: value.template,
|
|
1205
|
+
as: value.as,
|
|
1206
|
+
attributes
|
|
1207
|
+
};
|
|
1208
|
+
const data = evaluateEmbedded(spec);
|
|
1209
|
+
if (data.cells.length === 0) return "";
|
|
1210
|
+
const cellMarkdown = (cell, index) => interpolate(value.template, cellScope2(value.as, cell, index)).trim();
|
|
1211
|
+
if (value.layout === "matrix" && data.rowHeaders?.length && data.colHeaders?.length) {
|
|
1212
|
+
const rowHeaders = data.rowHeaders;
|
|
1213
|
+
const colHeaders = data.colHeaders;
|
|
1214
|
+
const at = /* @__PURE__ */ new Map();
|
|
1215
|
+
data.cells.forEach((cell, i) => {
|
|
1216
|
+
if (cell.row != null && cell.col != null) {
|
|
1217
|
+
at.set(`${cell.row}|${cell.col}`, cellMarkdown(cell, i));
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
const headerRow = ["", ...colHeaders.map(escapeTableCell)];
|
|
1221
|
+
const dividerRow = headerRow.map(() => "---");
|
|
1222
|
+
const bodyRows = rowHeaders.map((row) => [
|
|
1223
|
+
escapeTableCell(row),
|
|
1224
|
+
...colHeaders.map((col) => escapeTableCell(at.get(`${row}|${col}`) ?? ""))
|
|
1225
|
+
]);
|
|
1226
|
+
return [headerRow, dividerRow, ...bodyRows].map((r) => `| ${r.join(" | ")} |`).join("\n");
|
|
1227
|
+
}
|
|
1228
|
+
return data.cells.map((cell, i) => cellMarkdown(cell, i)).join("\n\n");
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/metric-block/metric-block.tsx
|
|
1232
|
+
import { MetricCard } from "@elabs-ai/components-ui";
|
|
1233
|
+
var MetricBlock = MetricCard;
|
|
1234
|
+
|
|
1235
|
+
// src/markdown-editor/slash/insert-directive.ts
|
|
1236
|
+
import { editorViewCtx, parserCtx } from "@milkdown/kit/core";
|
|
1237
|
+
import { Fragment } from "@milkdown/kit/prose/model";
|
|
1238
|
+
import { TextSelection } from "@milkdown/kit/prose/state";
|
|
1239
|
+
|
|
1240
|
+
// src/markdown-editor/directive-nodes.ts
|
|
1241
|
+
import { $nodeSchema, $remark } from "@milkdown/kit/utils";
|
|
1242
|
+
import remarkDirective from "remark-directive";
|
|
1243
|
+
var directiveRemark = $remark("brandDirective", () => remarkDirective);
|
|
1244
|
+
var containerDirectiveSchema = $nodeSchema("brand_container_directive", () => ({
|
|
1245
|
+
content: "block+",
|
|
1246
|
+
group: "block",
|
|
1247
|
+
defining: true,
|
|
1248
|
+
attrs: {
|
|
1249
|
+
name: { default: "card" },
|
|
1250
|
+
attributes: { default: {} }
|
|
1251
|
+
},
|
|
1252
|
+
parseDOM: [
|
|
1253
|
+
{
|
|
1254
|
+
tag: "div[data-brand-directive]",
|
|
1255
|
+
getAttrs: (dom) => {
|
|
1256
|
+
if (typeof dom === "string") return false;
|
|
1257
|
+
return {
|
|
1258
|
+
name: dom.getAttribute("data-brand-directive") ?? "card",
|
|
1259
|
+
attributes: JSON.parse(dom.getAttribute("data-brand-attrs") ?? "{}")
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
],
|
|
1264
|
+
toDOM: (node) => {
|
|
1265
|
+
const name = String(node.attrs.name);
|
|
1266
|
+
const attrs = node.attrs.attributes ?? {};
|
|
1267
|
+
const heading = attrs.title || (name === "callout" ? attrs.type ?? "note" : "");
|
|
1268
|
+
const chrome = [
|
|
1269
|
+
"div",
|
|
1270
|
+
{
|
|
1271
|
+
"data-brand-directive": name,
|
|
1272
|
+
"data-brand-attrs": JSON.stringify(attrs),
|
|
1273
|
+
class: `brand-directive brand-directive--${name}`,
|
|
1274
|
+
"data-callout-type": name === "callout" ? attrs.type ?? "note" : null
|
|
1275
|
+
}
|
|
1276
|
+
];
|
|
1277
|
+
if (heading)
|
|
1278
|
+
chrome.push(["div", { class: "brand-directive__title", contenteditable: "false" }, heading]);
|
|
1279
|
+
chrome.push(["div", { class: "brand-directive__body" }, 0]);
|
|
1280
|
+
return chrome;
|
|
1281
|
+
},
|
|
1282
|
+
parseMarkdown: {
|
|
1283
|
+
match: (node) => node.type === "containerDirective",
|
|
1284
|
+
runner: (state, node, type) => {
|
|
1285
|
+
const d = node;
|
|
1286
|
+
state.openNode(type, { name: d.name ?? "card", attributes: d.attributes ?? {} });
|
|
1287
|
+
state.next(d.children ?? []);
|
|
1288
|
+
state.closeNode();
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
toMarkdown: {
|
|
1292
|
+
match: (node) => node.type.name === "brand_container_directive",
|
|
1293
|
+
runner: (state, node) => {
|
|
1294
|
+
state.openNode("containerDirective", void 0, {
|
|
1295
|
+
name: node.attrs.name,
|
|
1296
|
+
attributes: node.attrs.attributes
|
|
1297
|
+
});
|
|
1298
|
+
state.next(node.content);
|
|
1299
|
+
state.closeNode();
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}));
|
|
1303
|
+
var leafDirectiveSchema = $nodeSchema("brand_leaf_directive", () => ({
|
|
1304
|
+
group: "block",
|
|
1305
|
+
atom: true,
|
|
1306
|
+
isolating: true,
|
|
1307
|
+
attrs: {
|
|
1308
|
+
name: { default: "metric" },
|
|
1309
|
+
attributes: { default: {} }
|
|
1310
|
+
},
|
|
1311
|
+
parseDOM: [
|
|
1312
|
+
{
|
|
1313
|
+
tag: "div[data-brand-leaf]",
|
|
1314
|
+
getAttrs: (dom) => {
|
|
1315
|
+
if (typeof dom === "string") return false;
|
|
1316
|
+
return {
|
|
1317
|
+
name: dom.getAttribute("data-brand-leaf") ?? "metric",
|
|
1318
|
+
attributes: JSON.parse(dom.getAttribute("data-brand-attrs") ?? "{}")
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
],
|
|
1323
|
+
toDOM: (node) => {
|
|
1324
|
+
const name = String(node.attrs.name);
|
|
1325
|
+
const a = node.attrs.attributes ?? {};
|
|
1326
|
+
return [
|
|
1327
|
+
"div",
|
|
1328
|
+
{
|
|
1329
|
+
"data-brand-leaf": name,
|
|
1330
|
+
"data-brand-attrs": JSON.stringify(a),
|
|
1331
|
+
class: `brand-directive brand-directive--leaf brand-directive--${name}`,
|
|
1332
|
+
contenteditable: "false"
|
|
1333
|
+
},
|
|
1334
|
+
["div", { class: "brand-metric__label" }, a.label ?? name],
|
|
1335
|
+
["div", { class: "brand-metric__value" }, a.value ?? ""],
|
|
1336
|
+
...a.description ? [["div", { class: "brand-metric__desc" }, a.description]] : []
|
|
1337
|
+
];
|
|
1338
|
+
},
|
|
1339
|
+
parseMarkdown: {
|
|
1340
|
+
match: (node) => node.type === "leafDirective",
|
|
1341
|
+
runner: (state, node, type) => {
|
|
1342
|
+
const d = node;
|
|
1343
|
+
state.addNode(type, { name: d.name ?? "metric", attributes: d.attributes ?? {} });
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
toMarkdown: {
|
|
1347
|
+
match: (node) => node.type.name === "brand_leaf_directive",
|
|
1348
|
+
runner: (state, node) => {
|
|
1349
|
+
state.addNode("leafDirective", void 0, void 0, {
|
|
1350
|
+
name: node.attrs.name,
|
|
1351
|
+
attributes: node.attrs.attributes
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}));
|
|
1356
|
+
var directivePlugins = [
|
|
1357
|
+
directiveRemark,
|
|
1358
|
+
containerDirectiveSchema,
|
|
1359
|
+
leafDirectiveSchema
|
|
1360
|
+
].flat();
|
|
1361
|
+
|
|
1362
|
+
// src/markdown-editor/slash/insert-directive.ts
|
|
1363
|
+
var DIRECTIVE_DEFAULTS = {
|
|
1364
|
+
card: { title: "Title" },
|
|
1365
|
+
callout: { type: "info", title: "Note" },
|
|
1366
|
+
timeline: {},
|
|
1367
|
+
metric: { label: "Label", value: "0", description: "detail" },
|
|
1368
|
+
iterate: { as: "item", layout: "stacked" },
|
|
1369
|
+
pivot: { layout: "matrix" }
|
|
1370
|
+
};
|
|
1371
|
+
var DIRECTIVE_BODY_SEED = {
|
|
1372
|
+
iterate: "{{item.name}}",
|
|
1373
|
+
pivot: "{{cell}}"
|
|
1374
|
+
};
|
|
1375
|
+
var CONTAINER_DIRECTIVES = /* @__PURE__ */ new Set(["card", "callout", "timeline", "iterate", "pivot"]);
|
|
1376
|
+
var LEAF_DIRECTIVES = /* @__PURE__ */ new Set(["metric"]);
|
|
1377
|
+
function containerBody(schema, name) {
|
|
1378
|
+
const paragraph = schema.nodes.paragraph;
|
|
1379
|
+
if (name === "timeline") {
|
|
1380
|
+
const bulletList = schema.nodes.bullet_list;
|
|
1381
|
+
const listItem = schema.nodes.list_item;
|
|
1382
|
+
if (bulletList && listItem && paragraph) {
|
|
1383
|
+
const steps = ["(done) Step one", "(active) Step two", "(pending) Step three"];
|
|
1384
|
+
const items = steps.map(
|
|
1385
|
+
(text) => listItem.create(null, paragraph.create(null, schema.text(text)))
|
|
1386
|
+
);
|
|
1387
|
+
return Fragment.from(bulletList.create(null, items));
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const seed = DIRECTIVE_BODY_SEED[name];
|
|
1391
|
+
if (seed && paragraph) return Fragment.from(paragraph.create(null, schema.text(seed)));
|
|
1392
|
+
if (!paragraph) return Fragment.empty;
|
|
1393
|
+
return Fragment.from(paragraph.create());
|
|
1394
|
+
}
|
|
1395
|
+
function insertBrandDirective(name, containerType, leafType, schema, range) {
|
|
1396
|
+
return (state, dispatch) => {
|
|
1397
|
+
const isContainer = CONTAINER_DIRECTIVES.has(name);
|
|
1398
|
+
const isLeaf = LEAF_DIRECTIVES.has(name);
|
|
1399
|
+
if (!isContainer && !isLeaf) return false;
|
|
1400
|
+
const attributes = { ...DIRECTIVE_DEFAULTS[name] ?? {} };
|
|
1401
|
+
const node = isContainer ? containerType.create({ name, attributes }, containerBody(schema, name)) : leafType.create({ name, attributes });
|
|
1402
|
+
if (!node) return false;
|
|
1403
|
+
if (!dispatch) return true;
|
|
1404
|
+
const { from, to } = range ?? { from: state.selection.from, to: state.selection.to };
|
|
1405
|
+
const tr = state.tr.replaceRangeWith(from, to, node);
|
|
1406
|
+
const caret = isContainer ? Math.min(from + 2, tr.doc.content.size) : Math.min(from + node.nodeSize, tr.doc.content.size);
|
|
1407
|
+
const sel = TextSelection.near(tr.doc.resolve(caret), 1);
|
|
1408
|
+
tr.setSelection(sel).scrollIntoView();
|
|
1409
|
+
dispatch(tr);
|
|
1410
|
+
return true;
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
function insertBasicBlock(id, schema, range) {
|
|
1414
|
+
return (state, dispatch) => {
|
|
1415
|
+
const n = schema.nodes;
|
|
1416
|
+
const { from, to } = range ?? { from: state.selection.from, to: state.selection.to };
|
|
1417
|
+
const tr = state.tr;
|
|
1418
|
+
const replaceWith = (node, caretOffset) => {
|
|
1419
|
+
if (!node) return false;
|
|
1420
|
+
if (!dispatch) return true;
|
|
1421
|
+
tr.replaceRangeWith(from, to, node);
|
|
1422
|
+
const caret = Math.min(from + caretOffset, tr.doc.content.size);
|
|
1423
|
+
tr.setSelection(TextSelection.near(tr.doc.resolve(caret), 1)).scrollIntoView();
|
|
1424
|
+
dispatch(tr);
|
|
1425
|
+
return true;
|
|
1426
|
+
};
|
|
1427
|
+
switch (id) {
|
|
1428
|
+
case "heading":
|
|
1429
|
+
return replaceWith(n.heading?.create({ level: 2 }) ?? null, 1);
|
|
1430
|
+
case "bullet-list": {
|
|
1431
|
+
const li = n.list_item?.create(null, n.paragraph?.create());
|
|
1432
|
+
return replaceWith(li ? n.bullet_list?.create(null, li) ?? null : null, 2);
|
|
1433
|
+
}
|
|
1434
|
+
case "ordered-list": {
|
|
1435
|
+
const li = n.list_item?.create(null, n.paragraph?.create());
|
|
1436
|
+
return replaceWith(li ? n.ordered_list?.create(null, li) ?? null : null, 2);
|
|
1437
|
+
}
|
|
1438
|
+
case "quote":
|
|
1439
|
+
return replaceWith(n.blockquote?.create(null, n.paragraph?.create()) ?? null, 2);
|
|
1440
|
+
case "code":
|
|
1441
|
+
return replaceWith(n.code_block?.create() ?? null, 1);
|
|
1442
|
+
case "divider": {
|
|
1443
|
+
const hr = n.hr ?? n.horizontal_rule;
|
|
1444
|
+
if (!hr) return false;
|
|
1445
|
+
if (!dispatch) return true;
|
|
1446
|
+
const node = hr.create();
|
|
1447
|
+
tr.replaceRangeWith(from, to, node);
|
|
1448
|
+
const para = n.paragraph?.create();
|
|
1449
|
+
if (para) tr.insert(from + node.nodeSize, para);
|
|
1450
|
+
const caret = Math.min(from + node.nodeSize + 1, tr.doc.content.size);
|
|
1451
|
+
tr.setSelection(TextSelection.near(tr.doc.resolve(caret), 1)).scrollIntoView();
|
|
1452
|
+
dispatch(tr);
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1455
|
+
default:
|
|
1456
|
+
return false;
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
var CALC_FENCE_SEED = ["items = 3", "price = 4.50", "total = items * price"].join("\n");
|
|
1461
|
+
function insertCalcFence(schema, range, seed = CALC_FENCE_SEED) {
|
|
1462
|
+
return (state, dispatch) => {
|
|
1463
|
+
const codeBlock = schema.nodes.code_block;
|
|
1464
|
+
if (!codeBlock) return false;
|
|
1465
|
+
const node = seed ? codeBlock.create({ language: "calc" }, schema.text(seed)) : codeBlock.create({ language: "calc" });
|
|
1466
|
+
if (!node) return false;
|
|
1467
|
+
if (!dispatch) return true;
|
|
1468
|
+
const { from, to } = range ?? { from: state.selection.from, to: state.selection.to };
|
|
1469
|
+
const tr = state.tr.replaceRangeWith(from, to, node);
|
|
1470
|
+
const caret = Math.min(from + 1, tr.doc.content.size);
|
|
1471
|
+
tr.setSelection(TextSelection.near(tr.doc.resolve(caret), 1)).scrollIntoView();
|
|
1472
|
+
dispatch(tr);
|
|
1473
|
+
return true;
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function resolveCalcInsert(ctx2, range) {
|
|
1477
|
+
const view = ctx2.get(editorViewCtx);
|
|
1478
|
+
const command = insertCalcFence(view.state.schema, range);
|
|
1479
|
+
return command(view.state, view.dispatch.bind(view));
|
|
1480
|
+
}
|
|
1481
|
+
function resolveBrandInsert(ctx2, name, range) {
|
|
1482
|
+
const view = ctx2.get(editorViewCtx);
|
|
1483
|
+
const containerType = containerDirectiveSchema.type(ctx2);
|
|
1484
|
+
const leafType = leafDirectiveSchema.type(ctx2);
|
|
1485
|
+
const schema = view.state.schema;
|
|
1486
|
+
const command = insertBrandDirective(name, containerType, leafType, schema, range);
|
|
1487
|
+
return command(view.state, view.dispatch.bind(view));
|
|
1488
|
+
}
|
|
1489
|
+
function resolveBasicInsert(ctx2, id, range) {
|
|
1490
|
+
const view = ctx2.get(editorViewCtx);
|
|
1491
|
+
const command = insertBasicBlock(id, view.state.schema, range);
|
|
1492
|
+
return command(view.state, view.dispatch.bind(view));
|
|
1493
|
+
}
|
|
1494
|
+
function insertParsedMarkdown(ctx2, markdown, range) {
|
|
1495
|
+
try {
|
|
1496
|
+
const view = ctx2.get(editorViewCtx);
|
|
1497
|
+
const parse = ctx2.get(parserCtx);
|
|
1498
|
+
const parsed = parse(markdown);
|
|
1499
|
+
if (!parsed) return false;
|
|
1500
|
+
const { from, to } = range ?? { from: view.state.selection.from, to: view.state.selection.to };
|
|
1501
|
+
const tr = view.state.tr.replaceWith(from, to, parsed.content);
|
|
1502
|
+
const caret = Math.min(from + 2, tr.doc.content.size);
|
|
1503
|
+
tr.setSelection(TextSelection.near(tr.doc.resolve(caret), 1)).scrollIntoView();
|
|
1504
|
+
view.dispatch(tr);
|
|
1505
|
+
return true;
|
|
1506
|
+
} catch {
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function resolveGuidedIterationInsert(ctx2, kind, range, handler) {
|
|
1511
|
+
const seed = emptyBuilderValue(kind);
|
|
1512
|
+
handler({
|
|
1513
|
+
kind,
|
|
1514
|
+
template: seed.template,
|
|
1515
|
+
attributes: {},
|
|
1516
|
+
onSave: (template) => {
|
|
1517
|
+
const value = builderValueFromParts(kind, {}, template);
|
|
1518
|
+
insertParsedMarkdown(ctx2, serializeIterationDirective(value), range);
|
|
1519
|
+
},
|
|
1520
|
+
onSaveData: ({ attributes, template }) => {
|
|
1521
|
+
const value = builderValueFromParts(kind, attributes, template);
|
|
1522
|
+
insertParsedMarkdown(ctx2, serializeIterationDirective(value), range);
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// src/markdown-editor/slash/brand-slash-commands.ts
|
|
1528
|
+
import {
|
|
1529
|
+
Calculator,
|
|
1530
|
+
Grid3x3,
|
|
1531
|
+
Heading2,
|
|
1532
|
+
List,
|
|
1533
|
+
ListOrdered,
|
|
1534
|
+
Minus,
|
|
1535
|
+
Quote,
|
|
1536
|
+
Repeat2 as Repeat22,
|
|
1537
|
+
SquareCode
|
|
1538
|
+
} from "lucide-react";
|
|
1539
|
+
import { createElement } from "react";
|
|
1540
|
+
function glyph(icon) {
|
|
1541
|
+
return createElement(icon, { className: "size-4", "aria-hidden": "true" });
|
|
1542
|
+
}
|
|
1543
|
+
var DIRECTIVE_SNIPPET = {
|
|
1544
|
+
card: `:::card{title="Title"}
|
|
1545
|
+
Content
|
|
1546
|
+
:::`,
|
|
1547
|
+
callout: `:::callout{type="info" title="Note"}
|
|
1548
|
+
Message
|
|
1549
|
+
:::`,
|
|
1550
|
+
metric: `::metric{label="Label" value="0" description="detail"}`,
|
|
1551
|
+
timeline: `:::timeline
|
|
1552
|
+
- (done) Step one
|
|
1553
|
+
- (active) Step two
|
|
1554
|
+
- (pending) Step three
|
|
1555
|
+
:::`,
|
|
1556
|
+
iterate: `:::iterate{as="item" layout="stacked"}
|
|
1557
|
+
{{item.name}}
|
|
1558
|
+
:::`,
|
|
1559
|
+
pivot: `:::pivot{layout="matrix"}
|
|
1560
|
+
{{cell}}
|
|
1561
|
+
:::`
|
|
1562
|
+
};
|
|
1563
|
+
var GUIDED_DIRECTIVES = /* @__PURE__ */ new Set(["iterate", "pivot"]);
|
|
1564
|
+
function brandCommand(name, label, description, keywords, icon) {
|
|
1565
|
+
const command = {
|
|
1566
|
+
id: `brand-${name}`,
|
|
1567
|
+
label,
|
|
1568
|
+
group: "Brand blocks",
|
|
1569
|
+
description,
|
|
1570
|
+
keywords,
|
|
1571
|
+
icon,
|
|
1572
|
+
snippet: DIRECTIVE_SNIPPET[name],
|
|
1573
|
+
run: ({ ctx: ctx2, range }) => resolveBrandInsert(ctx2, name, range)
|
|
1574
|
+
};
|
|
1575
|
+
if (GUIDED_DIRECTIVES.has(name)) {
|
|
1576
|
+
command.guided = ({ ctx: ctx2, range }, handler) => resolveGuidedIterationInsert(ctx2, name, range, handler);
|
|
1577
|
+
}
|
|
1578
|
+
return command;
|
|
1579
|
+
}
|
|
1580
|
+
function basicCommand(id, label, description, keywords, icon) {
|
|
1581
|
+
return {
|
|
1582
|
+
id: `basic-${id}`,
|
|
1583
|
+
label,
|
|
1584
|
+
group: "Basic",
|
|
1585
|
+
description,
|
|
1586
|
+
keywords,
|
|
1587
|
+
icon: glyph(icon),
|
|
1588
|
+
run: ({ ctx: ctx2, range }) => resolveBasicInsert(ctx2, id, range)
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
var BRAND_SLASH_COMMANDS = [
|
|
1592
|
+
brandCommand(
|
|
1593
|
+
"card",
|
|
1594
|
+
"Card",
|
|
1595
|
+
"A titled content card",
|
|
1596
|
+
["card", "panel", "box", "section"],
|
|
1597
|
+
glyph(SquareCode)
|
|
1598
|
+
),
|
|
1599
|
+
brandCommand(
|
|
1600
|
+
"callout",
|
|
1601
|
+
"Callout",
|
|
1602
|
+
"A highlighted note / alert",
|
|
1603
|
+
["callout", "alert", "note", "info", "warning", "tip"],
|
|
1604
|
+
glyph(Quote)
|
|
1605
|
+
),
|
|
1606
|
+
brandCommand(
|
|
1607
|
+
"metric",
|
|
1608
|
+
"Metric",
|
|
1609
|
+
"A single KPI value",
|
|
1610
|
+
["metric", "kpi", "stat", "number", "value"],
|
|
1611
|
+
glyph(Minus)
|
|
1612
|
+
),
|
|
1613
|
+
brandCommand(
|
|
1614
|
+
"timeline",
|
|
1615
|
+
"Timeline",
|
|
1616
|
+
"A list of steps with status",
|
|
1617
|
+
["timeline", "steps", "milestones", "roadmap", "progress"],
|
|
1618
|
+
glyph(List)
|
|
1619
|
+
),
|
|
1620
|
+
brandCommand(
|
|
1621
|
+
"iterate",
|
|
1622
|
+
"Iterate",
|
|
1623
|
+
"Repeat a template per data row",
|
|
1624
|
+
["iterate", "repeat", "loop", "for-each", "foreach", "map", "list", "template"],
|
|
1625
|
+
glyph(Repeat22)
|
|
1626
|
+
),
|
|
1627
|
+
brandCommand(
|
|
1628
|
+
"pivot",
|
|
1629
|
+
"Pivot",
|
|
1630
|
+
"A row \xD7 column cross-tab",
|
|
1631
|
+
["pivot", "matrix", "cross-tab", "crosstab", "table", "grid"],
|
|
1632
|
+
glyph(Grid3x3)
|
|
1633
|
+
),
|
|
1634
|
+
{
|
|
1635
|
+
// Inserts a ```calc fence (not a `:::` directive), so it runs its own command
|
|
1636
|
+
// rather than `resolveBrandInsert`. Highlight + result inlays come from the
|
|
1637
|
+
// editor's calc hooks; the fence is harmless when none are wired.
|
|
1638
|
+
id: "brand-calc",
|
|
1639
|
+
label: "Calc",
|
|
1640
|
+
group: "Brand blocks",
|
|
1641
|
+
description: "A live calculation block",
|
|
1642
|
+
keywords: ["calc", "calculation", "math", "formula", "sum", "ledger", "budget"],
|
|
1643
|
+
icon: glyph(Calculator),
|
|
1644
|
+
snippet: ["```calc", CALC_FENCE_SEED, "```"].join("\n"),
|
|
1645
|
+
run: ({ ctx: ctx2, range }) => resolveCalcInsert(ctx2, range)
|
|
1646
|
+
},
|
|
1647
|
+
basicCommand("heading", "Heading", "Section heading", ["heading", "title", "h2"], Heading2),
|
|
1648
|
+
basicCommand(
|
|
1649
|
+
"bullet-list",
|
|
1650
|
+
"Bullet list",
|
|
1651
|
+
"An unordered list",
|
|
1652
|
+
["bullet", "list", "unordered", "ul"],
|
|
1653
|
+
List
|
|
1654
|
+
),
|
|
1655
|
+
basicCommand(
|
|
1656
|
+
"ordered-list",
|
|
1657
|
+
"Numbered list",
|
|
1658
|
+
"An ordered list",
|
|
1659
|
+
["numbered", "ordered", "list", "ol"],
|
|
1660
|
+
ListOrdered
|
|
1661
|
+
),
|
|
1662
|
+
basicCommand("quote", "Quote", "A block quotation", ["quote", "blockquote", "citation"], Quote),
|
|
1663
|
+
basicCommand(
|
|
1664
|
+
"code",
|
|
1665
|
+
"Code block",
|
|
1666
|
+
"A fenced code block",
|
|
1667
|
+
["code", "snippet", "pre", "fence"],
|
|
1668
|
+
SquareCode
|
|
1669
|
+
),
|
|
1670
|
+
basicCommand(
|
|
1671
|
+
"divider",
|
|
1672
|
+
"Divider",
|
|
1673
|
+
"A horizontal rule",
|
|
1674
|
+
["divider", "rule", "hr", "separator"],
|
|
1675
|
+
Minus
|
|
1676
|
+
)
|
|
1677
|
+
];
|
|
1678
|
+
function filterSlashCommands(commands, query) {
|
|
1679
|
+
const q = query.trim().toLowerCase();
|
|
1680
|
+
if (!q) return commands;
|
|
1681
|
+
return commands.filter((c) => {
|
|
1682
|
+
if (c.label.toLowerCase().includes(q)) return true;
|
|
1683
|
+
return (c.keywords ?? []).some((k) => k.toLowerCase().includes(q));
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
function groupSlashCommands(commands) {
|
|
1687
|
+
const order = [];
|
|
1688
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
1689
|
+
for (const c of commands) {
|
|
1690
|
+
const g = c.group ?? "Other";
|
|
1691
|
+
if (!byGroup.has(g)) {
|
|
1692
|
+
byGroup.set(g, []);
|
|
1693
|
+
order.push(g);
|
|
1694
|
+
}
|
|
1695
|
+
byGroup.get(g).push(c);
|
|
1696
|
+
}
|
|
1697
|
+
return order.map((group) => ({ group, commands: byGroup.get(group) }));
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/markdown-editor/slash/slash-menu.tsx
|
|
1701
|
+
import { cn as cn4 } from "@elabs-ai/components-ui/lib/cn";
|
|
1702
|
+
import { forwardRef as forwardRef4 } from "react";
|
|
1703
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1704
|
+
function slashOptionId(idPrefix, commandId) {
|
|
1705
|
+
return `${idPrefix}-${commandId}`;
|
|
1706
|
+
}
|
|
1707
|
+
var SlashMenu = forwardRef4(function SlashMenu2({
|
|
1708
|
+
commands,
|
|
1709
|
+
activeId,
|
|
1710
|
+
onSelect,
|
|
1711
|
+
idPrefix = "brand-slash",
|
|
1712
|
+
emptyLabel = "No matching blocks",
|
|
1713
|
+
className,
|
|
1714
|
+
...props
|
|
1715
|
+
}, ref) {
|
|
1716
|
+
const groups = groupSlashCommands(commands);
|
|
1717
|
+
return /* @__PURE__ */ jsx5(
|
|
1718
|
+
"div",
|
|
1719
|
+
{
|
|
1720
|
+
ref,
|
|
1721
|
+
role: "listbox",
|
|
1722
|
+
"aria-label": "Insert block",
|
|
1723
|
+
className: cn4(
|
|
1724
|
+
"max-h-[min(320px,60vh)] w-72 overflow-y-auto overflow-x-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
|
|
1725
|
+
className
|
|
1726
|
+
),
|
|
1727
|
+
...props,
|
|
1728
|
+
children: commands.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "px-2 py-6 text-center text-caption text-muted-foreground", children: emptyLabel }) : groups.map(({ group, commands: groupCommands }) => /* @__PURE__ */ jsxs4("div", { role: "group", "aria-label": group, className: "overflow-hidden p-1", children: [
|
|
1729
|
+
/* @__PURE__ */ jsx5("div", { className: "px-2 py-1.5 text-meta font-medium text-muted-foreground", children: group }),
|
|
1730
|
+
groupCommands.map((command) => {
|
|
1731
|
+
const selected = command.id === activeId;
|
|
1732
|
+
return /* @__PURE__ */ jsxs4(
|
|
1733
|
+
"div",
|
|
1734
|
+
{
|
|
1735
|
+
id: slashOptionId(idPrefix, command.id),
|
|
1736
|
+
role: "option",
|
|
1737
|
+
"aria-selected": selected,
|
|
1738
|
+
"data-selected": selected ? "true" : void 0,
|
|
1739
|
+
onMouseDown: (e) => {
|
|
1740
|
+
e.preventDefault();
|
|
1741
|
+
onSelect(command);
|
|
1742
|
+
},
|
|
1743
|
+
className: cn4(
|
|
1744
|
+
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast",
|
|
1745
|
+
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground"
|
|
1746
|
+
),
|
|
1747
|
+
children: [
|
|
1748
|
+
command.icon ? /* @__PURE__ */ jsx5("span", { className: "flex size-5 shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4", children: command.icon }) : null,
|
|
1749
|
+
/* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 flex-col", children: [
|
|
1750
|
+
/* @__PURE__ */ jsx5("span", { className: "truncate", children: command.label }),
|
|
1751
|
+
command.description ? /* @__PURE__ */ jsx5("span", { className: "truncate text-meta text-muted-foreground", children: command.description }) : null
|
|
1752
|
+
] })
|
|
1753
|
+
]
|
|
1754
|
+
},
|
|
1755
|
+
command.id
|
|
1756
|
+
);
|
|
1757
|
+
})
|
|
1758
|
+
] }, group))
|
|
1759
|
+
}
|
|
1760
|
+
);
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
// src/markdown-editor/slash/brand-slash-plugin.ts
|
|
1764
|
+
import { $prose as $prose3 } from "@milkdown/kit/utils";
|
|
1765
|
+
import { Plugin as Plugin3, PluginKey as PluginKey3 } from "@milkdown/kit/prose/state";
|
|
1766
|
+
import { DecorationSet as DecorationSet2 } from "@milkdown/kit/prose/view";
|
|
1767
|
+
|
|
1768
|
+
// src/markdown-editor/slash/shortcut.ts
|
|
1769
|
+
var DEFAULT_SLASH_SHORTCUT = "Mod-Shift-O";
|
|
1770
|
+
function matchesKeyboardEvent(shortcut, event) {
|
|
1771
|
+
const parts = shortcut.split("-");
|
|
1772
|
+
const keyPart = parts[parts.length - 1] ?? "";
|
|
1773
|
+
const modifiers = new Set(parts.slice(0, -1).map((m) => m.toLowerCase()));
|
|
1774
|
+
const wantsMod = modifiers.has("mod");
|
|
1775
|
+
const wantsShift = modifiers.has("shift");
|
|
1776
|
+
const wantsAlt = modifiers.has("alt");
|
|
1777
|
+
if (wantsMod && !(event.metaKey || event.ctrlKey)) return false;
|
|
1778
|
+
if (!wantsMod && (event.metaKey || event.ctrlKey)) return false;
|
|
1779
|
+
if (wantsShift && !event.shiftKey) return false;
|
|
1780
|
+
if (!wantsShift && event.shiftKey) return false;
|
|
1781
|
+
if (wantsAlt && !event.altKey) return false;
|
|
1782
|
+
if (!wantsAlt && event.altKey) return false;
|
|
1783
|
+
return event.key.toLowerCase() === keyPart.toLowerCase();
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// src/markdown-editor/slash/brand-slash-plugin.ts
|
|
1787
|
+
var CLOSED = {
|
|
1788
|
+
active: false,
|
|
1789
|
+
from: 0,
|
|
1790
|
+
query: "",
|
|
1791
|
+
index: 0,
|
|
1792
|
+
triggered: "char"
|
|
1793
|
+
};
|
|
1794
|
+
var slashPluginKey = new PluginKey3("brand-slash");
|
|
1795
|
+
function createSlashController(commands, trigger = "/", shortcut, getIterationEditHandler) {
|
|
1796
|
+
const controller = {
|
|
1797
|
+
commands,
|
|
1798
|
+
trigger,
|
|
1799
|
+
shortcut,
|
|
1800
|
+
getIterationEditHandler,
|
|
1801
|
+
getCtx: () => {
|
|
1802
|
+
if (!controller._ctx) {
|
|
1803
|
+
throw new Error("Brand slash controller used before the editor Ctx was captured.");
|
|
1804
|
+
}
|
|
1805
|
+
return controller._ctx;
|
|
1806
|
+
},
|
|
1807
|
+
openMenu: (view) => {
|
|
1808
|
+
const pos = view.state.selection.from;
|
|
1809
|
+
view.dispatch(
|
|
1810
|
+
view.state.tr.setMeta(slashPluginKey, {
|
|
1811
|
+
_open: true,
|
|
1812
|
+
from: pos,
|
|
1813
|
+
triggered: "shortcut"
|
|
1814
|
+
})
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
return controller;
|
|
1819
|
+
}
|
|
1820
|
+
function slashRange(state) {
|
|
1821
|
+
return { from: state.from, to: state.from + 1 + state.query.length };
|
|
1822
|
+
}
|
|
1823
|
+
function runSlashCommand(view, controller, state, command) {
|
|
1824
|
+
const range = state.triggered === "shortcut" ? { from: state.from, to: state.from + state.query.length } : slashRange(state);
|
|
1825
|
+
const iterationHandler = controller.getIterationEditHandler?.();
|
|
1826
|
+
if (command.guided && iterationHandler) {
|
|
1827
|
+
view.dispatch(view.state.tr.delete(range.from, range.to));
|
|
1828
|
+
command.guided({ ctx: controller.getCtx(), range: null }, iterationHandler);
|
|
1829
|
+
} else {
|
|
1830
|
+
command.run({ ctx: controller.getCtx(), range });
|
|
1831
|
+
}
|
|
1832
|
+
if (slashPluginKey.getState(view.state)?.active) {
|
|
1833
|
+
view.dispatch(view.state.tr.setMeta(slashPluginKey, "close"));
|
|
1834
|
+
}
|
|
1835
|
+
view.focus();
|
|
1836
|
+
}
|
|
1837
|
+
function triggerAllowed(doc, triggerPos) {
|
|
1838
|
+
const $pos = doc.resolve(triggerPos);
|
|
1839
|
+
if (!$pos.parent.isTextblock) return false;
|
|
1840
|
+
if ($pos.parent.type.spec.code) return false;
|
|
1841
|
+
if ($pos.parentOffset === 0) return true;
|
|
1842
|
+
const before = $pos.parent.textBetween(Math.max(0, $pos.parentOffset - 1), $pos.parentOffset);
|
|
1843
|
+
return /\s/.test(before);
|
|
1844
|
+
}
|
|
1845
|
+
function nextState(prev, tr, trigger) {
|
|
1846
|
+
const meta = tr.getMeta(slashPluginKey);
|
|
1847
|
+
if (meta === "close") return CLOSED;
|
|
1848
|
+
if (meta && typeof meta === "object") {
|
|
1849
|
+
if (meta._open) {
|
|
1850
|
+
return {
|
|
1851
|
+
active: true,
|
|
1852
|
+
from: meta.from,
|
|
1853
|
+
query: "",
|
|
1854
|
+
index: 0,
|
|
1855
|
+
triggered: "shortcut"
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
return prev.active ? { ...prev, ...meta } : prev;
|
|
1859
|
+
}
|
|
1860
|
+
const sel = tr.selection;
|
|
1861
|
+
if (!sel.empty) return prev.active ? CLOSED : prev;
|
|
1862
|
+
const pos = sel.from;
|
|
1863
|
+
const $pos = tr.doc.resolve(pos);
|
|
1864
|
+
if (prev.active) {
|
|
1865
|
+
if (pos <= prev.from) return CLOSED;
|
|
1866
|
+
const start = tr.doc.resolve(prev.from);
|
|
1867
|
+
if (start.parent !== $pos.parent) return CLOSED;
|
|
1868
|
+
if (prev.triggered === "shortcut") {
|
|
1869
|
+
const query2 = $pos.parent.textBetween(start.parentOffset, $pos.parentOffset);
|
|
1870
|
+
if (/\s/.test(query2)) return CLOSED;
|
|
1871
|
+
const index2 = query2 === prev.query ? prev.index : 0;
|
|
1872
|
+
return { active: true, from: prev.from, query: query2, index: index2, triggered: "shortcut" };
|
|
1873
|
+
}
|
|
1874
|
+
const triggerChar = start.parent.textBetween(start.parentOffset, start.parentOffset + 1);
|
|
1875
|
+
if (triggerChar !== trigger) return CLOSED;
|
|
1876
|
+
const query = $pos.parent.textBetween(start.parentOffset + 1, $pos.parentOffset);
|
|
1877
|
+
if (/\s/.test(query)) return CLOSED;
|
|
1878
|
+
const index = query === prev.query ? prev.index : 0;
|
|
1879
|
+
return { active: true, from: prev.from, query, index, triggered: "char" };
|
|
1880
|
+
}
|
|
1881
|
+
if (!tr.docChanged) return prev;
|
|
1882
|
+
const justTyped = tr.doc.textBetween(Math.max(0, pos - 1), pos);
|
|
1883
|
+
if (justTyped !== trigger) return prev;
|
|
1884
|
+
const triggerPos = pos - 1;
|
|
1885
|
+
if (!triggerAllowed(tr.doc, triggerPos)) return prev;
|
|
1886
|
+
return { active: true, from: triggerPos, query: "", index: 0, triggered: "char" };
|
|
1887
|
+
}
|
|
1888
|
+
function brandSlashPlugin(options) {
|
|
1889
|
+
const { widgetFactory, widgetComponent, controller } = options;
|
|
1890
|
+
const trigger = controller.trigger || "/";
|
|
1891
|
+
return $prose3((ctx2) => {
|
|
1892
|
+
controller._ctx = ctx2;
|
|
1893
|
+
return new Plugin3({
|
|
1894
|
+
key: slashPluginKey,
|
|
1895
|
+
state: {
|
|
1896
|
+
init: () => CLOSED,
|
|
1897
|
+
apply: (tr, value) => nextState(value, tr, trigger)
|
|
1898
|
+
},
|
|
1899
|
+
props: {
|
|
1900
|
+
decorations: (state) => {
|
|
1901
|
+
const s = slashPluginKey.getState(state);
|
|
1902
|
+
if (!s?.active) return DecorationSet2.empty;
|
|
1903
|
+
const factory = widgetFactory({ component: widgetComponent, as: "span" });
|
|
1904
|
+
const anchor = s.triggered === "shortcut" ? s.from : s.from + 1;
|
|
1905
|
+
const decoration = factory(anchor, {
|
|
1906
|
+
side: 1,
|
|
1907
|
+
ignoreSelection: true,
|
|
1908
|
+
key: `brand-slash:${s.from}:${s.query}:${s.index}`
|
|
1909
|
+
});
|
|
1910
|
+
return DecorationSet2.create(state.doc, [decoration]);
|
|
1911
|
+
},
|
|
1912
|
+
handleKeyDown: (view, event) => {
|
|
1913
|
+
if (controller.shortcut && matchesKeyboardEvent(controller.shortcut, event)) {
|
|
1914
|
+
const pos = view.state.selection.from;
|
|
1915
|
+
view.dispatch(
|
|
1916
|
+
view.state.tr.setMeta(slashPluginKey, {
|
|
1917
|
+
_open: true,
|
|
1918
|
+
from: pos,
|
|
1919
|
+
triggered: "shortcut"
|
|
1920
|
+
})
|
|
1921
|
+
);
|
|
1922
|
+
event.preventDefault();
|
|
1923
|
+
return true;
|
|
1924
|
+
}
|
|
1925
|
+
const s = slashPluginKey.getState(view.state);
|
|
1926
|
+
if (!s?.active) return false;
|
|
1927
|
+
const filtered = filterSlashCommands(controller.commands, s.query);
|
|
1928
|
+
if (event.key === "Escape") {
|
|
1929
|
+
view.dispatch(view.state.tr.setMeta(slashPluginKey, "close"));
|
|
1930
|
+
event.preventDefault();
|
|
1931
|
+
return true;
|
|
1932
|
+
}
|
|
1933
|
+
if (filtered.length === 0) return false;
|
|
1934
|
+
if (event.key === "ArrowDown") {
|
|
1935
|
+
const index = (s.index + 1) % filtered.length;
|
|
1936
|
+
view.dispatch(view.state.tr.setMeta(slashPluginKey, { index }));
|
|
1937
|
+
event.preventDefault();
|
|
1938
|
+
return true;
|
|
1939
|
+
}
|
|
1940
|
+
if (event.key === "ArrowUp") {
|
|
1941
|
+
const index = (s.index - 1 + filtered.length) % filtered.length;
|
|
1942
|
+
view.dispatch(view.state.tr.setMeta(slashPluginKey, { index }));
|
|
1943
|
+
event.preventDefault();
|
|
1944
|
+
return true;
|
|
1945
|
+
}
|
|
1946
|
+
if (event.key === "Enter") {
|
|
1947
|
+
const command = filtered[Math.min(s.index, filtered.length - 1)];
|
|
1948
|
+
if (command) {
|
|
1949
|
+
runSlashCommand(view, controller, s, command);
|
|
1950
|
+
event.preventDefault();
|
|
1951
|
+
return true;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
if (event.key === "Tab") {
|
|
1955
|
+
const command = filtered[Math.min(s.index, filtered.length - 1)];
|
|
1956
|
+
if (command) {
|
|
1957
|
+
runSlashCommand(view, controller, s, command);
|
|
1958
|
+
event.preventDefault();
|
|
1959
|
+
return true;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// src/markdown-editor/slash/slash-widget.tsx
|
|
1970
|
+
import { useWidgetViewContext } from "@prosemirror-adapter/react";
|
|
1971
|
+
import { useLayoutEffect, useRef as useRef2 } from "react";
|
|
1972
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1973
|
+
var ID_PREFIX = "brand-slash";
|
|
1974
|
+
function createSlashWidget(controller) {
|
|
1975
|
+
function SlashWidget() {
|
|
1976
|
+
const { view } = useWidgetViewContext();
|
|
1977
|
+
const wrapperRef = useRef2(null);
|
|
1978
|
+
const state = slashPluginKey.getState(view.state);
|
|
1979
|
+
const query = state?.query ?? "";
|
|
1980
|
+
const filtered = filterSlashCommands(controller.commands, query);
|
|
1981
|
+
const activeIndex = state ? Math.min(state.index, Math.max(0, filtered.length - 1)) : 0;
|
|
1982
|
+
const active = filtered[activeIndex];
|
|
1983
|
+
const activeId = active?.id;
|
|
1984
|
+
useLayoutEffect(() => {
|
|
1985
|
+
const dom = view.dom;
|
|
1986
|
+
const listEl = wrapperRef.current?.querySelector('[role="listbox"]');
|
|
1987
|
+
if (listEl && !listEl.id) listEl.id = `${ID_PREFIX}-listbox`;
|
|
1988
|
+
dom.setAttribute("aria-expanded", "true");
|
|
1989
|
+
if (listEl) dom.setAttribute("aria-controls", listEl.id);
|
|
1990
|
+
if (activeId) dom.setAttribute("aria-activedescendant", slashOptionId(ID_PREFIX, activeId));
|
|
1991
|
+
else dom.removeAttribute("aria-activedescendant");
|
|
1992
|
+
return () => {
|
|
1993
|
+
dom.removeAttribute("aria-expanded");
|
|
1994
|
+
dom.removeAttribute("aria-controls");
|
|
1995
|
+
dom.removeAttribute("aria-activedescendant");
|
|
1996
|
+
};
|
|
1997
|
+
}, [view, activeId]);
|
|
1998
|
+
useLayoutEffect(() => {
|
|
1999
|
+
if (!activeId) return;
|
|
2000
|
+
const el = wrapperRef.current?.querySelector(
|
|
2001
|
+
`#${CSS.escape(slashOptionId(ID_PREFIX, activeId))}`
|
|
2002
|
+
);
|
|
2003
|
+
el?.scrollIntoView({ block: "nearest" });
|
|
2004
|
+
}, [activeId]);
|
|
2005
|
+
const onSelect = (command) => {
|
|
2006
|
+
if (!state) return;
|
|
2007
|
+
runSlashCommand(view, controller, state, command);
|
|
2008
|
+
};
|
|
2009
|
+
return (
|
|
2010
|
+
// The widget anchor is zero-width inline; the menu floats below the caret.
|
|
2011
|
+
/* @__PURE__ */ jsx6(
|
|
2012
|
+
"span",
|
|
2013
|
+
{
|
|
2014
|
+
ref: wrapperRef,
|
|
2015
|
+
contentEditable: false,
|
|
2016
|
+
className: "brand-slash-anchor relative inline-block h-0 w-0 align-baseline",
|
|
2017
|
+
children: /* @__PURE__ */ jsx6("span", { className: "absolute left-0 top-1 z-50 block", children: /* @__PURE__ */ jsx6(
|
|
2018
|
+
SlashMenu,
|
|
2019
|
+
{
|
|
2020
|
+
commands: filtered,
|
|
2021
|
+
activeId,
|
|
2022
|
+
onSelect,
|
|
2023
|
+
idPrefix: ID_PREFIX
|
|
2024
|
+
}
|
|
2025
|
+
) })
|
|
2026
|
+
}
|
|
2027
|
+
)
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
return SlashWidget;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// src/markdown-editor/slash/index.ts
|
|
2034
|
+
function brandSlashViewPlugins(widgetFactory, options = {}) {
|
|
2035
|
+
const commands = options.commands ?? BRAND_SLASH_COMMANDS;
|
|
2036
|
+
const shortcut = "shortcut" in options ? options.shortcut : DEFAULT_SLASH_SHORTCUT;
|
|
2037
|
+
const controller = createSlashController(
|
|
2038
|
+
commands,
|
|
2039
|
+
options.trigger ?? "/",
|
|
2040
|
+
shortcut,
|
|
2041
|
+
options.getIterationEditHandler
|
|
2042
|
+
);
|
|
2043
|
+
const widgetComponent = createSlashWidget(controller);
|
|
2044
|
+
return [brandSlashPlugin({ widgetFactory, widgetComponent, controller })];
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// src/markdown-editor/markdown-editor.tsx
|
|
2048
|
+
import "@milkdown/kit/prose/view/style/prosemirror.css";
|
|
2049
|
+
import {
|
|
2050
|
+
Editor,
|
|
2051
|
+
defaultValueCtx,
|
|
2052
|
+
editorViewCtx as editorViewCtx3,
|
|
2053
|
+
editorViewOptionsCtx,
|
|
2054
|
+
parserCtx as parserCtx3,
|
|
2055
|
+
rootCtx,
|
|
2056
|
+
serializerCtx as serializerCtx2
|
|
2057
|
+
} from "@milkdown/kit/core";
|
|
2058
|
+
import { commonmark } from "@milkdown/kit/preset/commonmark";
|
|
2059
|
+
import { gfm } from "@milkdown/kit/preset/gfm";
|
|
2060
|
+
import { history } from "@milkdown/kit/plugin/history";
|
|
2061
|
+
import { listener, listenerCtx } from "@milkdown/kit/plugin/listener";
|
|
2062
|
+
import { TextSelection as TextSelection3 } from "@milkdown/kit/prose/state";
|
|
2063
|
+
import { getMarkdown, replaceAll } from "@milkdown/kit/utils";
|
|
2064
|
+
import { cn as cn8 } from "@elabs-ai/components-ui/lib/cn";
|
|
2065
|
+
import {
|
|
2066
|
+
ProsemirrorAdapterProvider,
|
|
2067
|
+
useNodeViewFactory,
|
|
2068
|
+
usePluginViewFactory,
|
|
2069
|
+
useWidgetViewFactory
|
|
2070
|
+
} from "@prosemirror-adapter/react";
|
|
2071
|
+
import {
|
|
2072
|
+
forwardRef as forwardRef6,
|
|
2073
|
+
useContext as useContext5,
|
|
2074
|
+
useEffect as useEffect5,
|
|
2075
|
+
useImperativeHandle as useImperativeHandle2,
|
|
2076
|
+
useRef as useRef7
|
|
2077
|
+
} from "react";
|
|
2078
|
+
|
|
2079
|
+
// src/markdown-editor/completions/completions-prose.ts
|
|
2080
|
+
import { Plugin as Plugin4, PluginKey as PluginKey4 } from "@milkdown/kit/prose/state";
|
|
2081
|
+
import { DecorationSet as DecorationSet3 } from "@milkdown/kit/prose/view";
|
|
2082
|
+
import { $prose as $prose4 } from "@milkdown/kit/utils";
|
|
2083
|
+
var CLOSED2 = {
|
|
2084
|
+
active: false,
|
|
2085
|
+
from: 0,
|
|
2086
|
+
triggerChar: "",
|
|
2087
|
+
query: "",
|
|
2088
|
+
items: [],
|
|
2089
|
+
index: 0,
|
|
2090
|
+
requestId: 0
|
|
2091
|
+
};
|
|
2092
|
+
var completionsPluginKey = new PluginKey4("brand-completions");
|
|
2093
|
+
function nextCompletionState(prev, tr, triggerCharacters) {
|
|
2094
|
+
const meta = tr.getMeta(completionsPluginKey);
|
|
2095
|
+
if (meta === "close") return CLOSED2;
|
|
2096
|
+
if (meta && typeof meta === "object") {
|
|
2097
|
+
if (meta.type === "items") {
|
|
2098
|
+
return prev.active && meta.requestId === prev.requestId ? { ...prev, items: meta.items } : prev;
|
|
2099
|
+
}
|
|
2100
|
+
if (meta.type === "nav") {
|
|
2101
|
+
return prev.active ? { ...prev, index: meta.index } : prev;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
const sel = tr.selection;
|
|
2105
|
+
if (!sel.empty) return prev.active ? CLOSED2 : prev;
|
|
2106
|
+
const pos = sel.from;
|
|
2107
|
+
const isPureSingleCharInsert = tr.docChanged && tr.steps.length === 1 && tr.doc.content.size === tr.before.content.size + 1;
|
|
2108
|
+
if (isPureSingleCharInsert) {
|
|
2109
|
+
const justTyped = tr.doc.textBetween(Math.max(0, pos - 1), pos);
|
|
2110
|
+
if (triggerCharacters.includes(justTyped)) {
|
|
2111
|
+
return {
|
|
2112
|
+
active: true,
|
|
2113
|
+
from: pos - 1,
|
|
2114
|
+
triggerChar: justTyped,
|
|
2115
|
+
query: "",
|
|
2116
|
+
items: [],
|
|
2117
|
+
index: 0,
|
|
2118
|
+
requestId: prev.requestId + 1
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
if (!prev.active) return prev;
|
|
2123
|
+
if (pos <= prev.from) return CLOSED2;
|
|
2124
|
+
const start = tr.doc.resolve(prev.from);
|
|
2125
|
+
const $pos = tr.doc.resolve(pos);
|
|
2126
|
+
if (start.parent !== $pos.parent) return CLOSED2;
|
|
2127
|
+
const query = $pos.parent.textBetween(start.parentOffset + 1, $pos.parentOffset);
|
|
2128
|
+
if (/\s/.test(query)) return CLOSED2;
|
|
2129
|
+
if (query === prev.query) return prev;
|
|
2130
|
+
return { ...prev, query, index: 0, requestId: prev.requestId + 1 };
|
|
2131
|
+
}
|
|
2132
|
+
function buildCompletionContext(doc, state) {
|
|
2133
|
+
const resolved = doc.resolve(state.from);
|
|
2134
|
+
const blockStart = resolved.pos - resolved.parentOffset;
|
|
2135
|
+
const lineText = resolved.parent.textContent;
|
|
2136
|
+
const caretPos = state.from + 1 + state.query.length;
|
|
2137
|
+
const column = caretPos - blockStart + 1;
|
|
2138
|
+
return { source: lineText, line: 1, column, lineText };
|
|
2139
|
+
}
|
|
2140
|
+
function completionReplaceRange(doc, state, item) {
|
|
2141
|
+
const resolved = doc.resolve(state.from);
|
|
2142
|
+
const blockStart = resolved.pos - resolved.parentOffset;
|
|
2143
|
+
const lineText = resolved.parent.textContent;
|
|
2144
|
+
const caretPos = state.from + 1 + state.query.length;
|
|
2145
|
+
const column = caretPos - blockStart + 1;
|
|
2146
|
+
const range = resolveReplaceRange(item, { lineNumber: 1, column }, lineText, [state.triggerChar]);
|
|
2147
|
+
return {
|
|
2148
|
+
from: blockStart + (range.startColumn - 1),
|
|
2149
|
+
to: blockStart + (range.endColumn - 1)
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
function insertCompletionItem(view, state, item) {
|
|
2153
|
+
const { from, to } = completionReplaceRange(view.state.doc, state, item);
|
|
2154
|
+
view.dispatch(
|
|
2155
|
+
view.state.tr.insertText(item.insertText, from, to).setMeta(completionsPluginKey, "close")
|
|
2156
|
+
);
|
|
2157
|
+
view.focus();
|
|
2158
|
+
}
|
|
2159
|
+
function completionsProsePlugin(options) {
|
|
2160
|
+
const { widgetFactory, widgetComponent, getProviders } = options;
|
|
2161
|
+
return $prose4(() => {
|
|
2162
|
+
return new Plugin4({
|
|
2163
|
+
key: completionsPluginKey,
|
|
2164
|
+
state: {
|
|
2165
|
+
init: () => CLOSED2,
|
|
2166
|
+
apply: (tr, value) => {
|
|
2167
|
+
const providers = getProviders() ?? [];
|
|
2168
|
+
const triggerCharacters = Array.from(
|
|
2169
|
+
new Set(providers.flatMap((p) => p.triggerCharacters ?? []))
|
|
2170
|
+
);
|
|
2171
|
+
return nextCompletionState(value, tr, triggerCharacters);
|
|
2172
|
+
}
|
|
2173
|
+
},
|
|
2174
|
+
props: {
|
|
2175
|
+
decorations: (state) => {
|
|
2176
|
+
const s = completionsPluginKey.getState(state);
|
|
2177
|
+
if (!s?.active) return DecorationSet3.empty;
|
|
2178
|
+
const factory = widgetFactory({ component: widgetComponent, as: "span" });
|
|
2179
|
+
const anchor = s.from + 1 + s.query.length;
|
|
2180
|
+
const decoration = factory(anchor, {
|
|
2181
|
+
side: 1,
|
|
2182
|
+
ignoreSelection: true,
|
|
2183
|
+
key: `brand-completions:${String(s.from)}:${s.query}:${String(s.items.length)}:${String(s.index)}`
|
|
2184
|
+
});
|
|
2185
|
+
return DecorationSet3.create(state.doc, [decoration]);
|
|
2186
|
+
},
|
|
2187
|
+
handleKeyDown: (view, event) => {
|
|
2188
|
+
const s = completionsPluginKey.getState(view.state);
|
|
2189
|
+
if (!s?.active) return false;
|
|
2190
|
+
if (event.key === "Escape") {
|
|
2191
|
+
view.dispatch(view.state.tr.setMeta(completionsPluginKey, "close"));
|
|
2192
|
+
event.preventDefault();
|
|
2193
|
+
return true;
|
|
2194
|
+
}
|
|
2195
|
+
if (s.items.length === 0) return false;
|
|
2196
|
+
if (event.key === "ArrowDown") {
|
|
2197
|
+
const index = (s.index + 1) % s.items.length;
|
|
2198
|
+
view.dispatch(view.state.tr.setMeta(completionsPluginKey, { type: "nav", index }));
|
|
2199
|
+
event.preventDefault();
|
|
2200
|
+
return true;
|
|
2201
|
+
}
|
|
2202
|
+
if (event.key === "ArrowUp") {
|
|
2203
|
+
const index = (s.index - 1 + s.items.length) % s.items.length;
|
|
2204
|
+
view.dispatch(view.state.tr.setMeta(completionsPluginKey, { type: "nav", index }));
|
|
2205
|
+
event.preventDefault();
|
|
2206
|
+
return true;
|
|
2207
|
+
}
|
|
2208
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
2209
|
+
const item = s.items[Math.min(s.index, s.items.length - 1)];
|
|
2210
|
+
if (item) {
|
|
2211
|
+
insertCompletionItem(view, s, item);
|
|
2212
|
+
event.preventDefault();
|
|
2213
|
+
return true;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
},
|
|
2219
|
+
view: () => ({
|
|
2220
|
+
update: (view, prevEditorState) => {
|
|
2221
|
+
const state = completionsPluginKey.getState(view.state);
|
|
2222
|
+
const prev = completionsPluginKey.getState(prevEditorState);
|
|
2223
|
+
if (!state?.active) return;
|
|
2224
|
+
if (prev?.active && prev.requestId === state.requestId) return;
|
|
2225
|
+
const providers = (getProviders() ?? []).filter(
|
|
2226
|
+
(p) => !p.triggerCharacters || p.triggerCharacters.includes(state.triggerChar)
|
|
2227
|
+
);
|
|
2228
|
+
if (providers.length === 0) return;
|
|
2229
|
+
const ctx2 = buildCompletionContext(view.state.doc, state);
|
|
2230
|
+
const requestId = state.requestId;
|
|
2231
|
+
collectCompletions(providers, ctx2).then((matches) => {
|
|
2232
|
+
if (view.isDestroyed) return;
|
|
2233
|
+
const current = completionsPluginKey.getState(view.state);
|
|
2234
|
+
if (!current?.active || current.requestId !== requestId) return;
|
|
2235
|
+
view.dispatch(
|
|
2236
|
+
view.state.tr.setMeta(completionsPluginKey, {
|
|
2237
|
+
type: "items",
|
|
2238
|
+
items: matches.map((m) => m.item),
|
|
2239
|
+
requestId
|
|
2240
|
+
})
|
|
2241
|
+
);
|
|
2242
|
+
}).catch(() => {
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
})
|
|
2246
|
+
});
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// src/markdown-editor/completions/completions-widget.tsx
|
|
2251
|
+
import { useWidgetViewContext as useWidgetViewContext2 } from "@prosemirror-adapter/react";
|
|
2252
|
+
import { useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
|
|
2253
|
+
|
|
2254
|
+
// src/markdown-editor/completions/completions-menu.tsx
|
|
2255
|
+
import { cn as cn5 } from "@elabs-ai/components-ui/lib/cn";
|
|
2256
|
+
import { forwardRef as forwardRef5 } from "react";
|
|
2257
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2258
|
+
function completionOptionId(idPrefix, index) {
|
|
2259
|
+
return `${idPrefix}-${index}`;
|
|
2260
|
+
}
|
|
2261
|
+
var CompletionMenu = forwardRef5(
|
|
2262
|
+
function CompletionMenu2({
|
|
2263
|
+
items,
|
|
2264
|
+
activeIndex,
|
|
2265
|
+
onSelect,
|
|
2266
|
+
idPrefix = "brand-completions",
|
|
2267
|
+
emptyLabel = "No suggestions",
|
|
2268
|
+
className,
|
|
2269
|
+
...props
|
|
2270
|
+
}, ref) {
|
|
2271
|
+
return /* @__PURE__ */ jsx7(
|
|
2272
|
+
"div",
|
|
2273
|
+
{
|
|
2274
|
+
ref,
|
|
2275
|
+
role: "listbox",
|
|
2276
|
+
"aria-label": "Suggestions",
|
|
2277
|
+
className: cn5(
|
|
2278
|
+
"max-h-[min(280px,50vh)] w-64 overflow-y-auto overflow-x-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
|
|
2279
|
+
className
|
|
2280
|
+
),
|
|
2281
|
+
...props,
|
|
2282
|
+
children: items.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "px-2 py-3 text-center text-caption text-muted-foreground", children: emptyLabel }) : items.map((item, index) => {
|
|
2283
|
+
const selected = index === activeIndex;
|
|
2284
|
+
return /* @__PURE__ */ jsx7(
|
|
2285
|
+
"div",
|
|
2286
|
+
{
|
|
2287
|
+
id: completionOptionId(idPrefix, index),
|
|
2288
|
+
role: "option",
|
|
2289
|
+
"aria-selected": selected,
|
|
2290
|
+
"data-selected": selected ? "true" : void 0,
|
|
2291
|
+
onMouseDown: (e) => {
|
|
2292
|
+
e.preventDefault();
|
|
2293
|
+
onSelect(index);
|
|
2294
|
+
},
|
|
2295
|
+
className: cn5(
|
|
2296
|
+
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast",
|
|
2297
|
+
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground"
|
|
2298
|
+
),
|
|
2299
|
+
children: /* @__PURE__ */ jsxs5("span", { className: "flex min-w-0 flex-col", children: [
|
|
2300
|
+
/* @__PURE__ */ jsx7("span", { className: "truncate", children: item.label }),
|
|
2301
|
+
item.detail ? /* @__PURE__ */ jsx7("span", { className: "truncate text-meta text-muted-foreground", children: item.detail }) : null
|
|
2302
|
+
] })
|
|
2303
|
+
},
|
|
2304
|
+
`${item.label}-${String(index)}`
|
|
2305
|
+
);
|
|
2306
|
+
})
|
|
2307
|
+
}
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
);
|
|
2311
|
+
|
|
2312
|
+
// src/markdown-editor/completions/completions-widget.tsx
|
|
2313
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
2314
|
+
var ID_PREFIX2 = "brand-completions";
|
|
2315
|
+
function createCompletionWidget() {
|
|
2316
|
+
function CompletionWidget() {
|
|
2317
|
+
const { view } = useWidgetViewContext2();
|
|
2318
|
+
const wrapperRef = useRef3(null);
|
|
2319
|
+
const state = completionsPluginKey.getState(view.state);
|
|
2320
|
+
const items = state?.items ?? [];
|
|
2321
|
+
const activeIndex = state ? Math.min(state.index, Math.max(0, items.length - 1)) : 0;
|
|
2322
|
+
const activeId = items.length > 0 ? completionOptionId(ID_PREFIX2, activeIndex) : void 0;
|
|
2323
|
+
useLayoutEffect2(() => {
|
|
2324
|
+
const dom = view.dom;
|
|
2325
|
+
const listEl = wrapperRef.current?.querySelector('[role="listbox"]');
|
|
2326
|
+
if (listEl && !listEl.id) listEl.id = `${ID_PREFIX2}-listbox`;
|
|
2327
|
+
dom.setAttribute("aria-expanded", "true");
|
|
2328
|
+
if (listEl) dom.setAttribute("aria-controls", listEl.id);
|
|
2329
|
+
if (activeId) dom.setAttribute("aria-activedescendant", activeId);
|
|
2330
|
+
else dom.removeAttribute("aria-activedescendant");
|
|
2331
|
+
return () => {
|
|
2332
|
+
dom.removeAttribute("aria-expanded");
|
|
2333
|
+
dom.removeAttribute("aria-controls");
|
|
2334
|
+
dom.removeAttribute("aria-activedescendant");
|
|
2335
|
+
};
|
|
2336
|
+
}, [view, activeId]);
|
|
2337
|
+
const onSelect = (index) => {
|
|
2338
|
+
if (!state) return;
|
|
2339
|
+
const item = items[index];
|
|
2340
|
+
if (item) insertCompletionItem(view, state, item);
|
|
2341
|
+
};
|
|
2342
|
+
return /* @__PURE__ */ jsx8(
|
|
2343
|
+
"span",
|
|
2344
|
+
{
|
|
2345
|
+
ref: wrapperRef,
|
|
2346
|
+
contentEditable: false,
|
|
2347
|
+
className: "brand-completions-anchor relative inline-block h-0 w-0 align-baseline",
|
|
2348
|
+
children: /* @__PURE__ */ jsx8("span", { className: "absolute left-0 top-1 z-50 block", children: /* @__PURE__ */ jsx8(
|
|
2349
|
+
CompletionMenu,
|
|
2350
|
+
{
|
|
2351
|
+
items,
|
|
2352
|
+
activeIndex,
|
|
2353
|
+
onSelect,
|
|
2354
|
+
idPrefix: ID_PREFIX2
|
|
2355
|
+
}
|
|
2356
|
+
) })
|
|
2357
|
+
}
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
return CompletionWidget;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// src/markdown-editor/completions/index.ts
|
|
2364
|
+
function completionsViewPlugins(widgetFactory, getProviders) {
|
|
2365
|
+
const widgetComponent = createCompletionWidget();
|
|
2366
|
+
return [completionsProsePlugin({ widgetFactory, widgetComponent, getProviders })];
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// src/markdown-editor/directive-views.tsx
|
|
2370
|
+
import {
|
|
2371
|
+
Alert,
|
|
2372
|
+
AlertDescription,
|
|
2373
|
+
Card,
|
|
2374
|
+
CardContent,
|
|
2375
|
+
CardHeader,
|
|
2376
|
+
CardTitle,
|
|
2377
|
+
ContextMenu as ContextMenu2,
|
|
2378
|
+
ContextMenuContent as ContextMenuContent2,
|
|
2379
|
+
ContextMenuItem as ContextMenuItem2,
|
|
2380
|
+
ContextMenuRadioGroup,
|
|
2381
|
+
ContextMenuRadioItem,
|
|
2382
|
+
ContextMenuSub,
|
|
2383
|
+
ContextMenuSubContent,
|
|
2384
|
+
ContextMenuSubTrigger,
|
|
2385
|
+
ContextMenuTrigger as ContextMenuTrigger2,
|
|
2386
|
+
DropdownMenu,
|
|
2387
|
+
DropdownMenuContent,
|
|
2388
|
+
DropdownMenuItem,
|
|
2389
|
+
DropdownMenuRadioGroup,
|
|
2390
|
+
DropdownMenuRadioItem,
|
|
2391
|
+
DropdownMenuSub,
|
|
2392
|
+
DropdownMenuSubContent,
|
|
2393
|
+
DropdownMenuSubTrigger,
|
|
2394
|
+
DropdownMenuTrigger
|
|
2395
|
+
} from "@elabs-ai/components-ui";
|
|
2396
|
+
import { cn as cn6 } from "@elabs-ai/components-ui/lib/cn";
|
|
2397
|
+
import { editorViewCtx as editorViewCtx2, parserCtx as parserCtx2, serializerCtx } from "@milkdown/kit/core";
|
|
2398
|
+
import { $view } from "@milkdown/kit/utils";
|
|
2399
|
+
import { useNodeViewContext } from "@prosemirror-adapter/react";
|
|
2400
|
+
import {
|
|
2401
|
+
ArrowLeftRight,
|
|
2402
|
+
FileText,
|
|
2403
|
+
Grid3x3 as Grid3x32,
|
|
2404
|
+
LayoutGrid,
|
|
2405
|
+
MoreHorizontal,
|
|
2406
|
+
Pencil,
|
|
2407
|
+
Repeat2 as Repeat23
|
|
2408
|
+
} from "lucide-react";
|
|
2409
|
+
import { useContext as useContext4, useEffect as useEffect4, useRef as useRef6 } from "react";
|
|
2410
|
+
|
|
2411
|
+
// src/markdown-editor/milkdown-react/editor.tsx
|
|
2412
|
+
import { useMemo as useMemo2, useRef as useRef5, useState as useState3 } from "react";
|
|
2413
|
+
|
|
2414
|
+
// src/markdown-editor/milkdown-react/use-get-editor.ts
|
|
2415
|
+
import { createContext as createContext2, useContext, useEffect as useEffect3, useRef as useRef4 } from "react";
|
|
2416
|
+
var editorInfoContext = createContext2({});
|
|
2417
|
+
function useGetEditor() {
|
|
2418
|
+
const {
|
|
2419
|
+
dom,
|
|
2420
|
+
editor: editorRef,
|
|
2421
|
+
setLoading,
|
|
2422
|
+
editorFactory: getEditor
|
|
2423
|
+
} = useContext(editorInfoContext);
|
|
2424
|
+
const domRef = useRef4(null);
|
|
2425
|
+
useEffect3(() => {
|
|
2426
|
+
const div = domRef.current;
|
|
2427
|
+
if (!getEditor) return;
|
|
2428
|
+
if (!div) return;
|
|
2429
|
+
dom.current = div;
|
|
2430
|
+
const editor2 = getEditor(div);
|
|
2431
|
+
if (!editor2) return;
|
|
2432
|
+
setLoading(true);
|
|
2433
|
+
editor2.create().then((editor3) => {
|
|
2434
|
+
editorRef.current = editor3;
|
|
2435
|
+
}).finally(() => {
|
|
2436
|
+
setLoading(false);
|
|
2437
|
+
}).catch(console.error);
|
|
2438
|
+
return () => {
|
|
2439
|
+
editor2.destroy().catch(console.error);
|
|
2440
|
+
};
|
|
2441
|
+
}, [dom, editorRef, getEditor, setLoading]);
|
|
2442
|
+
return domRef;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// src/markdown-editor/milkdown-react/editor.tsx
|
|
2446
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
2447
|
+
var Milkdown = () => {
|
|
2448
|
+
const domRef = useGetEditor();
|
|
2449
|
+
return /* @__PURE__ */ jsx9("div", { "data-milkdown-root": true, ref: domRef });
|
|
2450
|
+
};
|
|
2451
|
+
var MilkdownProvider = ({ children }) => {
|
|
2452
|
+
const dom = useRef5(void 0);
|
|
2453
|
+
const [editorFactory, setEditorFactory] = useState3(void 0);
|
|
2454
|
+
const editor2 = useRef5(void 0);
|
|
2455
|
+
const [loading, setLoading] = useState3(true);
|
|
2456
|
+
const editorInfoCtx = useMemo2(
|
|
2457
|
+
() => ({ loading, dom, editor: editor2, setLoading, editorFactory, setEditorFactory }),
|
|
2458
|
+
[loading, editorFactory]
|
|
2459
|
+
);
|
|
2460
|
+
return /* @__PURE__ */ jsx9(editorInfoContext.Provider, { value: editorInfoCtx, children });
|
|
2461
|
+
};
|
|
2462
|
+
|
|
2463
|
+
// src/markdown-editor/milkdown-react/use-editor.ts
|
|
2464
|
+
import { useCallback, useContext as useContext2, useLayoutEffect as useLayoutEffect3 } from "react";
|
|
2465
|
+
function useEditor(getEditor, deps = []) {
|
|
2466
|
+
const editorInfo = useContext2(editorInfoContext);
|
|
2467
|
+
const factory = useCallback(getEditor, deps);
|
|
2468
|
+
useLayoutEffect3(() => {
|
|
2469
|
+
editorInfo.setEditorFactory(() => factory);
|
|
2470
|
+
}, [editorInfo, factory]);
|
|
2471
|
+
return {
|
|
2472
|
+
loading: editorInfo.loading,
|
|
2473
|
+
get: () => editorInfo.editor.current
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// src/markdown-editor/milkdown-react/use-instance.ts
|
|
2478
|
+
import { useCallback as useCallback2, useContext as useContext3 } from "react";
|
|
2479
|
+
function useInstance() {
|
|
2480
|
+
const editorInfo = useContext3(editorInfoContext);
|
|
2481
|
+
const getInstance = useCallback2(() => {
|
|
2482
|
+
return editorInfo.editor.current;
|
|
2483
|
+
}, [editorInfo.editor]);
|
|
2484
|
+
return [editorInfo.loading, getInstance];
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// src/markdown-editor/directive-views.tsx
|
|
2488
|
+
import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2489
|
+
var CALLOUT_VARIANT = {
|
|
2490
|
+
info: "info",
|
|
2491
|
+
note: "info",
|
|
2492
|
+
tip: "success",
|
|
2493
|
+
success: "success",
|
|
2494
|
+
warning: "warning",
|
|
2495
|
+
caution: "warning",
|
|
2496
|
+
danger: "destructive",
|
|
2497
|
+
error: "destructive",
|
|
2498
|
+
destructive: "destructive"
|
|
2499
|
+
};
|
|
2500
|
+
function capitalize(s) {
|
|
2501
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
2502
|
+
}
|
|
2503
|
+
function useDirectiveAttrs() {
|
|
2504
|
+
const { node, setAttrs } = useNodeViewContext();
|
|
2505
|
+
const name = String(node.attrs.name);
|
|
2506
|
+
const attributes = node.attrs.attributes ?? {};
|
|
2507
|
+
const update = (key, value) => {
|
|
2508
|
+
const next = { ...attributes };
|
|
2509
|
+
if (value === "") delete next[key];
|
|
2510
|
+
else next[key] = value;
|
|
2511
|
+
setAttrs({ attributes: next });
|
|
2512
|
+
};
|
|
2513
|
+
return { name, attributes, update };
|
|
2514
|
+
}
|
|
2515
|
+
function InlineEdit({ value, onCommit, ariaLabel, placeholder, className }) {
|
|
2516
|
+
const ref = useRef6(null);
|
|
2517
|
+
useEffect4(() => {
|
|
2518
|
+
const el = ref.current;
|
|
2519
|
+
if (!el) return;
|
|
2520
|
+
if (el === el.ownerDocument.activeElement) return;
|
|
2521
|
+
if (el.textContent !== value) el.textContent = value;
|
|
2522
|
+
}, [value]);
|
|
2523
|
+
const commit = () => {
|
|
2524
|
+
const next = (ref.current?.textContent ?? "").trim();
|
|
2525
|
+
if (next !== value) onCommit(next);
|
|
2526
|
+
};
|
|
2527
|
+
const onKeyDown = (e) => {
|
|
2528
|
+
if (e.key === "Enter") {
|
|
2529
|
+
e.preventDefault();
|
|
2530
|
+
e.currentTarget.blur();
|
|
2531
|
+
} else if (e.key === "Escape") {
|
|
2532
|
+
e.preventDefault();
|
|
2533
|
+
if (ref.current) ref.current.textContent = value;
|
|
2534
|
+
e.currentTarget.blur();
|
|
2535
|
+
}
|
|
2536
|
+
};
|
|
2537
|
+
return /* @__PURE__ */ jsx10(
|
|
2538
|
+
"span",
|
|
2539
|
+
{
|
|
2540
|
+
ref,
|
|
2541
|
+
role: "textbox",
|
|
2542
|
+
"aria-label": ariaLabel,
|
|
2543
|
+
"aria-multiline": false,
|
|
2544
|
+
"data-directive-chrome": "",
|
|
2545
|
+
"data-placeholder": placeholder,
|
|
2546
|
+
contentEditable: true,
|
|
2547
|
+
suppressContentEditableWarning: true,
|
|
2548
|
+
tabIndex: 0,
|
|
2549
|
+
spellCheck: false,
|
|
2550
|
+
onBlur: commit,
|
|
2551
|
+
onKeyDown,
|
|
2552
|
+
className: cn6(
|
|
2553
|
+
"brand-inline-edit rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
2554
|
+
className
|
|
2555
|
+
)
|
|
2556
|
+
}
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
function ContainerDirectiveView() {
|
|
2560
|
+
const { contentRef } = useNodeViewContext();
|
|
2561
|
+
const { name, attributes, update } = useDirectiveAttrs();
|
|
2562
|
+
const body = /* @__PURE__ */ jsx10("div", { className: "brand-directive__body", ref: contentRef });
|
|
2563
|
+
if (name === "card") {
|
|
2564
|
+
return /* @__PURE__ */ jsxs6(Card, { className: "brand-directive brand-directive--card", "data-brand-directive": "card", children: [
|
|
2565
|
+
/* @__PURE__ */ jsx10(CardHeader, { className: "pb-3", children: /* @__PURE__ */ jsx10(CardTitle, { children: /* @__PURE__ */ jsx10(
|
|
2566
|
+
InlineEdit,
|
|
2567
|
+
{
|
|
2568
|
+
ariaLabel: "Card title",
|
|
2569
|
+
placeholder: "Card title",
|
|
2570
|
+
value: attributes.title ?? "",
|
|
2571
|
+
onCommit: (v) => update("title", v)
|
|
2572
|
+
}
|
|
2573
|
+
) }) }),
|
|
2574
|
+
/* @__PURE__ */ jsx10(CardContent, { children: body })
|
|
2575
|
+
] });
|
|
2576
|
+
}
|
|
2577
|
+
if (name === "callout") {
|
|
2578
|
+
const variant = CALLOUT_VARIANT[attributes.type ?? ""] ?? "default";
|
|
2579
|
+
return /* @__PURE__ */ jsxs6(
|
|
2580
|
+
Alert,
|
|
2581
|
+
{
|
|
2582
|
+
variant,
|
|
2583
|
+
className: "brand-directive brand-directive--callout",
|
|
2584
|
+
"data-brand-directive": "callout",
|
|
2585
|
+
children: [
|
|
2586
|
+
/* @__PURE__ */ jsx10("div", { className: "mb-1 font-medium leading-none tracking-tight", children: /* @__PURE__ */ jsx10(
|
|
2587
|
+
InlineEdit,
|
|
2588
|
+
{
|
|
2589
|
+
ariaLabel: "Callout title",
|
|
2590
|
+
placeholder: capitalize(attributes.type ?? "note"),
|
|
2591
|
+
value: attributes.title ?? "",
|
|
2592
|
+
onCommit: (v) => update("title", v)
|
|
2593
|
+
}
|
|
2594
|
+
) }),
|
|
2595
|
+
/* @__PURE__ */ jsx10(AlertDescription, { children: body })
|
|
2596
|
+
]
|
|
2597
|
+
}
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
if (name === "timeline") {
|
|
2601
|
+
return /* @__PURE__ */ jsx10("div", { className: "brand-directive brand-directive--timeline", "data-brand-directive": "timeline", children: body });
|
|
2602
|
+
}
|
|
2603
|
+
if (name === "iterate" || name === "pivot") {
|
|
2604
|
+
return /* @__PURE__ */ jsx10(IterationDirectiveView, {});
|
|
2605
|
+
}
|
|
2606
|
+
return /* @__PURE__ */ jsxs6(
|
|
2607
|
+
Alert,
|
|
2608
|
+
{
|
|
2609
|
+
role: "note",
|
|
2610
|
+
variant: "destructive",
|
|
2611
|
+
className: "brand-directive brand-directive--unknown",
|
|
2612
|
+
"data-brand-directive": name,
|
|
2613
|
+
children: [
|
|
2614
|
+
/* @__PURE__ */ jsxs6("div", { className: "mb-1 font-medium leading-none tracking-tight", children: [
|
|
2615
|
+
"Unknown block: ",
|
|
2616
|
+
name
|
|
2617
|
+
] }),
|
|
2618
|
+
/* @__PURE__ */ jsx10(AlertDescription, { children: body })
|
|
2619
|
+
]
|
|
2620
|
+
}
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
function readBodyMarkdown(getInstance, node) {
|
|
2624
|
+
try {
|
|
2625
|
+
const editor2 = getInstance();
|
|
2626
|
+
if (!editor2) return node.textContent;
|
|
2627
|
+
return editor2.action((ctx2) => {
|
|
2628
|
+
const serialize = ctx2.get(
|
|
2629
|
+
serializerCtx
|
|
2630
|
+
);
|
|
2631
|
+
const doc = node.type.schema.topNodeType.create(null, node.content);
|
|
2632
|
+
return serialize(doc);
|
|
2633
|
+
}).trim();
|
|
2634
|
+
} catch {
|
|
2635
|
+
return node.textContent;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
function writeBodyMarkdown(getInstance, getPos, template) {
|
|
2639
|
+
try {
|
|
2640
|
+
const editor2 = getInstance();
|
|
2641
|
+
const pos = getPos();
|
|
2642
|
+
if (!editor2 || pos == null) return;
|
|
2643
|
+
editor2.action((ctx2) => {
|
|
2644
|
+
const parse = ctx2.get(
|
|
2645
|
+
parserCtx2
|
|
2646
|
+
);
|
|
2647
|
+
const parsed = parse(template);
|
|
2648
|
+
if (!parsed) return;
|
|
2649
|
+
const view = ctx2.get(editorViewCtx2);
|
|
2650
|
+
const node = view.state.doc.nodeAt(pos);
|
|
2651
|
+
if (!node) return;
|
|
2652
|
+
const start = pos + 1;
|
|
2653
|
+
const end = start + node.content.size;
|
|
2654
|
+
const tr = view.state.tr.replaceWith(start, end, parsed.content);
|
|
2655
|
+
view.dispatch(tr);
|
|
2656
|
+
});
|
|
2657
|
+
} catch {
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
function mergeAttrsOmittingEmpty(attrs, next) {
|
|
2661
|
+
const merged = { ...attrs };
|
|
2662
|
+
for (const [key, value] of Object.entries(next)) {
|
|
2663
|
+
if (value === "") delete merged[key];
|
|
2664
|
+
else merged[key] = value;
|
|
2665
|
+
}
|
|
2666
|
+
return merged;
|
|
2667
|
+
}
|
|
2668
|
+
function replaceNodeWithMarkdown(getInstance, getPos, markdown) {
|
|
2669
|
+
if (!markdown.trim()) return;
|
|
2670
|
+
try {
|
|
2671
|
+
const editor2 = getInstance();
|
|
2672
|
+
const pos = getPos();
|
|
2673
|
+
if (!editor2 || pos == null) return;
|
|
2674
|
+
editor2.action((ctx2) => {
|
|
2675
|
+
const parse = ctx2.get(
|
|
2676
|
+
parserCtx2
|
|
2677
|
+
);
|
|
2678
|
+
const parsed = parse(markdown);
|
|
2679
|
+
if (!parsed) return;
|
|
2680
|
+
const view = ctx2.get(editorViewCtx2);
|
|
2681
|
+
const node = view.state.doc.nodeAt(pos);
|
|
2682
|
+
if (!node) return;
|
|
2683
|
+
const tr = view.state.tr.replaceWith(pos, pos + node.nodeSize, parsed.content);
|
|
2684
|
+
view.dispatch(tr);
|
|
2685
|
+
});
|
|
2686
|
+
} catch {
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
function IterationMenuItems({
|
|
2690
|
+
kind,
|
|
2691
|
+
entries
|
|
2692
|
+
}) {
|
|
2693
|
+
const isDropdown = kind === "dropdown";
|
|
2694
|
+
const Item = isDropdown ? DropdownMenuItem : ContextMenuItem2;
|
|
2695
|
+
const Sub = isDropdown ? DropdownMenuSub : ContextMenuSub;
|
|
2696
|
+
const SubTrigger = isDropdown ? DropdownMenuSubTrigger : ContextMenuSubTrigger;
|
|
2697
|
+
const SubContent = isDropdown ? DropdownMenuSubContent : ContextMenuSubContent;
|
|
2698
|
+
const RadioGroup = isDropdown ? DropdownMenuRadioGroup : ContextMenuRadioGroup;
|
|
2699
|
+
const RadioItem = isDropdown ? DropdownMenuRadioItem : ContextMenuRadioItem;
|
|
2700
|
+
return /* @__PURE__ */ jsx10(Fragment2, { children: entries.map((entry) => {
|
|
2701
|
+
if (entry.type === "layout") {
|
|
2702
|
+
return /* @__PURE__ */ jsxs6(Sub, { children: [
|
|
2703
|
+
/* @__PURE__ */ jsxs6(SubTrigger, { className: "gap-2", children: [
|
|
2704
|
+
entry.icon,
|
|
2705
|
+
entry.label
|
|
2706
|
+
] }),
|
|
2707
|
+
/* @__PURE__ */ jsx10(SubContent, { children: /* @__PURE__ */ jsx10(
|
|
2708
|
+
RadioGroup,
|
|
2709
|
+
{
|
|
2710
|
+
value: entry.value,
|
|
2711
|
+
onValueChange: (next) => entry.onChange(next),
|
|
2712
|
+
children: entry.options.map((option) => /* @__PURE__ */ jsx10(RadioItem, { value: option, className: "capitalize", children: option }, option))
|
|
2713
|
+
}
|
|
2714
|
+
) })
|
|
2715
|
+
] }, entry.id);
|
|
2716
|
+
}
|
|
2717
|
+
return /* @__PURE__ */ jsxs6(Item, { onSelect: entry.onSelect, disabled: entry.disabled, children: [
|
|
2718
|
+
entry.icon,
|
|
2719
|
+
entry.label
|
|
2720
|
+
] }, entry.id);
|
|
2721
|
+
}) });
|
|
2722
|
+
}
|
|
2723
|
+
function IterationDirectiveView() {
|
|
2724
|
+
const { contentRef, node, getPos, setAttrs } = useNodeViewContext();
|
|
2725
|
+
const { name, attributes } = useDirectiveAttrs();
|
|
2726
|
+
const [, getInstance] = useInstance();
|
|
2727
|
+
const onEdit = useContext4(IterationEditContext);
|
|
2728
|
+
const isPivot = name === "pivot";
|
|
2729
|
+
const kind = isPivot ? "pivot" : "iterate";
|
|
2730
|
+
const Icon = isPivot ? Grid3x32 : Repeat23;
|
|
2731
|
+
const requestEdit = () => {
|
|
2732
|
+
onEdit?.({
|
|
2733
|
+
kind,
|
|
2734
|
+
template: readBodyMarkdown(getInstance, node),
|
|
2735
|
+
// A5: hand the current attributes (value lists, bind name, layout) to the
|
|
2736
|
+
// handler so the GUIDED builder can reopen with its data — and a writer that
|
|
2737
|
+
// round-trips BOTH the attributes and the body, not just the template.
|
|
2738
|
+
attributes: { ...attributes },
|
|
2739
|
+
onSave: (template) => writeBodyMarkdown(getInstance, getPos, template),
|
|
2740
|
+
// MERGE the guided builder's write-back into the EXISTING attributes rather
|
|
2741
|
+
// than replacing the whole record — `directivePartsFromValue` only knows
|
|
2742
|
+
// about `as`/`layout`/`values`/`rows`/`cols`, so a naive
|
|
2743
|
+
// `setAttrs({ attributes: nextAttrs })` would silently drop every other
|
|
2744
|
+
// attribute the directive carries (e.g. a consumer's `source`/`region`
|
|
2745
|
+
// reference — `containerDirectiveSchema.attrs.attributes` is a free-form
|
|
2746
|
+
// record, and those keys are load-bearing for the consumer's `evaluate`).
|
|
2747
|
+
// Mirrors the `transpose()` fix below.
|
|
2748
|
+
onSaveData: ({ attributes: nextAttrs, template }) => {
|
|
2749
|
+
setAttrs({
|
|
2750
|
+
attributes: mergeAttrsOmittingEmpty(attributes, nextAttrs)
|
|
2751
|
+
});
|
|
2752
|
+
writeBodyMarkdown(getInstance, getPos, template);
|
|
2753
|
+
},
|
|
2754
|
+
onSetAttributes: (nextAttrs) => setAttrs({ attributes: nextAttrs }),
|
|
2755
|
+
onReplaceWithMarkdown: (markdown) => replaceNodeWithMarkdown(getInstance, getPos, markdown)
|
|
2756
|
+
});
|
|
2757
|
+
};
|
|
2758
|
+
const setLayout = (layout) => {
|
|
2759
|
+
setAttrs({ attributes: { ...attributes, layout } });
|
|
2760
|
+
};
|
|
2761
|
+
const transpose = () => {
|
|
2762
|
+
const seed = builderValueFromParts(kind, attributes, "");
|
|
2763
|
+
const { attributes: transposed } = directivePartsFromValue(transposeIterationValue(seed));
|
|
2764
|
+
setAttrs({
|
|
2765
|
+
attributes: mergeAttrsOmittingEmpty(attributes, {
|
|
2766
|
+
rows: transposed.rows ?? "",
|
|
2767
|
+
cols: transposed.cols ?? ""
|
|
2768
|
+
})
|
|
2769
|
+
});
|
|
2770
|
+
};
|
|
2771
|
+
const hasEmbeddedData = evaluateEmbedded({
|
|
2772
|
+
kind,
|
|
2773
|
+
layout: attributes.layout || ITERATION_LAYOUTS[kind][0],
|
|
2774
|
+
template: "",
|
|
2775
|
+
as: attributes.as || "item",
|
|
2776
|
+
attributes
|
|
2777
|
+
}).cells.length > 0;
|
|
2778
|
+
const disabledHint = "\u2014 needs embedded values";
|
|
2779
|
+
const convertToStatic = () => {
|
|
2780
|
+
const template = readBodyMarkdown(getInstance, node);
|
|
2781
|
+
const value = builderValueFromParts(kind, attributes, template);
|
|
2782
|
+
replaceNodeWithMarkdown(getInstance, getPos, staticMarkdownFromValue(value));
|
|
2783
|
+
};
|
|
2784
|
+
const menuEntries = [
|
|
2785
|
+
{
|
|
2786
|
+
type: "item",
|
|
2787
|
+
id: "edit",
|
|
2788
|
+
label: "Edit iteration\u2026",
|
|
2789
|
+
icon: /* @__PURE__ */ jsx10(Pencil, { className: "size-4", "aria-hidden": "true" }),
|
|
2790
|
+
onSelect: requestEdit
|
|
2791
|
+
},
|
|
2792
|
+
{
|
|
2793
|
+
type: "layout",
|
|
2794
|
+
id: "layout",
|
|
2795
|
+
label: "Change layout",
|
|
2796
|
+
icon: /* @__PURE__ */ jsx10(LayoutGrid, { className: "size-4", "aria-hidden": "true" }),
|
|
2797
|
+
value: attributes.layout || ITERATION_LAYOUTS[kind][0],
|
|
2798
|
+
options: ITERATION_LAYOUTS[kind],
|
|
2799
|
+
onChange: setLayout
|
|
2800
|
+
},
|
|
2801
|
+
...isPivot ? [
|
|
2802
|
+
{
|
|
2803
|
+
type: "item",
|
|
2804
|
+
id: "transpose",
|
|
2805
|
+
label: hasEmbeddedData ? "Transpose" : `Transpose ${disabledHint}`,
|
|
2806
|
+
icon: /* @__PURE__ */ jsx10(ArrowLeftRight, { className: "size-4", "aria-hidden": "true" }),
|
|
2807
|
+
onSelect: transpose,
|
|
2808
|
+
disabled: !hasEmbeddedData
|
|
2809
|
+
}
|
|
2810
|
+
] : [],
|
|
2811
|
+
{
|
|
2812
|
+
type: "item",
|
|
2813
|
+
id: "convert-to-static",
|
|
2814
|
+
label: hasEmbeddedData ? "Convert to static" : `Convert to static ${disabledHint}`,
|
|
2815
|
+
icon: /* @__PURE__ */ jsx10(FileText, { className: "size-4", "aria-hidden": "true" }),
|
|
2816
|
+
onSelect: convertToStatic,
|
|
2817
|
+
disabled: !hasEmbeddedData
|
|
2818
|
+
}
|
|
2819
|
+
];
|
|
2820
|
+
const header = /* @__PURE__ */ jsxs6("div", { className: "mb-1.5 flex items-center gap-1.5 text-meta font-medium text-info-text", children: [
|
|
2821
|
+
/* @__PURE__ */ jsx10(Icon, { className: "size-3.5 shrink-0", "aria-hidden": "true" }),
|
|
2822
|
+
/* @__PURE__ */ jsx10("span", { children: isPivot ? "Pivot" : "Iterate" }),
|
|
2823
|
+
!isPivot && attributes.as ? /* @__PURE__ */ jsxs6("span", { className: "font-normal text-muted-foreground", children: [
|
|
2824
|
+
"\xB7 per ",
|
|
2825
|
+
attributes.as
|
|
2826
|
+
] }) : null,
|
|
2827
|
+
/* @__PURE__ */ jsx10("span", { className: "font-normal text-muted-foreground", children: "\u2014 template" }),
|
|
2828
|
+
onEdit ? /* @__PURE__ */ jsxs6(DropdownMenu, { children: [
|
|
2829
|
+
/* @__PURE__ */ jsx10(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx10(
|
|
2830
|
+
"button",
|
|
2831
|
+
{
|
|
2832
|
+
type: "button",
|
|
2833
|
+
"data-directive-chrome": "",
|
|
2834
|
+
"aria-label": "Iteration actions",
|
|
2835
|
+
title: "Iteration actions\u2026",
|
|
2836
|
+
className: "ms-auto inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
2837
|
+
children: /* @__PURE__ */ jsx10(MoreHorizontal, { className: "size-4", "aria-hidden": "true" })
|
|
2838
|
+
}
|
|
2839
|
+
) }),
|
|
2840
|
+
/* @__PURE__ */ jsx10(DropdownMenuContent, { align: "end", children: /* @__PURE__ */ jsx10(IterationMenuItems, { kind: "dropdown", entries: menuEntries }) })
|
|
2841
|
+
] }) : null
|
|
2842
|
+
] });
|
|
2843
|
+
const body = (
|
|
2844
|
+
// The editable template body (inline ProseMirror content).
|
|
2845
|
+
/* @__PURE__ */ jsx10("div", { className: "brand-directive__body", ref: contentRef })
|
|
2846
|
+
);
|
|
2847
|
+
if (!onEdit) {
|
|
2848
|
+
return /* @__PURE__ */ jsxs6(
|
|
2849
|
+
"div",
|
|
2850
|
+
{
|
|
2851
|
+
className: "brand-directive brand-directive--iterate border-s-2 border-s-info ps-3",
|
|
2852
|
+
"data-brand-directive": name,
|
|
2853
|
+
children: [
|
|
2854
|
+
header,
|
|
2855
|
+
body
|
|
2856
|
+
]
|
|
2857
|
+
}
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
return /* @__PURE__ */ jsxs6(ContextMenu2, { children: [
|
|
2861
|
+
/* @__PURE__ */ jsxs6(
|
|
2862
|
+
"div",
|
|
2863
|
+
{
|
|
2864
|
+
className: "brand-directive brand-directive--iterate border-s-2 border-s-info ps-3",
|
|
2865
|
+
"data-brand-directive": name,
|
|
2866
|
+
children: [
|
|
2867
|
+
/* @__PURE__ */ jsx10(ContextMenuTrigger2, { asChild: true, children: header }),
|
|
2868
|
+
body
|
|
2869
|
+
]
|
|
2870
|
+
}
|
|
2871
|
+
),
|
|
2872
|
+
/* @__PURE__ */ jsx10(ContextMenuContent2, { children: /* @__PURE__ */ jsx10(IterationMenuItems, { kind: "context", entries: menuEntries }) })
|
|
2873
|
+
] });
|
|
2874
|
+
}
|
|
2875
|
+
function LeafDirectiveView() {
|
|
2876
|
+
const { name, attributes, update } = useDirectiveAttrs();
|
|
2877
|
+
if (name !== "metric") {
|
|
2878
|
+
return /* @__PURE__ */ jsxs6(
|
|
2879
|
+
"div",
|
|
2880
|
+
{
|
|
2881
|
+
className: "brand-directive brand-directive--leaf brand-directive--unknown rounded-md border border-destructive/40 bg-surface-muted p-3 text-sm text-muted-foreground",
|
|
2882
|
+
"data-brand-leaf": name,
|
|
2883
|
+
children: [
|
|
2884
|
+
"Unknown inline block: ",
|
|
2885
|
+
/* @__PURE__ */ jsxs6("code", { children: [
|
|
2886
|
+
"::",
|
|
2887
|
+
name
|
|
2888
|
+
] })
|
|
2889
|
+
]
|
|
2890
|
+
}
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
const delta = attributes.delta;
|
|
2894
|
+
return /* @__PURE__ */ jsx10(
|
|
2895
|
+
MetricBlock,
|
|
2896
|
+
{
|
|
2897
|
+
className: "brand-directive brand-directive--leaf brand-directive--metric",
|
|
2898
|
+
"data-brand-leaf": "metric",
|
|
2899
|
+
label: /* @__PURE__ */ jsx10(
|
|
2900
|
+
InlineEdit,
|
|
2901
|
+
{
|
|
2902
|
+
ariaLabel: "Metric label",
|
|
2903
|
+
placeholder: "Label",
|
|
2904
|
+
value: attributes.label ?? "",
|
|
2905
|
+
onCommit: (v) => update("label", v)
|
|
2906
|
+
}
|
|
2907
|
+
),
|
|
2908
|
+
value: /* @__PURE__ */ jsx10(
|
|
2909
|
+
InlineEdit,
|
|
2910
|
+
{
|
|
2911
|
+
ariaLabel: "Metric value",
|
|
2912
|
+
placeholder: "0",
|
|
2913
|
+
value: attributes.value ?? "",
|
|
2914
|
+
onCommit: (v) => update("value", v),
|
|
2915
|
+
className: "min-w-[1ch]"
|
|
2916
|
+
}
|
|
2917
|
+
),
|
|
2918
|
+
description: attributes.description,
|
|
2919
|
+
delta,
|
|
2920
|
+
deltaDirection: delta?.startsWith("+") ? "up" : delta?.startsWith("-") ? "down" : "neutral"
|
|
2921
|
+
}
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
function directiveStopEvent(event) {
|
|
2925
|
+
let node = event.target;
|
|
2926
|
+
while (node instanceof HTMLElement) {
|
|
2927
|
+
if (node.hasAttribute("data-directive-chrome")) return true;
|
|
2928
|
+
if (node.hasAttribute("data-node-view-root")) return false;
|
|
2929
|
+
node = node.parentElement;
|
|
2930
|
+
}
|
|
2931
|
+
return false;
|
|
2932
|
+
}
|
|
2933
|
+
function directiveViewPlugins(nodeViewFactory) {
|
|
2934
|
+
return [
|
|
2935
|
+
$view(
|
|
2936
|
+
containerDirectiveSchema.node,
|
|
2937
|
+
() => nodeViewFactory({
|
|
2938
|
+
component: ContainerDirectiveView,
|
|
2939
|
+
as: "div",
|
|
2940
|
+
contentAs: "div",
|
|
2941
|
+
stopEvent: directiveStopEvent
|
|
2942
|
+
})
|
|
2943
|
+
),
|
|
2944
|
+
$view(
|
|
2945
|
+
leafDirectiveSchema.node,
|
|
2946
|
+
() => nodeViewFactory({
|
|
2947
|
+
component: LeafDirectiveView,
|
|
2948
|
+
as: "div",
|
|
2949
|
+
stopEvent: directiveStopEvent
|
|
2950
|
+
})
|
|
2951
|
+
)
|
|
2952
|
+
].flat();
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// src/markdown-editor/exit-keymap.ts
|
|
2956
|
+
import { $prose as $prose5 } from "@milkdown/kit/utils";
|
|
2957
|
+
import { Plugin as Plugin5, TextSelection as TextSelection2 } from "@milkdown/kit/prose/state";
|
|
2958
|
+
var exitCodeBlock = (state, dispatch) => {
|
|
2959
|
+
const { selection } = state;
|
|
2960
|
+
if (!selection.empty) return false;
|
|
2961
|
+
const { $head } = selection;
|
|
2962
|
+
if (!$head.parent.type.spec.code) return false;
|
|
2963
|
+
const paragraph = state.schema.nodes.paragraph;
|
|
2964
|
+
if (!paragraph) return false;
|
|
2965
|
+
const after = $head.after($head.depth);
|
|
2966
|
+
if (dispatch) {
|
|
2967
|
+
const nodeAfter = state.doc.resolve(after).nodeAfter;
|
|
2968
|
+
let tr = state.tr;
|
|
2969
|
+
if (nodeAfter && nodeAfter.isTextblock) {
|
|
2970
|
+
tr = tr.setSelection(TextSelection2.create(tr.doc, after + 1));
|
|
2971
|
+
} else {
|
|
2972
|
+
const para = paragraph.createAndFill();
|
|
2973
|
+
if (!para) return false;
|
|
2974
|
+
tr = tr.insert(after, para);
|
|
2975
|
+
tr = tr.setSelection(TextSelection2.create(tr.doc, after + 1));
|
|
2976
|
+
}
|
|
2977
|
+
dispatch(tr.scrollIntoView());
|
|
2978
|
+
}
|
|
2979
|
+
return true;
|
|
2980
|
+
};
|
|
2981
|
+
function exitKeymapPlugins() {
|
|
2982
|
+
return [
|
|
2983
|
+
$prose5(
|
|
2984
|
+
() => new Plugin5({
|
|
2985
|
+
props: {
|
|
2986
|
+
handleKeyDown: (view, event) => {
|
|
2987
|
+
const isTab = event.key === "Tab" && !event.shiftKey;
|
|
2988
|
+
const isModEnter = event.key === "Enter" && (event.metaKey || event.ctrlKey);
|
|
2989
|
+
if (!isTab && !isModEnter) return false;
|
|
2990
|
+
const handled = exitCodeBlock(view.state, view.dispatch);
|
|
2991
|
+
if (handled) event.preventDefault();
|
|
2992
|
+
return handled;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
})
|
|
2996
|
+
)
|
|
2997
|
+
];
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
// src/markdown-editor/paste-embed.ts
|
|
3001
|
+
import { $prose as $prose6 } from "@milkdown/kit/utils";
|
|
3002
|
+
import { Plugin as Plugin6, PluginKey as PluginKey5 } from "@milkdown/kit/prose/state";
|
|
3003
|
+
import { Decoration as Decoration2, DecorationSet as DecorationSet4 } from "@milkdown/kit/prose/view";
|
|
3004
|
+
import { toast } from "@elabs-ai/components-ui";
|
|
3005
|
+
function uniqueId() {
|
|
3006
|
+
return `embed-${Math.random().toString(36).slice(2)}`;
|
|
3007
|
+
}
|
|
3008
|
+
var embedPluginKey = new PluginKey5("brand-embed");
|
|
3009
|
+
function makePlaceholderChip(filename) {
|
|
3010
|
+
const chip = document.createElement("span");
|
|
3011
|
+
chip.setAttribute("role", "status");
|
|
3012
|
+
chip.setAttribute("aria-live", "polite");
|
|
3013
|
+
chip.setAttribute("aria-label", `Uploading ${filename}`);
|
|
3014
|
+
chip.setAttribute("title", `Uploading ${filename}\u2026`);
|
|
3015
|
+
chip.className = "inline-flex items-center gap-1 rounded px-2 py-0.5 text-caption bg-muted text-muted-foreground border border-border select-none transition-opacity duration-normal ease-standard motion-reduce:transition-none";
|
|
3016
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
3017
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
3018
|
+
svg.setAttribute("fill", "none");
|
|
3019
|
+
svg.setAttribute("aria-hidden", "true");
|
|
3020
|
+
svg.style.cssText = "width:0.85em;height:0.85em;animation:spin 1s linear infinite;flex-shrink:0;";
|
|
3021
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
3022
|
+
circle.setAttribute("cx", "12");
|
|
3023
|
+
circle.setAttribute("cy", "12");
|
|
3024
|
+
circle.setAttribute("r", "9");
|
|
3025
|
+
circle.setAttribute("stroke", "currentColor");
|
|
3026
|
+
circle.setAttribute("stroke-width", "2.5");
|
|
3027
|
+
circle.setAttribute("stroke-dasharray", "56.5");
|
|
3028
|
+
circle.setAttribute("stroke-dashoffset", "42");
|
|
3029
|
+
circle.setAttribute("stroke-linecap", "round");
|
|
3030
|
+
svg.appendChild(circle);
|
|
3031
|
+
chip.appendChild(svg);
|
|
3032
|
+
if (!chip.ownerDocument.head.querySelector("#embed-spin-keyframe")) {
|
|
3033
|
+
const style = chip.ownerDocument.createElement("style");
|
|
3034
|
+
style.id = "embed-spin-keyframe";
|
|
3035
|
+
style.textContent = "@keyframes spin{to{transform:rotate(360deg)}}";
|
|
3036
|
+
chip.ownerDocument.head.appendChild(style);
|
|
3037
|
+
}
|
|
3038
|
+
const label = document.createElement("span");
|
|
3039
|
+
label.textContent = `${filename.length > 24 ? filename.slice(0, 22) + "\u2026" : filename} Uploading\u2026`;
|
|
3040
|
+
chip.appendChild(label);
|
|
3041
|
+
return chip;
|
|
3042
|
+
}
|
|
3043
|
+
function makeErrorChip(message) {
|
|
3044
|
+
const chip = document.createElement("span");
|
|
3045
|
+
chip.setAttribute("role", "alert");
|
|
3046
|
+
chip.setAttribute("aria-live", "assertive");
|
|
3047
|
+
chip.setAttribute("title", message);
|
|
3048
|
+
chip.className = "inline-flex items-center gap-1 rounded px-2 py-0.5 text-caption bg-destructive/10 text-destructive border border-destructive/30 select-none";
|
|
3049
|
+
const label = document.createElement("span");
|
|
3050
|
+
label.textContent = `\u26A0 Upload failed`;
|
|
3051
|
+
chip.appendChild(label);
|
|
3052
|
+
return chip;
|
|
3053
|
+
}
|
|
3054
|
+
function embedAsset(view, file, pos, onEmbedAsset, onError) {
|
|
3055
|
+
const id = uniqueId();
|
|
3056
|
+
const addMeta = { type: "add", id, pos, filename: file.name };
|
|
3057
|
+
view.dispatch(view.state.tr.setMeta(embedPluginKey, addMeta));
|
|
3058
|
+
onEmbedAsset(file).then(
|
|
3059
|
+
(path) => {
|
|
3060
|
+
const pluginState = embedPluginKey.getState(view.state);
|
|
3061
|
+
const deco = pluginState?.byId.get(id);
|
|
3062
|
+
const removeMeta = { type: "remove", id };
|
|
3063
|
+
const tr = view.state.tr.setMeta(embedPluginKey, removeMeta);
|
|
3064
|
+
if (deco) {
|
|
3065
|
+
const decoPos = deco.from;
|
|
3066
|
+
const imageNode = view.state.schema.nodes.image?.create({
|
|
3067
|
+
src: path,
|
|
3068
|
+
alt: file.name.replace(/\.[^.]+$/, ""),
|
|
3069
|
+
title: null
|
|
3070
|
+
});
|
|
3071
|
+
if (imageNode) {
|
|
3072
|
+
const insertTr = view.state.tr.insert(decoPos, imageNode);
|
|
3073
|
+
view.dispatch(insertTr);
|
|
3074
|
+
const removeTr = view.state.tr.setMeta(embedPluginKey, removeMeta);
|
|
3075
|
+
view.dispatch(removeTr);
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
view.dispatch(tr);
|
|
3080
|
+
},
|
|
3081
|
+
(err) => {
|
|
3082
|
+
const pluginState = embedPluginKey.getState(view.state);
|
|
3083
|
+
const deco = pluginState?.byId.get(id);
|
|
3084
|
+
const errPos = deco ? deco.from : pos;
|
|
3085
|
+
const message = err instanceof Error ? err.message : "Upload failed";
|
|
3086
|
+
const errorMeta = { type: "error", id, pos: errPos, message };
|
|
3087
|
+
view.dispatch(view.state.tr.setMeta(embedPluginKey, errorMeta));
|
|
3088
|
+
toast.error(`Upload failed: ${message}`);
|
|
3089
|
+
onError?.(message);
|
|
3090
|
+
setTimeout(() => {
|
|
3091
|
+
if (view.isDestroyed) return;
|
|
3092
|
+
const clearMeta = { type: "clear-error", id };
|
|
3093
|
+
view.dispatch(view.state.tr.setMeta(embedPluginKey, clearMeta));
|
|
3094
|
+
}, 4e3);
|
|
3095
|
+
}
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
function imageFiles(list) {
|
|
3099
|
+
if (!list) return [];
|
|
3100
|
+
const files = [];
|
|
3101
|
+
for (let i = 0; i < list.length; i++) {
|
|
3102
|
+
const f = list[i];
|
|
3103
|
+
if (f && f.type.startsWith("image/")) files.push(f);
|
|
3104
|
+
}
|
|
3105
|
+
return files;
|
|
3106
|
+
}
|
|
3107
|
+
function pasteEmbedPlugin(onEmbedAsset) {
|
|
3108
|
+
return $prose6(() => {
|
|
3109
|
+
return new Plugin6({
|
|
3110
|
+
key: embedPluginKey,
|
|
3111
|
+
state: {
|
|
3112
|
+
init: () => ({
|
|
3113
|
+
decos: DecorationSet4.empty,
|
|
3114
|
+
byId: /* @__PURE__ */ new Map(),
|
|
3115
|
+
errorIds: /* @__PURE__ */ new Set()
|
|
3116
|
+
}),
|
|
3117
|
+
apply: (tr, prev, _oldState, newState) => {
|
|
3118
|
+
let decos = prev.decos.map(tr.mapping, tr.doc);
|
|
3119
|
+
const byId = new Map(prev.byId);
|
|
3120
|
+
const errorIds = new Set(prev.errorIds);
|
|
3121
|
+
for (const [id, oldDeco] of prev.byId.entries()) {
|
|
3122
|
+
const key = oldDeco.spec.key;
|
|
3123
|
+
if (key) {
|
|
3124
|
+
const found = decos.find(void 0, void 0, (spec) => spec.key === key);
|
|
3125
|
+
if (found.length > 0 && found[0]) {
|
|
3126
|
+
byId.set(id, found[0]);
|
|
3127
|
+
} else {
|
|
3128
|
+
byId.delete(id);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
const meta = tr.getMeta(embedPluginKey);
|
|
3133
|
+
if (!meta) return { decos, byId, errorIds };
|
|
3134
|
+
switch (meta.type) {
|
|
3135
|
+
case "add": {
|
|
3136
|
+
const chip = makePlaceholderChip(meta.filename);
|
|
3137
|
+
const deco = Decoration2.widget(meta.pos, chip, {
|
|
3138
|
+
key: `embed-placeholder:${meta.id}`,
|
|
3139
|
+
side: -1
|
|
3140
|
+
});
|
|
3141
|
+
decos = decos.add(newState.doc, [deco]);
|
|
3142
|
+
byId.set(meta.id, deco);
|
|
3143
|
+
break;
|
|
3144
|
+
}
|
|
3145
|
+
case "remove": {
|
|
3146
|
+
const key = `embed-placeholder:${meta.id}`;
|
|
3147
|
+
const toRemove = decos.find(void 0, void 0, (spec) => spec.key === key);
|
|
3148
|
+
if (toRemove.length > 0) {
|
|
3149
|
+
decos = decos.remove(toRemove);
|
|
3150
|
+
}
|
|
3151
|
+
byId.delete(meta.id);
|
|
3152
|
+
errorIds.delete(meta.id);
|
|
3153
|
+
break;
|
|
3154
|
+
}
|
|
3155
|
+
case "error": {
|
|
3156
|
+
const placeholderKey = `embed-placeholder:${meta.id}`;
|
|
3157
|
+
const placeholders = decos.find(
|
|
3158
|
+
void 0,
|
|
3159
|
+
void 0,
|
|
3160
|
+
(spec) => spec.key === placeholderKey
|
|
3161
|
+
);
|
|
3162
|
+
if (placeholders.length > 0) {
|
|
3163
|
+
decos = decos.remove(placeholders);
|
|
3164
|
+
}
|
|
3165
|
+
byId.delete(meta.id);
|
|
3166
|
+
const chip = makeErrorChip(meta.message);
|
|
3167
|
+
const errorKey = `embed-error:${meta.id}`;
|
|
3168
|
+
const deco = Decoration2.widget(meta.pos, chip, { key: errorKey, side: 1 });
|
|
3169
|
+
decos = decos.add(newState.doc, [deco]);
|
|
3170
|
+
errorIds.add(meta.id);
|
|
3171
|
+
break;
|
|
3172
|
+
}
|
|
3173
|
+
case "clear-error": {
|
|
3174
|
+
const errorKey = `embed-error:${meta.id}`;
|
|
3175
|
+
const toRemove = decos.find(void 0, void 0, (spec) => spec.key === errorKey);
|
|
3176
|
+
if (toRemove.length > 0) {
|
|
3177
|
+
decos = decos.remove(toRemove);
|
|
3178
|
+
}
|
|
3179
|
+
errorIds.delete(meta.id);
|
|
3180
|
+
break;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
return { decos, byId, errorIds };
|
|
3184
|
+
}
|
|
3185
|
+
},
|
|
3186
|
+
props: {
|
|
3187
|
+
decorations: (state) => embedPluginKey.getState(state)?.decos ?? DecorationSet4.empty,
|
|
3188
|
+
handlePaste: (view, event) => {
|
|
3189
|
+
if (!onEmbedAsset) return false;
|
|
3190
|
+
const files = imageFiles(event.clipboardData?.files);
|
|
3191
|
+
if (files.length === 0) return false;
|
|
3192
|
+
event.preventDefault();
|
|
3193
|
+
const pos = view.state.selection.from;
|
|
3194
|
+
for (const file of files) {
|
|
3195
|
+
embedAsset(view, file, pos, onEmbedAsset);
|
|
3196
|
+
}
|
|
3197
|
+
return true;
|
|
3198
|
+
},
|
|
3199
|
+
handleDrop: (view, event) => {
|
|
3200
|
+
if (!onEmbedAsset) return false;
|
|
3201
|
+
const files = imageFiles(event.dataTransfer?.files);
|
|
3202
|
+
if (files.length === 0) return false;
|
|
3203
|
+
event.preventDefault();
|
|
3204
|
+
const coords = view.posAtCoords({
|
|
3205
|
+
left: event.clientX,
|
|
3206
|
+
top: event.clientY
|
|
3207
|
+
});
|
|
3208
|
+
const pos = coords?.pos ?? view.state.selection.from;
|
|
3209
|
+
for (const file of files) {
|
|
3210
|
+
embedAsset(view, file, pos, onEmbedAsset);
|
|
3211
|
+
}
|
|
3212
|
+
return true;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
});
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
// src/markdown-editor/table-view.tsx
|
|
3220
|
+
import { commandsCtx } from "@milkdown/kit/core";
|
|
3221
|
+
import {
|
|
3222
|
+
addColAfterCommand,
|
|
3223
|
+
addColBeforeCommand,
|
|
3224
|
+
addRowAfterCommand,
|
|
3225
|
+
addRowBeforeCommand,
|
|
3226
|
+
deleteSelectedCellsCommand
|
|
3227
|
+
} from "@milkdown/kit/preset/gfm";
|
|
3228
|
+
import { Plugin as Plugin7, PluginKey as PluginKey6 } from "@milkdown/kit/prose/state";
|
|
3229
|
+
import { $prose as $prose7 } from "@milkdown/kit/utils";
|
|
3230
|
+
import { isInTable } from "@milkdown/kit/prose/tables";
|
|
3231
|
+
import { cn as cn7 } from "@elabs-ai/components-ui/lib/cn";
|
|
3232
|
+
import { usePluginViewContext } from "@prosemirror-adapter/react";
|
|
3233
|
+
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3234
|
+
var ctxByView = /* @__PURE__ */ new WeakMap();
|
|
3235
|
+
function dispatchCommand(view, key) {
|
|
3236
|
+
const ctx2 = ctxByView.get(view);
|
|
3237
|
+
if (!ctx2) return;
|
|
3238
|
+
try {
|
|
3239
|
+
ctx2.get(commandsCtx).call(key);
|
|
3240
|
+
} catch {
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
var tableControlsKey = new PluginKey6("brand-table-controls");
|
|
3244
|
+
function ToolbarButton({
|
|
3245
|
+
onClick,
|
|
3246
|
+
ariaLabel,
|
|
3247
|
+
title,
|
|
3248
|
+
children,
|
|
3249
|
+
variant = "default"
|
|
3250
|
+
}) {
|
|
3251
|
+
return /* @__PURE__ */ jsx11(
|
|
3252
|
+
"button",
|
|
3253
|
+
{
|
|
3254
|
+
type: "button",
|
|
3255
|
+
"aria-label": ariaLabel,
|
|
3256
|
+
title,
|
|
3257
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
3258
|
+
onClick,
|
|
3259
|
+
className: cn7(
|
|
3260
|
+
"inline-flex h-7 items-center gap-1 rounded px-2 text-caption font-medium",
|
|
3261
|
+
"border border-border-strong",
|
|
3262
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
3263
|
+
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
3264
|
+
variant === "destructive" ? "bg-background text-destructive hover:bg-destructive/10" : "bg-background text-foreground hover:bg-surface-muted"
|
|
3265
|
+
),
|
|
3266
|
+
children
|
|
3267
|
+
}
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
function ToolbarDivider() {
|
|
3271
|
+
return /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", className: "mx-0.5 h-4 w-px bg-border-strong" });
|
|
3272
|
+
}
|
|
3273
|
+
function TableControlsView() {
|
|
3274
|
+
const { view } = usePluginViewContext();
|
|
3275
|
+
const inTable = isInTable(view.state);
|
|
3276
|
+
const run = (key) => () => dispatchCommand(view, key);
|
|
3277
|
+
if (!inTable) return null;
|
|
3278
|
+
return /* @__PURE__ */ jsxs7(
|
|
3279
|
+
"div",
|
|
3280
|
+
{
|
|
3281
|
+
role: "toolbar",
|
|
3282
|
+
"aria-label": "Table controls",
|
|
3283
|
+
className: cn7(
|
|
3284
|
+
"flex flex-wrap items-center gap-1 px-2 py-1.5",
|
|
3285
|
+
"border-t border-border-strong bg-surface-muted"
|
|
3286
|
+
),
|
|
3287
|
+
children: [
|
|
3288
|
+
/* @__PURE__ */ jsx11("span", { className: "mr-1 select-none text-caption text-muted-foreground", children: "Row" }),
|
|
3289
|
+
/* @__PURE__ */ jsx11(
|
|
3290
|
+
ToolbarButton,
|
|
3291
|
+
{
|
|
3292
|
+
ariaLabel: "Add row above",
|
|
3293
|
+
title: "Add row above",
|
|
3294
|
+
onClick: run(addRowBeforeCommand.key),
|
|
3295
|
+
children: "\u2191+"
|
|
3296
|
+
}
|
|
3297
|
+
),
|
|
3298
|
+
/* @__PURE__ */ jsx11(
|
|
3299
|
+
ToolbarButton,
|
|
3300
|
+
{
|
|
3301
|
+
ariaLabel: "Add row below",
|
|
3302
|
+
title: "Add row below",
|
|
3303
|
+
onClick: run(addRowAfterCommand.key),
|
|
3304
|
+
children: "\u2193+"
|
|
3305
|
+
}
|
|
3306
|
+
),
|
|
3307
|
+
/* @__PURE__ */ jsx11(
|
|
3308
|
+
ToolbarButton,
|
|
3309
|
+
{
|
|
3310
|
+
ariaLabel: "Delete row",
|
|
3311
|
+
title: "Delete row",
|
|
3312
|
+
variant: "destructive",
|
|
3313
|
+
onClick: run(deleteSelectedCellsCommand.key),
|
|
3314
|
+
children: "\xD7row"
|
|
3315
|
+
}
|
|
3316
|
+
),
|
|
3317
|
+
/* @__PURE__ */ jsx11(ToolbarDivider, {}),
|
|
3318
|
+
/* @__PURE__ */ jsx11("span", { className: "mr-1 select-none text-caption text-muted-foreground", children: "Col" }),
|
|
3319
|
+
/* @__PURE__ */ jsx11(
|
|
3320
|
+
ToolbarButton,
|
|
3321
|
+
{
|
|
3322
|
+
ariaLabel: "Add column left",
|
|
3323
|
+
title: "Add column left",
|
|
3324
|
+
onClick: run(addColBeforeCommand.key),
|
|
3325
|
+
children: "\u2190+"
|
|
3326
|
+
}
|
|
3327
|
+
),
|
|
3328
|
+
/* @__PURE__ */ jsx11(
|
|
3329
|
+
ToolbarButton,
|
|
3330
|
+
{
|
|
3331
|
+
ariaLabel: "Add column right",
|
|
3332
|
+
title: "Add column right",
|
|
3333
|
+
onClick: run(addColAfterCommand.key),
|
|
3334
|
+
children: "\u2192+"
|
|
3335
|
+
}
|
|
3336
|
+
),
|
|
3337
|
+
/* @__PURE__ */ jsx11(
|
|
3338
|
+
ToolbarButton,
|
|
3339
|
+
{
|
|
3340
|
+
ariaLabel: "Delete column",
|
|
3341
|
+
title: "Delete column",
|
|
3342
|
+
variant: "destructive",
|
|
3343
|
+
onClick: run(deleteSelectedCellsCommand.key),
|
|
3344
|
+
children: "\xD7col"
|
|
3345
|
+
}
|
|
3346
|
+
)
|
|
3347
|
+
]
|
|
3348
|
+
}
|
|
3349
|
+
);
|
|
3350
|
+
}
|
|
3351
|
+
function tableViewPlugins(pluginViewFactory) {
|
|
3352
|
+
return [
|
|
3353
|
+
$prose7((ctx2) => {
|
|
3354
|
+
const makePluginView = pluginViewFactory({ component: TableControlsView });
|
|
3355
|
+
return new Plugin7({
|
|
3356
|
+
key: tableControlsKey,
|
|
3357
|
+
state: {
|
|
3358
|
+
// Track whether the cursor is in a table (bool) as plugin state so
|
|
3359
|
+
// ProseMirror knows when to trigger an `update()` on the plugin view.
|
|
3360
|
+
init: (_cfg, state) => isInTable(state),
|
|
3361
|
+
apply: (_tr, _prev, _old, state) => isInTable(state)
|
|
3362
|
+
},
|
|
3363
|
+
// Bind THIS editor's Ctx to THIS editor's view, so the toolbar's
|
|
3364
|
+
// commands dispatch to the right instance when several editors share
|
|
3365
|
+
// this module (autodocs page, split workspace).
|
|
3366
|
+
view: (editorView) => {
|
|
3367
|
+
ctxByView.set(editorView, ctx2);
|
|
3368
|
+
return makePluginView(editorView);
|
|
3369
|
+
}
|
|
3370
|
+
});
|
|
3371
|
+
})
|
|
3372
|
+
];
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
// src/markdown-editor/markdown-editor.tsx
|
|
3376
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
3377
|
+
function scrollHeadingBySlug(editor2, slug) {
|
|
3378
|
+
editor2.action((ctx2) => {
|
|
3379
|
+
const view = ctx2.get(editorViewCtx3);
|
|
3380
|
+
const { doc } = view.state;
|
|
3381
|
+
const used = /* @__PURE__ */ new Map();
|
|
3382
|
+
let targetPos = null;
|
|
3383
|
+
doc.forEach((node, offset) => {
|
|
3384
|
+
if (targetPos !== null) return;
|
|
3385
|
+
if (node.type.name === "heading") {
|
|
3386
|
+
const id = uniqueSlug(slugifyHeading(plainText(node.textContent)), used);
|
|
3387
|
+
if (id === slug) targetPos = offset + 1;
|
|
3388
|
+
}
|
|
3389
|
+
});
|
|
3390
|
+
if (targetPos === null) return;
|
|
3391
|
+
try {
|
|
3392
|
+
const resolved = doc.resolve(targetPos);
|
|
3393
|
+
view.dispatch(view.state.tr.setSelection(TextSelection3.near(resolved)).scrollIntoView());
|
|
3394
|
+
} catch {
|
|
3395
|
+
}
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
var MarkdownEditorView = forwardRef6(function MarkdownEditorView2({
|
|
3399
|
+
initialValue,
|
|
3400
|
+
value,
|
|
3401
|
+
onChange,
|
|
3402
|
+
readOnly,
|
|
3403
|
+
ariaLabel,
|
|
3404
|
+
slashMenu,
|
|
3405
|
+
calc,
|
|
3406
|
+
completions,
|
|
3407
|
+
onEmbedAsset
|
|
3408
|
+
}, ref) {
|
|
3409
|
+
const onChangeRef = useRef7(onChange);
|
|
3410
|
+
onChangeRef.current = onChange;
|
|
3411
|
+
const lastMarkdown = useRef7(initialValue);
|
|
3412
|
+
const nodeViewFactory = useNodeViewFactory();
|
|
3413
|
+
const widgetViewFactory = useWidgetViewFactory();
|
|
3414
|
+
const pluginViewFactory = usePluginViewFactory();
|
|
3415
|
+
const slashEnabled = slashMenu !== false;
|
|
3416
|
+
const slashConfigRef = useRef7(
|
|
3417
|
+
{}
|
|
3418
|
+
);
|
|
3419
|
+
slashConfigRef.current = typeof slashMenu === "object" ? slashMenu : {};
|
|
3420
|
+
const calcEnabled = calc != null;
|
|
3421
|
+
const calcRef = useRef7(calc);
|
|
3422
|
+
calcRef.current = calc;
|
|
3423
|
+
const completionsEnabled = completions != null;
|
|
3424
|
+
const completionsRef = useRef7(completions);
|
|
3425
|
+
completionsRef.current = completions;
|
|
3426
|
+
const onEmbedAssetRef = useRef7(onEmbedAsset);
|
|
3427
|
+
onEmbedAssetRef.current = onEmbedAsset;
|
|
3428
|
+
const iterationEditHandler = useContext5(IterationEditContext);
|
|
3429
|
+
const iterationEditHandlerRef = useRef7(iterationEditHandler);
|
|
3430
|
+
iterationEditHandlerRef.current = iterationEditHandler;
|
|
3431
|
+
const selectionListeners = useRef7(/* @__PURE__ */ new Set()).current;
|
|
3432
|
+
const serializeSliceRef = useRef7(() => "");
|
|
3433
|
+
useEditor(
|
|
3434
|
+
(root) => {
|
|
3435
|
+
const embedPlugin = pasteEmbedPlugin(
|
|
3436
|
+
onEmbedAssetRef.current ? (file) => onEmbedAssetRef.current(file) : void 0
|
|
3437
|
+
);
|
|
3438
|
+
let editor2 = Editor.make().config((ctx2) => {
|
|
3439
|
+
ctx2.set(rootCtx, root);
|
|
3440
|
+
ctx2.set(defaultValueCtx, lastMarkdown.current);
|
|
3441
|
+
ctx2.update(editorViewOptionsCtx, (prev) => ({
|
|
3442
|
+
...prev,
|
|
3443
|
+
editable: () => !readOnly,
|
|
3444
|
+
// Name the ProseMirror `role="textbox"` surface so AT announces it.
|
|
3445
|
+
// Spread prev.attributes so we never clobber Milkdown's own view attrs.
|
|
3446
|
+
attributes: { ...prev.attributes, "aria-label": ariaLabel }
|
|
3447
|
+
}));
|
|
3448
|
+
ctx2.get(listenerCtx).markdownUpdated((_, markdown) => {
|
|
3449
|
+
lastMarkdown.current = markdown;
|
|
3450
|
+
onChangeRef.current?.(markdown);
|
|
3451
|
+
});
|
|
3452
|
+
}).use(commonmark).use(gfm).use(tableViewPlugins(pluginViewFactory)).use(exitKeymapPlugins()).use(history).use(listener).use(directivePlugins).use(directiveViewPlugins(nodeViewFactory)).use(embedPlugin).use(
|
|
3453
|
+
selectionWatchPlugin(
|
|
3454
|
+
() => selectionListeners,
|
|
3455
|
+
() => serializeSliceRef.current
|
|
3456
|
+
)
|
|
3457
|
+
);
|
|
3458
|
+
if (slashEnabled) {
|
|
3459
|
+
editor2 = editor2.use(
|
|
3460
|
+
brandSlashViewPlugins(widgetViewFactory, {
|
|
3461
|
+
...slashConfigRef.current,
|
|
3462
|
+
getIterationEditHandler: () => iterationEditHandlerRef.current
|
|
3463
|
+
})
|
|
3464
|
+
);
|
|
3465
|
+
}
|
|
3466
|
+
if (calcEnabled) {
|
|
3467
|
+
editor2 = editor2.use(calcProsePlugins(() => calcRef.current));
|
|
3468
|
+
}
|
|
3469
|
+
if (completionsEnabled) {
|
|
3470
|
+
editor2 = editor2.use(
|
|
3471
|
+
completionsViewPlugins(widgetViewFactory, () => completionsRef.current)
|
|
3472
|
+
);
|
|
3473
|
+
}
|
|
3474
|
+
return editor2;
|
|
3475
|
+
},
|
|
3476
|
+
[readOnly, ariaLabel, slashEnabled, calcEnabled, completionsEnabled]
|
|
3477
|
+
);
|
|
3478
|
+
const [loading, getInstance] = useInstance();
|
|
3479
|
+
useEffect5(() => {
|
|
3480
|
+
if (loading || value === void 0) return;
|
|
3481
|
+
if (value === lastMarkdown.current) return;
|
|
3482
|
+
const editor2 = getInstance();
|
|
3483
|
+
if (!editor2) return;
|
|
3484
|
+
lastMarkdown.current = value;
|
|
3485
|
+
editor2.action(replaceAll(value));
|
|
3486
|
+
}, [loading, value, getInstance]);
|
|
3487
|
+
useImperativeHandle2(
|
|
3488
|
+
ref,
|
|
3489
|
+
() => {
|
|
3490
|
+
const serializeSlice = (view) => {
|
|
3491
|
+
const editor2 = getInstance();
|
|
3492
|
+
if (!editor2) return "";
|
|
3493
|
+
const { selection } = view.state;
|
|
3494
|
+
if (selection.empty) return "";
|
|
3495
|
+
try {
|
|
3496
|
+
return editor2.action((ctx2) => {
|
|
3497
|
+
const serialize = ctx2.get(serializerCtx2);
|
|
3498
|
+
const doc = selection.content().content;
|
|
3499
|
+
const wrapper = view.state.doc.type.schema.topNodeType.create(null, doc);
|
|
3500
|
+
return serialize(wrapper);
|
|
3501
|
+
}).trim();
|
|
3502
|
+
} catch {
|
|
3503
|
+
return view.state.doc.textBetween(selection.from, selection.to, "\n");
|
|
3504
|
+
}
|
|
3505
|
+
};
|
|
3506
|
+
serializeSliceRef.current = serializeSlice;
|
|
3507
|
+
const parseAndReplace = (view, md) => {
|
|
3508
|
+
const editor2 = getInstance();
|
|
3509
|
+
if (!editor2) return;
|
|
3510
|
+
try {
|
|
3511
|
+
editor2.action((ctx2) => {
|
|
3512
|
+
const parse = ctx2.get(parserCtx3);
|
|
3513
|
+
const parsed = parse(md);
|
|
3514
|
+
if (!parsed) {
|
|
3515
|
+
view.dispatch(view.state.tr.insertText(md));
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
const tr = view.state.tr.replaceSelectionWith(parsed);
|
|
3519
|
+
view.dispatch(tr);
|
|
3520
|
+
});
|
|
3521
|
+
} catch {
|
|
3522
|
+
try {
|
|
3523
|
+
view.dispatch(view.state.tr.insertText(md));
|
|
3524
|
+
} catch {
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
};
|
|
3528
|
+
const getView = () => {
|
|
3529
|
+
const editor2 = getInstance();
|
|
3530
|
+
if (!editor2) return null;
|
|
3531
|
+
try {
|
|
3532
|
+
return editor2.action((ctx2) => ctx2.get(editorViewCtx3));
|
|
3533
|
+
} catch {
|
|
3534
|
+
return null;
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
const access = proseMirrorContentAccess({
|
|
3538
|
+
getView,
|
|
3539
|
+
getText: () => getInstance()?.action(getMarkdown()) ?? lastMarkdown.current,
|
|
3540
|
+
serializeSlice,
|
|
3541
|
+
parseAndReplace,
|
|
3542
|
+
listeners: selectionListeners
|
|
3543
|
+
});
|
|
3544
|
+
return {
|
|
3545
|
+
// EditorContentAccess methods (via proseMirrorContentAccess).
|
|
3546
|
+
getText: access.getText,
|
|
3547
|
+
getSelection: access.getSelection,
|
|
3548
|
+
replaceSelection: access.replaceSelection,
|
|
3549
|
+
insertAtCursor: access.insertAtCursor,
|
|
3550
|
+
focus: access.focus,
|
|
3551
|
+
onSelectionChange: access.onSelectionChange,
|
|
3552
|
+
// Markdown-specific methods.
|
|
3553
|
+
getMarkdown: () => getInstance()?.action(getMarkdown()) ?? lastMarkdown.current,
|
|
3554
|
+
serialized: () => getInstance()?.action(getMarkdown()) ?? null,
|
|
3555
|
+
scrollToHeading: (slug) => {
|
|
3556
|
+
const editor2 = getInstance();
|
|
3557
|
+
if (editor2) scrollHeadingBySlug(editor2, slug);
|
|
3558
|
+
},
|
|
3559
|
+
revealLine: (line, _opts) => {
|
|
3560
|
+
const editor2 = getInstance();
|
|
3561
|
+
if (!editor2) return;
|
|
3562
|
+
const md = editor2.action(getMarkdown());
|
|
3563
|
+
const preceding = parseMarkdownOutline(md).filter((item) => item.line <= line).at(-1);
|
|
3564
|
+
if (preceding) scrollHeadingBySlug(editor2, preceding.id);
|
|
3565
|
+
}
|
|
3566
|
+
};
|
|
3567
|
+
},
|
|
3568
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3569
|
+
[getInstance]
|
|
3570
|
+
);
|
|
3571
|
+
return /* @__PURE__ */ jsx12(Milkdown, {});
|
|
3572
|
+
});
|
|
3573
|
+
var MarkdownEditor = forwardRef6(
|
|
3574
|
+
function MarkdownEditor2({
|
|
3575
|
+
value,
|
|
3576
|
+
defaultValue,
|
|
3577
|
+
onChange,
|
|
3578
|
+
readOnly = false,
|
|
3579
|
+
ariaLabel = "Markdown editor",
|
|
3580
|
+
slashMenu = true,
|
|
3581
|
+
calc,
|
|
3582
|
+
completions,
|
|
3583
|
+
onEmbedAsset,
|
|
3584
|
+
className,
|
|
3585
|
+
style,
|
|
3586
|
+
...props
|
|
3587
|
+
}, ref) {
|
|
3588
|
+
const initialValue = useRef7(value ?? defaultValue ?? "").current;
|
|
3589
|
+
return /* @__PURE__ */ jsx12(
|
|
3590
|
+
"div",
|
|
3591
|
+
{
|
|
3592
|
+
"data-testid": "markdown-editor",
|
|
3593
|
+
className: cn8(
|
|
3594
|
+
// A 1px hairline focus ring (not a heavy 2px ring) — the editable is a
|
|
3595
|
+
// large surface, so a thinner edit-mode ring reads calmer while still
|
|
3596
|
+
// meeting the visible-focus requirement (same `ring` token). (A7)
|
|
3597
|
+
"milkdown-host overflow-auto rounded-md border border-border bg-background text-foreground focus-within:ring-1 focus-within:ring-ring",
|
|
3598
|
+
className
|
|
3599
|
+
),
|
|
3600
|
+
style: { ...markdownScaleVars(), ...style },
|
|
3601
|
+
...props,
|
|
3602
|
+
children: /* @__PURE__ */ jsx12(MilkdownProvider, { children: /* @__PURE__ */ jsx12(ProsemirrorAdapterProvider, { children: /* @__PURE__ */ jsx12(
|
|
3603
|
+
MarkdownEditorView,
|
|
3604
|
+
{
|
|
3605
|
+
ref,
|
|
3606
|
+
initialValue,
|
|
3607
|
+
value,
|
|
3608
|
+
onChange,
|
|
3609
|
+
readOnly,
|
|
3610
|
+
ariaLabel,
|
|
3611
|
+
slashMenu,
|
|
3612
|
+
calc,
|
|
3613
|
+
completions,
|
|
3614
|
+
onEmbedAsset
|
|
3615
|
+
}
|
|
3616
|
+
) }) })
|
|
3617
|
+
}
|
|
3618
|
+
);
|
|
3619
|
+
}
|
|
3620
|
+
);
|
|
3621
|
+
|
|
3622
|
+
// src/copy-button/copy-button.tsx
|
|
3623
|
+
import { Button, useCopyToClipboard } from "@elabs-ai/components-ui";
|
|
3624
|
+
import { cn as cn9 } from "@elabs-ai/components-ui/lib/cn";
|
|
3625
|
+
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
3626
|
+
import { useCallback as useCallback3 } from "react";
|
|
3627
|
+
import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3628
|
+
function CopyButton({ value, label = true, className, ...props }) {
|
|
3629
|
+
const { copied, copy } = useCopyToClipboard();
|
|
3630
|
+
const onClick = useCallback3(() => {
|
|
3631
|
+
void copy(value);
|
|
3632
|
+
}, [copy, value]);
|
|
3633
|
+
return /* @__PURE__ */ jsxs8(
|
|
3634
|
+
Button,
|
|
3635
|
+
{
|
|
3636
|
+
variant: "ghost",
|
|
3637
|
+
size: "sm",
|
|
3638
|
+
...props,
|
|
3639
|
+
type: "button",
|
|
3640
|
+
className: cn9("h-7 gap-1.5", className),
|
|
3641
|
+
onClick,
|
|
3642
|
+
"aria-label": copied ? "Copied" : "Copy",
|
|
3643
|
+
children: [
|
|
3644
|
+
copied ? /* @__PURE__ */ jsx13(
|
|
3645
|
+
CheckIcon,
|
|
3646
|
+
{
|
|
3647
|
+
className: "size-4 text-success animate-in fade-in zoom-in-95 duration-fast ease-entrance",
|
|
3648
|
+
"aria-hidden": "true"
|
|
3649
|
+
},
|
|
3650
|
+
String(copied)
|
|
3651
|
+
) : /* @__PURE__ */ jsx13(CopyIcon, { className: "size-4", "aria-hidden": "true" }),
|
|
3652
|
+
label ? /* @__PURE__ */ jsx13("span", { className: "text-xs", children: copied ? "Copied" : "Copy" }) : null
|
|
3653
|
+
]
|
|
3654
|
+
}
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
// src/lib/editor-content-access.ts
|
|
3659
|
+
function monacoContentAccess(editor2) {
|
|
3660
|
+
const readSelection = () => {
|
|
3661
|
+
const model = editor2.getModel();
|
|
3662
|
+
const selection = editor2.getSelection();
|
|
3663
|
+
if (!model || !selection) return { text: "", empty: true };
|
|
3664
|
+
const text = model.getValueInRange(selection);
|
|
3665
|
+
return { text, empty: selection.isEmpty() };
|
|
3666
|
+
};
|
|
3667
|
+
const applyAtSelection = (text) => {
|
|
3668
|
+
const selection = editor2.getSelection();
|
|
3669
|
+
if (!selection) return;
|
|
3670
|
+
editor2.executeEdits("editor-content-access", [
|
|
3671
|
+
{ range: selection, text, forceMoveMarkers: true }
|
|
3672
|
+
]);
|
|
3673
|
+
editor2.pushUndoStop();
|
|
3674
|
+
};
|
|
3675
|
+
return {
|
|
3676
|
+
getText: () => editor2.getValue(),
|
|
3677
|
+
getSelection: readSelection,
|
|
3678
|
+
replaceSelection: applyAtSelection,
|
|
3679
|
+
insertAtCursor: applyAtSelection,
|
|
3680
|
+
focus: () => editor2.focus(),
|
|
3681
|
+
onSelectionChange: (listener2) => {
|
|
3682
|
+
const sub = editor2.onDidChangeCursorSelection(() => listener2(readSelection()));
|
|
3683
|
+
return () => sub.dispose();
|
|
3684
|
+
}
|
|
3685
|
+
};
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
export {
|
|
3689
|
+
EditorContextMenu,
|
|
3690
|
+
buildBrandThemeData,
|
|
3691
|
+
brandThemeId,
|
|
3692
|
+
applyBrandTheme,
|
|
3693
|
+
useDataTheme,
|
|
3694
|
+
CodeEditor,
|
|
3695
|
+
findCalcFences,
|
|
3696
|
+
calcTokenClassName,
|
|
3697
|
+
identifierPrefix,
|
|
3698
|
+
calcDecorationSpecs,
|
|
3699
|
+
calcInlaySpecs,
|
|
3700
|
+
calcProsePlugins,
|
|
3701
|
+
triggerQueryStart,
|
|
3702
|
+
resolveReplaceRange,
|
|
3703
|
+
collectCompletions,
|
|
3704
|
+
selectionWatchPlugin,
|
|
3705
|
+
proseMirrorContentAccess,
|
|
3706
|
+
MARKDOWN_HEADING_REM,
|
|
3707
|
+
MARKDOWN_HEADING_WEIGHT,
|
|
3708
|
+
MARKDOWN_HEADING_TRACKING,
|
|
3709
|
+
MARKDOWN_MEASURE,
|
|
3710
|
+
markdownScaleVars,
|
|
3711
|
+
IterationEditContext,
|
|
3712
|
+
parseFrontmatter,
|
|
3713
|
+
serializeFrontmatter,
|
|
3714
|
+
parseMarkdownOutline,
|
|
3715
|
+
useMarkdownOutline,
|
|
3716
|
+
DocumentOutline,
|
|
3717
|
+
defaultInterpolate,
|
|
3718
|
+
IterationBlock,
|
|
3719
|
+
ITERATION_LAYOUTS,
|
|
3720
|
+
DEFAULT_TEMPLATE,
|
|
3721
|
+
splitList,
|
|
3722
|
+
parseAttributes,
|
|
3723
|
+
directivePartsFromValue,
|
|
3724
|
+
serializeIterationDirective,
|
|
3725
|
+
builderValueFromParts,
|
|
3726
|
+
parseIterationDirective,
|
|
3727
|
+
emptyBuilderValue,
|
|
3728
|
+
evaluateEmbedded,
|
|
3729
|
+
transposeIterationValue,
|
|
3730
|
+
staticMarkdownFromValue,
|
|
3731
|
+
MetricBlock,
|
|
3732
|
+
insertBrandDirective,
|
|
3733
|
+
insertBasicBlock,
|
|
3734
|
+
CALC_FENCE_SEED,
|
|
3735
|
+
insertCalcFence,
|
|
3736
|
+
resolveCalcInsert,
|
|
3737
|
+
BRAND_SLASH_COMMANDS,
|
|
3738
|
+
filterSlashCommands,
|
|
3739
|
+
groupSlashCommands,
|
|
3740
|
+
DEFAULT_SLASH_SHORTCUT,
|
|
3741
|
+
slashOptionId,
|
|
3742
|
+
SlashMenu,
|
|
3743
|
+
brandSlashViewPlugins,
|
|
3744
|
+
MarkdownEditor,
|
|
3745
|
+
CopyButton,
|
|
3746
|
+
monacoContentAccess
|
|
3747
|
+
};
|
|
3748
|
+
//# sourceMappingURL=chunk-LBC5VJBD.js.map
|