@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,128 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Milkdown / ProseMirror calc layer (#220) — live highlighting + result inlays for
5
+ * ```calc fences inside the WYSIWYG editor.
6
+ *
7
+ * A self-owned raw `$prose` plugin (zero new deps) that builds a `DecorationSet`
8
+ * over every `code_block` whose language is `calc`:
9
+ * - inline decorations color each token from the `--calc-*` tokens (calc-editor.css);
10
+ * - a widget decoration after each line shows the computed result inlay.
11
+ * The set is recomputed when the document changes. Hooks are read through a getter
12
+ * so a fresh `calc` prop identity never rebuilds the editor (mirrors `slashConfigRef`).
13
+ *
14
+ * The position math reuses the engine-neutral helpers in `calc-editor.ts`; a
15
+ * ProseMirror code block stores its body as plain text with literal `\n`, so a
16
+ * line's absolute position is `codeBlockContentStart + lineOffset + column`.
17
+ */
18
+ import type { MilkdownPlugin } from "@milkdown/kit/ctx";
19
+ import type { Node as ProseNode } from "@milkdown/kit/prose/model";
20
+ import { Plugin, PluginKey } from "@milkdown/kit/prose/state";
21
+ import { Decoration, DecorationSet } from "@milkdown/kit/prose/view";
22
+ import { $prose } from "@milkdown/kit/utils";
23
+
24
+ import "./calc-editor.css";
25
+
26
+ import {
27
+ CALC_FENCE_LANG,
28
+ calcLineLayout,
29
+ calcTokenClassName,
30
+ resolveHighlight,
31
+ resolveInlays,
32
+ type CalcInlay,
33
+ } from "./calc-editor";
34
+ import type { CalcEditorHooks } from "./types";
35
+
36
+ const clamp = (n: number, lo: number, hi: number): number => Math.min(hi, Math.max(lo, n));
37
+
38
+ const calcDecorationKey = new PluginKey<DecorationSet>("brand-calc-decorations");
39
+
40
+ /**
41
+ * Build the inline result-inlay widget DOM (non-editable, announced as text).
42
+ *
43
+ * a11y: the computed value is rendered as TEXT (never color-only) and is NOT
44
+ * `aria-hidden`, so AT reads it in browse mode; a native `title` labels it for
45
+ * mouse users. It is a ProseMirror decoration (not a model node), so a host that
46
+ * must guarantee results to AT in active-editing/forms mode should also surface
47
+ * them via a `role="status"` live region outside the editor.
48
+ */
49
+ function inlayWidget(inlay: CalcInlay): HTMLElement {
50
+ const span = document.createElement("span");
51
+ span.className = inlay.isError ? "brand-calc-inlay brand-calc-inlay--error" : "brand-calc-inlay";
52
+ // The number itself is the signal (rendered as text); color is enhancement only.
53
+ span.textContent = inlay.text;
54
+ span.setAttribute("contenteditable", "false");
55
+ span.setAttribute("title", inlay.isError ? "Calc error" : "Calc result");
56
+ return span;
57
+ }
58
+
59
+ /** Walk the doc, decorating every ```calc code block with highlight + inlays. */
60
+ function buildCalcDecorations(doc: ProseNode, hooks: CalcEditorHooks | undefined): DecorationSet {
61
+ if (!hooks || (!hooks.tokenize && !hooks.evaluate)) return DecorationSet.empty;
62
+ const decorations: Decoration[] = [];
63
+
64
+ doc.descendants((node, pos) => {
65
+ if (node.type.name !== "code_block" || node.attrs.language !== CALC_FENCE_LANG) {
66
+ return undefined;
67
+ }
68
+ const source = node.textContent;
69
+ const contentStart = pos + 1; // first char inside the code block
70
+ const layout = calcLineLayout(source);
71
+ const tokensByLine = resolveHighlight(hooks, source);
72
+
73
+ layout.forEach((span, i) => {
74
+ const lineStart = contentStart + span.offset;
75
+ for (const t of tokensByLine[i] ?? []) {
76
+ const start = clamp(t.start, 0, span.text.length);
77
+ const end = clamp(t.end, start, span.text.length);
78
+ if (end <= start) continue;
79
+ decorations.push(
80
+ Decoration.inline(lineStart + start, lineStart + end, {
81
+ class: calcTokenClassName(t.kind, t.resolved),
82
+ }),
83
+ );
84
+ }
85
+ });
86
+
87
+ for (const inlay of resolveInlays(hooks, source)) {
88
+ const span = layout[inlay.lineNumber - 1];
89
+ if (!span) continue;
90
+ const at = contentStart + span.offset + span.text.length;
91
+ decorations.push(
92
+ Decoration.widget(at, () => inlayWidget(inlay), {
93
+ side: 1,
94
+ ignoreSelection: true,
95
+ key: `brand-calc-inlay:${String(inlay.lineNumber)}:${inlay.text}`,
96
+ }),
97
+ );
98
+ }
99
+ return false; // a code block's content is plain text — don't descend
100
+ });
101
+
102
+ return DecorationSet.create(doc, decorations);
103
+ }
104
+
105
+ /**
106
+ * Build the calc decoration plugin. `getHooks` is read on every recompute so the
107
+ * editor never rebuilds when the `calc` prop identity changes. Returns a Milkdown
108
+ * plugin array to `.use()` (only when calc authoring is enabled).
109
+ */
110
+ export function calcProsePlugins(getHooks: () => CalcEditorHooks | undefined): MilkdownPlugin[] {
111
+ const plugin = $prose(
112
+ () =>
113
+ new Plugin<DecorationSet>({
114
+ key: calcDecorationKey,
115
+ state: {
116
+ init: (_config, state) => buildCalcDecorations(state.doc, getHooks()),
117
+ apply: (tr, value, _old, newState) =>
118
+ tr.docChanged ? buildCalcDecorations(newState.doc, getHooks()) : value,
119
+ },
120
+ props: {
121
+ decorations(state) {
122
+ return calcDecorationKey.getState(state);
123
+ },
124
+ },
125
+ }),
126
+ );
127
+ return [plugin as unknown as MilkdownPlugin];
128
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Calc authoring decorations (#220) — shared by the Monaco source surface
3
+ * (decoration `inlineClassName`) and the Milkdown WYSIWYG surface (ProseMirror
4
+ * inline decorations + result-inlay widgets).
5
+ *
6
+ * Colors come from the `--calc-*` syntax tokens — the SAME tokens `CalcBlock`
7
+ * paints with (#221), already AA-verified against `--background` (the editor
8
+ * surface) and `--card` in all three themes — so highlighting tracks the theme via
9
+ * the cascade with no hardcoded hex. The compound `.brand-calc-tok.brand-calc-tok--x`
10
+ * selector raises specificity above Monaco's own token color class (`.mtkN`) so
11
+ * the calc color wins regardless of stylesheet load order (no `!important`).
12
+ *
13
+ * Hue is never the SOLE cue (var-def = weight, unresolved = dotted underline), so
14
+ * roles stay distinct in the high-contrast theme where the calc colors collapse
15
+ * toward the foreground — mirroring `CalcBlock.tokenClass`.
16
+ */
17
+
18
+ .brand-calc-tok.brand-calc-tok--number,
19
+ .brand-calc-tok.brand-calc-tok--constant {
20
+ color: var(--calc-number);
21
+ }
22
+ .brand-calc-tok.brand-calc-tok--unit {
23
+ color: var(--calc-unit);
24
+ }
25
+ .brand-calc-tok.brand-calc-tok--currency {
26
+ color: var(--calc-currency);
27
+ }
28
+ .brand-calc-tok.brand-calc-tok--operator {
29
+ color: var(--calc-operator);
30
+ }
31
+ .brand-calc-tok.brand-calc-tok--function {
32
+ color: var(--calc-function);
33
+ }
34
+ .brand-calc-tok.brand-calc-tok--var-ref {
35
+ color: var(--calc-variable);
36
+ }
37
+ .brand-calc-tok.brand-calc-tok--var-def {
38
+ color: var(--calc-variable);
39
+ font-weight: 500;
40
+ }
41
+ .brand-calc-tok.brand-calc-tok--line-ref {
42
+ color: var(--calc-reference);
43
+ font-variant-numeric: tabular-nums;
44
+ }
45
+ .brand-calc-tok.brand-calc-tok--comment {
46
+ color: var(--calc-comment);
47
+ font-style: italic;
48
+ }
49
+ .brand-calc-tok.brand-calc-tok--unknown {
50
+ color: var(--calc-warning);
51
+ }
52
+
53
+ /* Non-hue cue for unresolved identifiers / dangling refs — carries the role in
54
+ high-contrast, where hue is flattened. */
55
+ .brand-calc-tok.brand-calc-tok--unresolved {
56
+ text-decoration: underline dotted;
57
+ text-decoration-thickness: 1px;
58
+ text-underline-offset: 2px;
59
+ }
60
+
61
+ /**
62
+ * WYSIWYG result inlay — a ProseMirror widget after each calc line showing the
63
+ * computed answer. The number IS the signal (shown as text), color is only an
64
+ * enhancement, and it is non-editable / non-selectable so it never enters the
65
+ * document. (The Monaco surface uses Monaco's own inlay hints, themed via the
66
+ * theme bridge's `editorInlayHint.*` colors — not this class.)
67
+ */
68
+ .brand-calc-inlay {
69
+ /* Full strength (no opacity de-emphasis): the result is the answer, and the
70
+ `--calc-result` token is AA-verified at FULL opacity (#221) — reducing it
71
+ would drop effective contrast below what the contrast gate models. */
72
+ margin-inline-start: 1ch;
73
+ color: var(--calc-result);
74
+ font-variant-numeric: tabular-nums;
75
+ user-select: none;
76
+ white-space: pre;
77
+ }
78
+ .brand-calc-inlay--error {
79
+ color: var(--calc-warning);
80
+ }
@@ -0,0 +1,272 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { MarkdownEditor } from "../markdown-editor";
5
+ import { MarkdownWorkspace } from "../markdown-workspace";
6
+ import type {
7
+ CalcComplete,
8
+ CalcCompletion,
9
+ CalcEditorHooks,
10
+ CalcToken,
11
+ CalcTokenize,
12
+ EvaluateCalc,
13
+ } from "./types";
14
+
15
+ /**
16
+ * Calc AUTHORING (#220) — the editor side of calc. The same `--calc-*` tokens
17
+ * `CalcBlock` renders with now drive LIVE highlighting + autocomplete + result
18
+ * inlays inside `calc` fences, in BOTH the Monaco source surface and the Milkdown
19
+ * WYSIWYG surface. Opt in via the `calc` prop with the consumer's hooks — the
20
+ * library decorates, the consumer computes (no calc engine is bundled).
21
+ *
22
+ * The `demoEngine` below stands in for the consumer's real evaluator (a tiny
23
+ * arithmetic + currency engine); it is intentionally OUTSIDE the package.
24
+ */
25
+ const meta = {
26
+ title: "Editor/CalcEditor",
27
+ component: MarkdownWorkspace,
28
+ tags: ["autodocs"],
29
+ parameters: { layout: "fullscreen" },
30
+ } satisfies Meta<typeof MarkdownWorkspace>;
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof meta>;
34
+
35
+ /* ----------------------------- demo calc engine ---------------------------- */
36
+ /* A toy arithmetic + currency engine — the CONSUMER's domain logic, here only to
37
+ make the stories live. A real app supplies its own `tokenize`/`evaluate`/`complete`. */
38
+
39
+ const CURRENCIES: Record<string, string> = { EUR: "€", USD: "$", GBP: "£" };
40
+ const FUNCTIONS = ["sum", "avg", "min", "max", "round"];
41
+
42
+ function createDemoEngine(): Required<CalcEditorHooks> {
43
+ /** Var names seen by the latest evaluate — lets `tokenize` mark dangling refs. */
44
+ const defined = new Set<string>();
45
+
46
+ const tokenize: CalcTokenize = (line) => {
47
+ const tokens: CalcToken[] = [];
48
+ const commentAt = line.indexOf("//");
49
+ const code = commentAt >= 0 ? line.slice(0, commentAt) : line;
50
+ if (commentAt >= 0) {
51
+ tokens.push({ start: commentAt, end: line.length, kind: "comment", resolved: true });
52
+ }
53
+ const isDef = /^\s*[A-Za-z_]\w*\s*=/.test(code);
54
+ let seenEq = false;
55
+ const re = /([A-Za-z_]\w*)|(\d[\d,]*(?:\.\d+)?)|([-+*/=()])|(%)/g;
56
+ let m: RegExpExecArray | null;
57
+ while ((m = re.exec(code))) {
58
+ const text = m[0];
59
+ const start = m.index;
60
+ const end = start + text.length;
61
+ if (m[1]) {
62
+ if (text in CURRENCIES) tokens.push({ start, end, kind: "currency", resolved: true });
63
+ else if (FUNCTIONS.includes(text))
64
+ tokens.push({ start, end, kind: "function", resolved: true });
65
+ else if (isDef && !seenEq) tokens.push({ start, end, kind: "var-def", resolved: true });
66
+ else tokens.push({ start, end, kind: "var-ref", resolved: defined.has(text) });
67
+ } else if (m[2]) {
68
+ tokens.push({ start, end, kind: "number", resolved: true });
69
+ } else if (m[3]) {
70
+ if (text === "=") seenEq = true;
71
+ tokens.push({ start, end, kind: "operator", resolved: true });
72
+ } else if (m[4]) {
73
+ tokens.push({ start, end, kind: "unit", resolved: true });
74
+ }
75
+ }
76
+ return tokens.sort((a, b) => a.start - b.start);
77
+ };
78
+
79
+ /** Evaluate one expression string against the numeric env (precedence-correct). */
80
+ function evalExpr(expr: string, env: Map<string, number>): number {
81
+ const toks =
82
+ expr.replace(/\b(EUR|USD|GBP)\b/g, " ").match(/\d[\d,]*\.?\d*|[A-Za-z_]\w*|[-+*/()]/g) ?? [];
83
+ let i = 0;
84
+ const peek = () => toks[i];
85
+ const prim = (): number => {
86
+ const t = toks[i++];
87
+ if (t === "(") {
88
+ const v = addsub();
89
+ if (peek() === ")") i++;
90
+ return v;
91
+ }
92
+ if (t && /^\d/.test(t)) return Number(t.replace(/,/g, ""));
93
+ const v = t ? env.get(t) : undefined;
94
+ if (v === undefined) throw new Error(`Undefined symbol "${t ?? "?"}"`);
95
+ return v;
96
+ };
97
+ const muldiv = (): number => {
98
+ let v = prim();
99
+ while (peek() === "*" || peek() === "/") {
100
+ const op = toks[i++];
101
+ const r = prim();
102
+ v = op === "*" ? v * r : v / r;
103
+ }
104
+ return v;
105
+ };
106
+ function addsub(): number {
107
+ let v = muldiv();
108
+ while (peek() === "+" || peek() === "-") {
109
+ const op = toks[i++];
110
+ const r = muldiv();
111
+ v = op === "+" ? v + r : v - r;
112
+ }
113
+ return v;
114
+ }
115
+ return addsub();
116
+ }
117
+
118
+ const evaluate: EvaluateCalc = (source) => {
119
+ const env = new Map<string, number>();
120
+ const units = new Map<string, string>();
121
+ defined.clear();
122
+ const results = source.split("\n").map((raw, idx) => {
123
+ const line = idx + 1;
124
+ const tokens = tokenize(raw);
125
+ const code = raw.includes("//") ? raw.slice(0, raw.indexOf("//")) : raw;
126
+ const trimmed = code.trim();
127
+ if (trimmed === "" || trimmed.startsWith("#")) return { line, tokens };
128
+ const def = /^\s*([A-Za-z_]\w*)\s*=\s*(.+)$/.exec(code);
129
+ const name = def?.[1];
130
+ const expr = def ? def[2] : code;
131
+ const curMatch = /\b(EUR|USD|GBP)\b/.exec(expr);
132
+ try {
133
+ const value = evalExpr(expr, env);
134
+ // Unit: an explicit currency in the line, else inherited from a referenced var.
135
+ let unit = curMatch?.[1];
136
+ if (!unit) {
137
+ for (const id of expr.match(/[A-Za-z_]\w*/g) ?? []) {
138
+ if (units.has(id)) {
139
+ unit = units.get(id);
140
+ break;
141
+ }
142
+ }
143
+ }
144
+ if (name) {
145
+ env.set(name, value);
146
+ defined.add(name);
147
+ if (unit) units.set(name, unit);
148
+ }
149
+ const display = unit
150
+ ? `${CURRENCIES[unit] ?? ""}${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
151
+ : value.toLocaleString("en-US", { maximumFractionDigits: 2 });
152
+ return {
153
+ line,
154
+ tokens,
155
+ value: {
156
+ kind: unit ? ("currency" as const) : ("number" as const),
157
+ raw: value,
158
+ unit,
159
+ display,
160
+ },
161
+ };
162
+ } catch (err) {
163
+ return { line, tokens, error: { message: (err as Error).message } };
164
+ }
165
+ });
166
+ const totals = results.filter((r) => r.value).map((r) => Number(r.value!.raw));
167
+ const last = results.filter((r) => r.value).at(-1);
168
+ return {
169
+ results,
170
+ total: last?.value
171
+ ? { ...last.value }
172
+ : totals.length
173
+ ? {
174
+ kind: "number",
175
+ raw: totals.reduce((a, b) => a + b, 0),
176
+ display: String(totals.reduce((a, b) => a + b, 0)),
177
+ }
178
+ : undefined,
179
+ };
180
+ };
181
+
182
+ const complete: CalcComplete = ({ source, prefix }) => {
183
+ const vars = new Set<string>();
184
+ for (const line of source.split("\n")) {
185
+ const m = /^\s*([A-Za-z_]\w*)\s*=/.exec(line);
186
+ if (m?.[1]) vars.add(m[1]);
187
+ }
188
+ const items: CalcCompletion[] = [];
189
+ for (const v of vars) items.push({ label: v, insert: v, kind: "variable", detail: "variable" });
190
+ for (const f of FUNCTIONS)
191
+ items.push({ label: f, insert: `${f}()`, kind: "function", detail: "function" });
192
+ for (const c of Object.keys(CURRENCIES))
193
+ items.push({ label: c, insert: c, kind: "currency", detail: "currency unit" });
194
+ const p = prefix.toLowerCase();
195
+ return p ? items.filter((i) => i.label.toLowerCase().startsWith(p)) : items;
196
+ };
197
+
198
+ return { tokenize, evaluate, complete };
199
+ }
200
+
201
+ const demoEngine = createDemoEngine();
202
+
203
+ const DOC = [
204
+ "# Trip budget",
205
+ "",
206
+ "Live numbers for the Lisbon trip — edit a value and the answers update.",
207
+ "",
208
+ "```calc",
209
+ "flights = 320 EUR",
210
+ "hotel = 95 EUR * 4",
211
+ "transit = 60 EUR",
212
+ "total = flights + hotel + transit",
213
+ "mystery = ghost + 1 // a dangling ref",
214
+ "```",
215
+ "",
216
+ "The fence above is highlighted and shows each line's result inline.",
217
+ ].join("\n");
218
+
219
+ function Frame({ children }: { children: ReactNode }) {
220
+ return <div className="h-[520px] bg-background p-4 text-foreground">{children}</div>;
221
+ }
222
+
223
+ /**
224
+ * The hybrid workspace in SPLIT mode: the Monaco source pane highlights the
225
+ * `calc` fence + shows result inlays, and the preview renders the same fence as
226
+ * a branded `CalcBlock`. Type inside the fence (e.g. `flights`) for completions.
227
+ */
228
+ export const Workspace: Story = {
229
+ render: () => (
230
+ <Frame>
231
+ <MarkdownWorkspace defaultValue={DOC} defaultMode="split" calc={demoEngine} />
232
+ </Frame>
233
+ ),
234
+ };
235
+
236
+ /**
237
+ * SOURCE mode — the Monaco surface alone: calc highlighting, end-of-line result
238
+ * inlays, and autocomplete (Ctrl+Space or type a variable prefix inside the fence).
239
+ */
240
+ export const SourceEditor: Story = {
241
+ render: () => (
242
+ <Frame>
243
+ <MarkdownWorkspace defaultValue={DOC} defaultMode="source" calc={demoEngine} />
244
+ </Frame>
245
+ ),
246
+ };
247
+
248
+ /**
249
+ * The Milkdown WYSIWYG surface — the `calc` fence renders as an editable code
250
+ * block with the SAME calc highlighting + result inlays via a ProseMirror
251
+ * decoration plugin. Type `/calc` on a new line to insert a fresh fence.
252
+ */
253
+ export const Wysiwyg: Story = {
254
+ render: () => (
255
+ <Frame>
256
+ <MarkdownEditor defaultValue={DOC} calc={demoEngine} className="h-full" />
257
+ </Frame>
258
+ ),
259
+ };
260
+
261
+ /** Highlight + inlays with NO `complete` hook — a consumer who only wrote `evaluate`. */
262
+ export const EvaluateOnly: Story = {
263
+ render: () => (
264
+ <Frame>
265
+ <MarkdownWorkspace
266
+ defaultValue={DOC}
267
+ defaultMode="split"
268
+ calc={{ evaluate: demoEngine.evaluate }}
269
+ />
270
+ </Frame>
271
+ ),
272
+ };