@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,293 @@
1
+ "use client";
2
+
3
+ import type * as Monaco from "monaco-editor";
4
+ import { oklchToHex, resolveThemeIsDark, type ThemeName } from "@elabs-ai/components-tokens";
5
+
6
+ /**
7
+ * Bridges brand-ui's semantic tokens onto Monaco's theming API so the editor
8
+ * surface AND Monaco's own widgets (suggestion dropdown, find box, hovers,
9
+ * context menu) are recolored from the active brand theme.
10
+ *
11
+ * Why this exists: brand tokens are authored in `oklch(...)` but
12
+ * `monaco.editor.defineTheme` only accepts hex. The shared `oklchToHex`
13
+ * (`@elabs-ai/components-tokens`, ADR 0015) converts the oklch tokens dependency-free; a 1×1
14
+ * canvas rasterize remains only as the fallback for non-oklch CSS colors
15
+ * (`rgb()`, named), since `getComputedStyle` does NOT serialize `oklch()` to
16
+ * `rgb()` in Chromium. Results are cached per raw token value.
17
+ */
18
+
19
+ const hexCache = new Map<string, string>();
20
+ let ctx: CanvasRenderingContext2D | null = null;
21
+
22
+ /** Normalize any CSS color string (incl. `oklch(...)`) to `#rrggbb` / `#rrggbbaa`. */
23
+ function resolveCssColor(value: string, fallback = "#000000"): string {
24
+ const raw = value.trim();
25
+ if (!raw) return fallback;
26
+ if (/^#([0-9a-f]{3,8})$/i.test(raw)) return raw;
27
+ const cached = hexCache.get(raw);
28
+ if (cached) return cached;
29
+ const viaOklch = oklchToHex(raw);
30
+ if (viaOklch) {
31
+ hexCache.set(raw, viaOklch);
32
+ return viaOklch;
33
+ }
34
+ if (typeof document === "undefined") return fallback;
35
+
36
+ if (!ctx) {
37
+ const canvas = document.createElement("canvas");
38
+ canvas.width = 1;
39
+ canvas.height = 1;
40
+ ctx = canvas.getContext("2d", { willReadFrequently: true });
41
+ }
42
+ if (!ctx) return fallback;
43
+
44
+ // An invalid color leaves `fillStyle` unchanged, so prime it with the fallback.
45
+ ctx.fillStyle = "#000000";
46
+ ctx.fillStyle = fallback;
47
+ ctx.fillStyle = raw;
48
+ ctx.clearRect(0, 0, 1, 1);
49
+ ctx.fillRect(0, 0, 1, 1);
50
+ const data = ctx.getImageData(0, 0, 1, 1).data;
51
+ const r = data[0] ?? 0;
52
+ const g = data[1] ?? 0;
53
+ const b = data[2] ?? 0;
54
+ const a = data[3] ?? 255;
55
+ const hex =
56
+ a < 255 ? `#${byte(r)}${byte(g)}${byte(b)}${byte(a)}` : `#${byte(r)}${byte(g)}${byte(b)}`;
57
+ hexCache.set(raw, hex);
58
+ return hex;
59
+ }
60
+ const clamp01 = (n: number) => (Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 1);
61
+ const byte = (n: number) => n.toString(16).padStart(2, "0");
62
+
63
+ /** Mix `alpha` (0..1) into a `#rrggbb` color, returning `#rrggbbaa`. */
64
+ function withAlpha(hex: string, alpha: number): string {
65
+ const base = hex.slice(0, 7);
66
+ return `${base}${byte(Math.round(clamp01(alpha) * 255))}`;
67
+ }
68
+
69
+ /** Strip `#` and any alpha — Monaco token rules want a bare 6-char hex. */
70
+ function bare(hex: string): string {
71
+ return hex.replace("#", "").slice(0, 6).padEnd(6, "0");
72
+ }
73
+
74
+ // --- Contrast helpers: keep syntax tokens legible on the editor background. ---
75
+ const channel = (hex: string, i: number) => parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) || 0;
76
+ const toLinear = (c: number) => {
77
+ const s = c / 255;
78
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
79
+ };
80
+ function luminance(hex: string): number {
81
+ return (
82
+ 0.2126 * toLinear(channel(hex, 0)) +
83
+ 0.7152 * toLinear(channel(hex, 1)) +
84
+ 0.0722 * toLinear(channel(hex, 2))
85
+ );
86
+ }
87
+ function contrast(a: string, b: string): number {
88
+ const la = luminance(a);
89
+ const lb = luminance(b);
90
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
91
+ }
92
+ function mixHex(hex: string, target: string, t: number): string {
93
+ const lerp = (i: number) =>
94
+ Math.round(channel(hex, i) + (channel(target, i) - channel(hex, i)) * t);
95
+ return `#${byte(lerp(0))}${byte(lerp(1))}${byte(lerp(2))}`;
96
+ }
97
+ /**
98
+ * Darken/lighten `hex` toward black (on light backgrounds) or white (on dark)
99
+ * until it clears `minRatio` against `bg`. Keeps on-palette syntax colors
100
+ * legible across themes — notably saturated tokens on a white editor — without
101
+ * hardcoding per-theme values.
102
+ */
103
+ function ensureReadable(hex: string, bg: string, minRatio: number): string {
104
+ const base = hex.slice(0, 7);
105
+ if (contrast(base, bg) >= minRatio) return base;
106
+ const target = luminance(bg) > 0.5 ? "#000000" : "#ffffff";
107
+ let out = base;
108
+ for (let t = 0.1; t <= 1.0001; t += 0.1) {
109
+ out = mixHex(base, target, t);
110
+ if (contrast(out, bg) >= minRatio) break;
111
+ }
112
+ return out;
113
+ }
114
+
115
+ /**
116
+ * Map the theme active on `rootEl` to Monaco's nearest built-in base.
117
+ *
118
+ * Resolved from the element (the theme's own `color-scheme`), not from a
119
+ * registry lookup on the NAME — so a consumer-authored dark theme gets
120
+ * `vs-dark` without registering anything here (ADR 0029). Getting this wrong is
121
+ * visible: `vs` under a dark theme leaves Monaco's own chrome (the sticky-scroll
122
+ * shadow, the find widget, unstyled decorations) light on a dark editor.
123
+ */
124
+ function builtinBase(rootEl?: HTMLElement | null): Monaco.editor.BuiltinTheme {
125
+ return resolveThemeIsDark(rootEl) ? "vs-dark" : "vs";
126
+ }
127
+
128
+ /**
129
+ * Build a Monaco theme from the resolved CSS variables on `rootEl`
130
+ * (defaults to `<html>`, where `data-theme` lives).
131
+ */
132
+ export function buildBrandThemeData(
133
+ rootEl?: HTMLElement | null,
134
+ ): Monaco.editor.IStandaloneThemeData {
135
+ const el = rootEl ?? (typeof document !== "undefined" ? document.documentElement : null);
136
+ const read = (name: string, fallback?: string) =>
137
+ resolveCssColor(el ? getComputedStyle(el).getPropertyValue(name) : "", fallback);
138
+
139
+ const background = read("--background", "#ffffff");
140
+ const foreground = read("--foreground", "#000000");
141
+ const muted = read("--muted", background);
142
+ const mutedFg = read("--muted-foreground", foreground);
143
+ const border = read("--border", muted);
144
+ const primary = read("--primary", foreground);
145
+ const ring = read("--ring", primary);
146
+ const popover = read("--popover", background);
147
+ const popoverFg = read("--popover-foreground", foreground);
148
+ const input = read("--input", border);
149
+ const chart1 = read("--chart-1", primary);
150
+ const chart2 = read("--chart-2", primary);
151
+ const chart3 = read("--chart-3", primary);
152
+ const chart4 = read("--chart-4", primary);
153
+ const success = read("--success", chart2);
154
+ const destructive = read("--destructive", "#ff0000");
155
+ // Calc result-inlay color (#220): the computed answer shown after each ```calc
156
+ // line. Themed here (not hardcoded) so it re-applies on theme change with the
157
+ // rest of the editor; AA-clamped against the editor background like syntax tokens.
158
+ const calcResult = ensureReadable(read("--calc-result", primary), background, 4.5);
159
+
160
+ const colors: Monaco.editor.IColors = {
161
+ "editor.background": background,
162
+ "editor.foreground": foreground,
163
+ "editorGutter.background": background,
164
+ "editorLineNumber.foreground": withAlpha(mutedFg, 0.6),
165
+ "editorLineNumber.activeForeground": foreground,
166
+ "editorCursor.foreground": primary,
167
+ "editor.selectionBackground": withAlpha(primary, 0.28),
168
+ "editor.inactiveSelectionBackground": withAlpha(primary, 0.14),
169
+ "editor.selectionHighlightBackground": withAlpha(primary, 0.14),
170
+ "editor.lineHighlightBackground": withAlpha(foreground, 0.05),
171
+ "editor.lineHighlightBorder": "#00000000",
172
+ "editorIndentGuide.background1": withAlpha(border, 0.6),
173
+ "editorIndentGuide.activeBackground1": mutedFg,
174
+ "editorWhitespace.foreground": withAlpha(mutedFg, 0.35),
175
+ "editorBracketMatch.background": withAlpha(primary, 0.2),
176
+ "editorBracketMatch.border": withAlpha(primary, 0.45),
177
+ // Widgets — this is what makes Monaco's "built-in components" match brand-ui.
178
+ // A shadow lets the popover detach from the editor even on light themes where
179
+ // the popover surface and editor background are nearly identical.
180
+ "widget.shadow": "#0000002e",
181
+ "editorWidget.background": popover,
182
+ "editorWidget.foreground": popoverFg,
183
+ "editorWidget.border": border,
184
+ "editorHoverWidget.background": popover,
185
+ "editorHoverWidget.foreground": popoverFg,
186
+ "editorHoverWidget.border": border,
187
+ "editorSuggestWidget.background": popover,
188
+ "editorSuggestWidget.foreground": popoverFg,
189
+ "editorSuggestWidget.border": border,
190
+ "editorSuggestWidget.selectedBackground": withAlpha(primary, 0.18),
191
+ "editorSuggestWidget.selectedForeground": popoverFg,
192
+ "editorSuggestWidget.highlightForeground": primary,
193
+ "input.background": input,
194
+ "input.foreground": foreground,
195
+ "input.border": border,
196
+ focusBorder: ring,
197
+ "dropdown.background": popover,
198
+ "dropdown.foreground": popoverFg,
199
+ "dropdown.border": border,
200
+ "list.hoverBackground": withAlpha(mutedFg, 0.12),
201
+ "list.focusBackground": withAlpha(primary, 0.16),
202
+ // Context menu (Monaco's built-in, used when contextMenu="monaco").
203
+ "menu.background": popover,
204
+ "menu.foreground": popoverFg,
205
+ "menu.border": border,
206
+ "menu.selectionBackground": withAlpha(primary, 0.18),
207
+ "menu.selectionForeground": popoverFg,
208
+ "menu.separatorBackground": withAlpha(border, 0.8),
209
+ "scrollbarSlider.background": withAlpha(mutedFg, 0.2),
210
+ "scrollbarSlider.hoverBackground": withAlpha(mutedFg, 0.35),
211
+ "scrollbarSlider.activeBackground": withAlpha(mutedFg, 0.5),
212
+ // Minimap (off by default; themed for when it's enabled via `options`).
213
+ "minimap.background": background,
214
+ "minimapSlider.background": withAlpha(mutedFg, 0.18),
215
+ "minimapSlider.hoverBackground": withAlpha(mutedFg, 0.3),
216
+ "minimapSlider.activeBackground": withAlpha(mutedFg, 0.45),
217
+ "editorError.foreground": destructive,
218
+ "editorWarning.foreground": read("--warning", chart4),
219
+ // Inlay hints (#220 calc result inlays) — calm, legible, themed from the calc
220
+ // result token; transparent plate so it reads as an annotation, not a chip.
221
+ "editorInlayHint.foreground": calcResult,
222
+ "editorInlayHint.background": "#00000000",
223
+ "editorInlayHint.typeForeground": calcResult,
224
+ "editorInlayHint.parameterForeground": calcResult,
225
+ // Diff editor — brand the add/remove bands from success/destructive tokens
226
+ // (instead of Monaco's default green/red) at low alpha so syntax reads on top.
227
+ "diffEditor.insertedTextBackground": withAlpha(success, 0.16),
228
+ "diffEditor.removedTextBackground": withAlpha(destructive, 0.16),
229
+ "diffEditor.insertedLineBackground": withAlpha(success, 0.08),
230
+ "diffEditor.removedLineBackground": withAlpha(destructive, 0.08),
231
+ "diffEditorGutter.insertedLineBackground": withAlpha(success, 0.12),
232
+ "diffEditorGutter.removedLineBackground": withAlpha(destructive, 0.12),
233
+ "diffEditorOverview.insertedForeground": withAlpha(success, 0.6),
234
+ "diffEditorOverview.removedForeground": withAlpha(destructive, 0.6),
235
+ "diffEditor.border": border,
236
+ };
237
+
238
+ // Syntax tokens: enforce AA (4.5:1) against the editor background; comments
239
+ // get a softer 3.2:1 so they stay intentionally muted but legible. Keyword/
240
+ // operator/tag stay on the brand primary (identity), readability-clamped too.
241
+ const ink = (hex: string, ratio = 4.5) => bare(ensureReadable(hex, background, ratio));
242
+ const rules: Monaco.editor.ITokenThemeRule[] = [
243
+ { token: "", foreground: bare(foreground), background: bare(background) },
244
+ { token: "comment", foreground: ink(mutedFg, 3.2), fontStyle: "italic" },
245
+ { token: "keyword", foreground: ink(primary) },
246
+ { token: "operator", foreground: ink(primary) },
247
+ { token: "string", foreground: ink(chart2) },
248
+ { token: "number", foreground: ink(chart4) },
249
+ { token: "regexp", foreground: ink(chart4) },
250
+ { token: "constant", foreground: ink(chart4) },
251
+ { token: "type", foreground: ink(chart1) },
252
+ { token: "type.identifier", foreground: ink(chart1) },
253
+ { token: "function", foreground: ink(chart3) },
254
+ { token: "identifier", foreground: bare(foreground) },
255
+ { token: "variable", foreground: bare(foreground) },
256
+ { token: "variable.predefined", foreground: ink(chart3) },
257
+ { token: "delimiter", foreground: ink(mutedFg, 3.2) },
258
+ { token: "tag", foreground: ink(primary) },
259
+ { token: "attribute.name", foreground: ink(chart3) },
260
+ { token: "attribute.value", foreground: ink(chart2) },
261
+ { token: "key", foreground: ink(chart1) }, // JSON keys
262
+ { token: "string.key", foreground: ink(chart1) },
263
+ { token: "string.value", foreground: ink(chart2) },
264
+ { token: "invalid", foreground: ink(destructive) },
265
+ { token: "namespace", foreground: ink(success) },
266
+ ];
267
+
268
+ // `base` is a placeholder; `applyBrandTheme` overrides it per theme.
269
+ return { base: "vs", inherit: true, colors, rules };
270
+ }
271
+
272
+ /** The Monaco theme id used for a given brand theme. */
273
+ export function brandThemeId(theme: ThemeName): string {
274
+ return `brand-${theme}`;
275
+ }
276
+
277
+ /**
278
+ * Define + activate the Monaco theme for `theme`, reading live token values.
279
+ * `setTheme` is global to all editors, so calling this from any mounted editor
280
+ * is idempotent and keeps every editor in sync.
281
+ */
282
+ export function applyBrandTheme(
283
+ monaco: typeof Monaco,
284
+ theme: ThemeName,
285
+ rootEl?: HTMLElement | null,
286
+ ): string {
287
+ const id = brandThemeId(theme);
288
+ const data = buildBrandThemeData(rootEl);
289
+ data.base = builtinBase(rootEl);
290
+ monaco.editor.defineTheme(id, data);
291
+ monaco.editor.setTheme(id);
292
+ return id;
293
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { DEFAULT_THEME, type ThemeName } from "@elabs-ai/components-tokens";
5
+
6
+ export interface DataThemeState {
7
+ /** The active brand theme parsed from `data-theme`. */
8
+ theme: ThemeName;
9
+ /**
10
+ * Increments on every observed `data-theme` mutation (and the initial read),
11
+ * even when the parsed theme name is unchanged. Consumers depend on this to
12
+ * re-run side effects once the attribute settles — e.g. the editor re-applies
13
+ * its Monaco theme after a late attribute write that matches the default,
14
+ * which a name-only dependency would miss.
15
+ */
16
+ revision: number;
17
+ }
18
+
19
+ /**
20
+ * Reads the active brand theme straight from the `data-theme` attribute and
21
+ * keeps it in sync via a `MutationObserver`. Deliberately decoupled from
22
+ * `ThemeProvider`/`useTheme` so the editor themes correctly whether or not a
23
+ * provider is present (Storybook's theme decorator just sets `data-theme`).
24
+ *
25
+ * @param target Element to watch. Defaults to `<html>` (where `ThemeProvider`
26
+ * writes). Pass a scoped element for nested theming; the document root is also
27
+ * observed as a fallback.
28
+ */
29
+ export function useDataTheme(target?: HTMLElement | null): DataThemeState {
30
+ const [state, setState] = useState<DataThemeState>({ theme: DEFAULT_THEME, revision: 0 });
31
+
32
+ useEffect(() => {
33
+ if (typeof document === "undefined") return;
34
+ const el = target ?? document.documentElement;
35
+
36
+ const read = () => {
37
+ const next =
38
+ el.getAttribute("data-theme") ?? document.documentElement.getAttribute("data-theme");
39
+ setState((prev) => ({
40
+ // Any non-empty attribute value is a theme (ADR 0029 — names are open).
41
+ // Only "no attribute anywhere" keeps the previous value, which is what
42
+ // makes a late ThemeProvider write a no-op rather than a flash.
43
+ theme: next || prev.theme,
44
+ revision: prev.revision + 1,
45
+ }));
46
+ };
47
+
48
+ read();
49
+ const observer = new MutationObserver(read);
50
+ observer.observe(el, { attributes: true, attributeFilter: ["data-theme"] });
51
+ if (el !== document.documentElement) {
52
+ observer.observe(document.documentElement, {
53
+ attributes: true,
54
+ attributeFilter: ["data-theme"],
55
+ });
56
+ }
57
+ return () => observer.disconnect();
58
+ }, [target]);
59
+
60
+ return state;
61
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `@elabs-ai/components-editor/markdown/frontmatter` — the lightweight frontmatter subpath.
3
+ *
4
+ * Just the YAML frontmatter parse/serialize (depends on `js-yaml` only). Split out
5
+ * from the `./markdown` barrel so DATA-only consumers — a metadata form, a document
6
+ * store — can read/write frontmatter WITHOUT pulling the editor engines (Milkdown,
7
+ * Monaco, Streamdown) into their bundle or their unit tests. Same dependency-control
8
+ * principle as the `.` vs `./markdown` split.
9
+ */
10
+ export {
11
+ parseFrontmatter,
12
+ serializeFrontmatter,
13
+ type ParsedDocument,
14
+ } from "../lib/markdown/frontmatter";