@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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/dist/chunk-LBC5VJBD.js +3748 -0
  4. package/dist/chunk-LBC5VJBD.js.map +1 -0
  5. package/dist/index.css +233 -0
  6. package/dist/index.css.map +1 -0
  7. package/dist/index.d.ts +199 -0
  8. package/dist/index.js +362 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lib/monaco-environment.d.ts +2 -0
  11. package/dist/lib/monaco-environment.js +44 -0
  12. package/dist/lib/monaco-environment.js.map +1 -0
  13. package/dist/markdown/frontmatter.d.ts +11 -0
  14. package/dist/markdown/frontmatter.js +33 -0
  15. package/dist/markdown/frontmatter.js.map +1 -0
  16. package/dist/markdown/index.css +233 -0
  17. package/dist/markdown/index.css.map +1 -0
  18. package/dist/markdown/index.d.ts +1486 -0
  19. package/dist/markdown/index.js +4882 -0
  20. package/dist/markdown/index.js.map +1 -0
  21. package/dist/markdown/parse.d.ts +25 -0
  22. package/dist/markdown/parse.js +14 -0
  23. package/dist/markdown/parse.js.map +1 -0
  24. package/dist/markdown-editor-DfBZibAn.d.ts +702 -0
  25. package/package.json +101 -0
  26. package/src/ai-objects/ai-objects-integration.test.tsx +128 -0
  27. package/src/ai-objects/ai-objects.stories.tsx +233 -0
  28. package/src/ai-objects/ai-objects.test.tsx +205 -0
  29. package/src/ai-objects/decision-card.tsx +178 -0
  30. package/src/ai-objects/directives.ts +136 -0
  31. package/src/ai-objects/entity.tsx +181 -0
  32. package/src/ai-objects/index.ts +34 -0
  33. package/src/ai-objects/knowledge-card.tsx +143 -0
  34. package/src/calc-block/calc-block.stories.tsx +297 -0
  35. package/src/calc-block/calc-block.test.tsx +164 -0
  36. package/src/calc-block/calc-block.tsx +398 -0
  37. package/src/calc-block/calc-editor-monaco.ts +214 -0
  38. package/src/calc-block/calc-editor-prose.ts +128 -0
  39. package/src/calc-block/calc-editor.css +80 -0
  40. package/src/calc-block/calc-editor.stories.tsx +272 -0
  41. package/src/calc-block/calc-editor.test.ts +228 -0
  42. package/src/calc-block/calc-editor.ts +270 -0
  43. package/src/calc-block/calc-inline.stories.tsx +44 -0
  44. package/src/calc-block/calc-inline.test.tsx +44 -0
  45. package/src/calc-block/calc-inline.tsx +75 -0
  46. package/src/calc-block/index.ts +20 -0
  47. package/src/calc-block/types.ts +164 -0
  48. package/src/code-editor/code-editor.stories.tsx +218 -0
  49. package/src/code-editor/code-editor.test.tsx +134 -0
  50. package/src/code-editor/code-editor.tsx +250 -0
  51. package/src/code-editor/index.ts +6 -0
  52. package/src/code-workspace/code-workspace.stories.tsx +51 -0
  53. package/src/code-workspace/code-workspace.test.tsx +146 -0
  54. package/src/code-workspace/code-workspace.tsx +253 -0
  55. package/src/code-workspace/index.ts +6 -0
  56. package/src/copy-button/copy-button.tsx +51 -0
  57. package/src/copy-button/index.ts +1 -0
  58. package/src/css.d.ts +3 -0
  59. package/src/diff-editor/diff-editor.stories.tsx +44 -0
  60. package/src/diff-editor/diff-editor.test.tsx +68 -0
  61. package/src/diff-editor/diff-editor.tsx +151 -0
  62. package/src/diff-editor/index.ts +1 -0
  63. package/src/editor-context-menu/editor-context-menu.tsx +120 -0
  64. package/src/editor-context-menu/index.ts +1 -0
  65. package/src/editor-toolbar/editor-toolbar.tsx +83 -0
  66. package/src/editor-toolbar/index.ts +1 -0
  67. package/src/index.ts +48 -0
  68. package/src/lib/editor-completions-monaco.test.ts +206 -0
  69. package/src/lib/editor-completions-monaco.ts +127 -0
  70. package/src/lib/editor-completions.test.ts +156 -0
  71. package/src/lib/editor-completions.ts +153 -0
  72. package/src/lib/editor-content-access-prose.test.ts +411 -0
  73. package/src/lib/editor-content-access-prose.ts +172 -0
  74. package/src/lib/editor-content-access.test.ts +182 -0
  75. package/src/lib/editor-content-access.ts +109 -0
  76. package/src/lib/languages.ts +32 -0
  77. package/src/lib/markdown/diff.test.ts +80 -0
  78. package/src/lib/markdown/diff.ts +194 -0
  79. package/src/lib/markdown/directives.ts +342 -0
  80. package/src/lib/markdown/frontmatter.ts +50 -0
  81. package/src/lib/markdown/markdown-scale.test.ts +65 -0
  82. package/src/lib/markdown/markdown-scale.ts +62 -0
  83. package/src/lib/markdown/merge.test.ts +64 -0
  84. package/src/lib/markdown/merge.ts +158 -0
  85. package/src/lib/markdown/slugify.test.ts +156 -0
  86. package/src/lib/markdown/slugify.ts +47 -0
  87. package/src/lib/monaco-environment.ts +103 -0
  88. package/src/lib/monaco-theme-bridge.ts +293 -0
  89. package/src/lib/use-data-theme.ts +61 -0
  90. package/src/markdown/frontmatter.ts +14 -0
  91. package/src/markdown/index.ts +323 -0
  92. package/src/markdown/parse.test.ts +60 -0
  93. package/src/markdown/parse.ts +38 -0
  94. package/src/markdown-academic/citations.test.ts +77 -0
  95. package/src/markdown-academic/citations.tsx +442 -0
  96. package/src/markdown-academic/footnotes.tsx +235 -0
  97. package/src/markdown-academic/index.ts +32 -0
  98. package/src/markdown-academic/math.tsx +163 -0
  99. package/src/markdown-academic/toc.tsx +88 -0
  100. package/src/markdown-editor/completions/completions-menu.tsx +96 -0
  101. package/src/markdown-editor/completions/completions-prose.test.ts +356 -0
  102. package/src/markdown-editor/completions/completions-prose.ts +313 -0
  103. package/src/markdown-editor/completions/completions-widget.tsx +75 -0
  104. package/src/markdown-editor/completions/index.ts +37 -0
  105. package/src/markdown-editor/directive-nodes.ts +151 -0
  106. package/src/markdown-editor/directive-views.test.tsx +189 -0
  107. package/src/markdown-editor/directive-views.tsx +795 -0
  108. package/src/markdown-editor/exit-keymap.test.ts +88 -0
  109. package/src/markdown-editor/exit-keymap.ts +87 -0
  110. package/src/markdown-editor/index.ts +6 -0
  111. package/src/markdown-editor/markdown-editor.css +250 -0
  112. package/src/markdown-editor/markdown-editor.directives.test.tsx +105 -0
  113. package/src/markdown-editor/markdown-editor.fill.test.ts +34 -0
  114. package/src/markdown-editor/markdown-editor.paste-embed.test.tsx +297 -0
  115. package/src/markdown-editor/markdown-editor.stories.tsx +560 -0
  116. package/src/markdown-editor/markdown-editor.strictmode.test.tsx +38 -0
  117. package/src/markdown-editor/markdown-editor.table.test.tsx +287 -0
  118. package/src/markdown-editor/markdown-editor.tsx +560 -0
  119. package/src/markdown-editor/milkdown-react/editor.tsx +33 -0
  120. package/src/markdown-editor/milkdown-react/index.ts +8 -0
  121. package/src/markdown-editor/milkdown-react/types.ts +27 -0
  122. package/src/markdown-editor/milkdown-react/use-editor.ts +27 -0
  123. package/src/markdown-editor/milkdown-react/use-get-editor.ts +56 -0
  124. package/src/markdown-editor/milkdown-react/use-instance.ts +23 -0
  125. package/src/markdown-editor/paste-embed.ts +355 -0
  126. package/src/markdown-editor/slash/brand-slash-commands.test.ts +194 -0
  127. package/src/markdown-editor/slash/brand-slash-commands.ts +324 -0
  128. package/src/markdown-editor/slash/brand-slash-plugin.test.ts +254 -0
  129. package/src/markdown-editor/slash/brand-slash-plugin.ts +385 -0
  130. package/src/markdown-editor/slash/index.ts +107 -0
  131. package/src/markdown-editor/slash/insert-directive.test.ts +263 -0
  132. package/src/markdown-editor/slash/insert-directive.ts +345 -0
  133. package/src/markdown-editor/slash/monaco-slash-menu.test.tsx +198 -0
  134. package/src/markdown-editor/slash/monaco-slash-menu.tsx +356 -0
  135. package/src/markdown-editor/slash/shortcut-monaco.ts +69 -0
  136. package/src/markdown-editor/slash/shortcut.test.ts +157 -0
  137. package/src/markdown-editor/slash/shortcut.ts +57 -0
  138. package/src/markdown-editor/slash/slash-menu.stories.tsx +139 -0
  139. package/src/markdown-editor/slash/slash-menu.tsx +115 -0
  140. package/src/markdown-editor/slash/slash-scroll.test.tsx +52 -0
  141. package/src/markdown-editor/slash/slash-widget.tsx +97 -0
  142. package/src/markdown-editor/slash/source-slash-trigger.test.ts +42 -0
  143. package/src/markdown-editor/slash/source-slash-trigger.ts +46 -0
  144. package/src/markdown-editor/table-view.tsx +249 -0
  145. package/src/markdown-iteration/directive.tsx +92 -0
  146. package/src/markdown-iteration/edit-context.ts +56 -0
  147. package/src/markdown-iteration/index.ts +24 -0
  148. package/src/markdown-iteration/iteration-block.test.tsx +138 -0
  149. package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +293 -0
  150. package/src/markdown-iteration/iteration-builder-dialog.tsx +312 -0
  151. package/src/markdown-iteration/iteration-builder.test.ts +252 -0
  152. package/src/markdown-iteration/iteration-builder.ts +303 -0
  153. package/src/markdown-iteration/iteration.test.ts +56 -0
  154. package/src/markdown-iteration/iteration.tsx +333 -0
  155. package/src/markdown-iteration/template-dialog.stories.tsx +63 -0
  156. package/src/markdown-iteration/template-dialog.test.tsx +104 -0
  157. package/src/markdown-iteration/template-dialog.tsx +116 -0
  158. package/src/markdown-outline/document-outline.stories.tsx +76 -0
  159. package/src/markdown-outline/document-outline.tsx +83 -0
  160. package/src/markdown-outline/index.ts +2 -0
  161. package/src/markdown-outline/markdown-outline.test.ts +49 -0
  162. package/src/markdown-outline/markdown-outline.ts +60 -0
  163. package/src/markdown-preview/code-fence.tsx +193 -0
  164. package/src/markdown-preview/index.ts +8 -0
  165. package/src/markdown-preview/markdown-preview-academic.stories.tsx +203 -0
  166. package/src/markdown-preview/markdown-preview-academic.test.tsx +257 -0
  167. package/src/markdown-preview/markdown-preview-iteration-calc.test.tsx +71 -0
  168. package/src/markdown-preview/markdown-preview-iteration-nested.test.tsx +58 -0
  169. package/src/markdown-preview/markdown-preview-iteration.stories.tsx +187 -0
  170. package/src/markdown-preview/markdown-preview-iteration.test.tsx +90 -0
  171. package/src/markdown-preview/markdown-preview-linking.test.tsx +61 -0
  172. package/src/markdown-preview/markdown-preview-transclusion.test.tsx +33 -0
  173. package/src/markdown-preview/markdown-preview.stories.tsx +108 -0
  174. package/src/markdown-preview/markdown-preview.test.tsx +411 -0
  175. package/src/markdown-preview/markdown-preview.tsx +1541 -0
  176. package/src/markdown-toolbar/index.ts +8 -0
  177. package/src/markdown-toolbar/markdown-commands.test.ts +84 -0
  178. package/src/markdown-toolbar/markdown-commands.ts +104 -0
  179. package/src/markdown-toolbar/markdown-toolbar.stories.tsx +56 -0
  180. package/src/markdown-toolbar/markdown-toolbar.tsx +263 -0
  181. package/src/markdown-workspace/focus-writing.test.ts +52 -0
  182. package/src/markdown-workspace/focus-writing.ts +38 -0
  183. package/src/markdown-workspace/index.ts +6 -0
  184. package/src/markdown-workspace/markdown-workspace.stories.tsx +688 -0
  185. package/src/markdown-workspace/markdown-workspace.test.tsx +326 -0
  186. package/src/markdown-workspace/markdown-workspace.tsx +787 -0
  187. package/src/mermaid-diagram/index.ts +1 -0
  188. package/src/mermaid-diagram/mermaid-diagram.stories.tsx +42 -0
  189. package/src/mermaid-diagram/mermaid-diagram.test.tsx +86 -0
  190. package/src/mermaid-diagram/mermaid-diagram.tsx +318 -0
  191. package/src/mermaid-diagram/mermaid-viewer.test.tsx +89 -0
  192. package/src/mermaid-diagram/mermaid-viewer.tsx +367 -0
  193. package/src/mermaid-diagram/real-parse.test.ts +42 -0
  194. package/src/mermaid-diagram/remediate.test.ts +60 -0
  195. package/src/mermaid-diagram/remediate.ts +124 -0
  196. package/src/mermaid-workspace/index.ts +1 -0
  197. package/src/mermaid-workspace/mermaid-workspace.stories.tsx +28 -0
  198. package/src/mermaid-workspace/mermaid-workspace.tsx +71 -0
  199. package/src/metric-block/index.ts +1 -0
  200. package/src/metric-block/metric-block.stories.tsx +40 -0
  201. package/src/metric-block/metric-block.tsx +14 -0
  202. package/src/monaco-workers.d.ts +10 -0
  203. package/src/prose/index.ts +14 -0
  204. package/src/prose/prose.stories.tsx +44 -0
  205. package/src/prose/prose.test.tsx +44 -0
  206. package/src/prose/prose.tsx +21 -0
  207. 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