@elabs-ai/components-editor 4.0.0 → 4.1.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 (54) hide show
  1. package/README.md +6 -7
  2. package/dist/{chunk-LBC5VJBD.js → chunk-C62O7IOQ.js} +110 -21
  3. package/dist/chunk-C62O7IOQ.js.map +1 -0
  4. package/dist/index.css +4 -2
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +57 -20
  8. package/dist/index.js.map +1 -1
  9. package/dist/markdown/index.css +4 -2
  10. package/dist/markdown/index.css.map +1 -1
  11. package/dist/markdown/index.d.ts +2 -2
  12. package/dist/markdown/index.js +9 -9
  13. package/dist/markdown/index.js.map +1 -1
  14. package/dist/{markdown-editor-DfBZibAn.d.ts → markdown-editor-CBp_4eDv.d.ts} +1 -1
  15. package/package.json +6 -6
  16. package/src/ai-objects/entity.tsx +1 -1
  17. package/src/ai-objects/knowledge-card.tsx +1 -1
  18. package/src/calc-block/calc-editor.css +1 -1
  19. package/src/code-editor/code-editor.stories.tsx +76 -1
  20. package/src/code-editor/code-editor.tsx +1 -1
  21. package/src/code-workspace/code-workspace.test.tsx +83 -0
  22. package/src/code-workspace/code-workspace.tsx +73 -20
  23. package/src/diff-editor/diff-editor.stories.tsx +9 -1
  24. package/src/lib/monaco-deep-imports.d.ts +59 -0
  25. package/src/lib/monaco-theme-bridge.test.ts +307 -0
  26. package/src/lib/monaco-theme-bridge.ts +154 -10
  27. package/src/markdown-academic/citations.tsx +2 -2
  28. package/src/markdown-academic/footnotes.tsx +2 -2
  29. package/src/markdown-academic/toc.tsx +1 -1
  30. package/src/markdown-editor/directive-views.tsx +2 -5
  31. package/src/markdown-editor/markdown-editor.css +26 -3
  32. package/src/markdown-editor/markdown-editor.focus.test.ts +49 -0
  33. package/src/markdown-editor/markdown-editor.stories.tsx +128 -7
  34. package/src/markdown-editor/markdown-editor.tsx +8 -5
  35. package/src/markdown-editor/milkdown-react/use-get-editor.timer-leak.test.ts +80 -0
  36. package/src/markdown-editor/milkdown-react/use-get-editor.ts +37 -1
  37. package/src/markdown-editor/paste-embed.ts +2 -1
  38. package/src/markdown-editor/slash/slash-menu.stories.tsx +8 -3
  39. package/src/markdown-editor/slash/slash-menu.test.tsx +27 -0
  40. package/src/markdown-editor/slash/slash-menu.tsx +13 -1
  41. package/src/markdown-editor/table-view.tsx +3 -2
  42. package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +57 -19
  43. package/src/markdown-outline/document-outline.stories.tsx +1 -1
  44. package/src/markdown-outline/document-outline.tsx +1 -1
  45. package/src/markdown-preview/markdown-preview-transclusion.test.tsx +1 -1
  46. package/src/markdown-preview/markdown-preview.stories.tsx +24 -2
  47. package/src/markdown-preview/markdown-preview.test.tsx +20 -3
  48. package/src/markdown-toolbar/markdown-toolbar.stories.tsx +3 -0
  49. package/src/markdown-workspace/markdown-workspace.test.tsx +31 -1
  50. package/src/mermaid-diagram/mermaid-viewer.tsx +1 -1
  51. package/src/prose/prose.stories.tsx +3 -0
  52. package/src/prose/prose.test.ts +54 -0
  53. package/src/prose/prose.tsx +7 -0
  54. package/dist/chunk-LBC5VJBD.js.map +0 -1
package/README.md CHANGED
@@ -6,15 +6,14 @@
6
6
  > Token-themed Monaco editor: CodeEditor, DiffEditor, CodeWorkspace.
7
7
 
8
8
  Part of **brand-ui**, a source-owned, token-driven React component system.
9
- These packages are **private** and are not published to any registry they are
10
- consumed from this workspace. See `docs/CONSUMING.md`.
9
+ Published to the **public npm registry** under the `@elabs-ai` scopeit
10
+ installs like any other npm dependency, with no registry configuration and
11
+ no token required. See `docs/CONSUMING.md`.
11
12
 
12
13
  ## Install
13
14
 
14
- Inside this monorepo the packages resolve as workspace dependencies:
15
-
16
- ```json
17
- "@elabs-ai/components-editor": "workspace:*"
15
+ ```bash
16
+ pnpm add @elabs-ai/components-tokens @elabs-ai/components-editor
18
17
  ```
19
18
 
20
19
  ## Set up styling (do not skip)
@@ -75,7 +74,7 @@ prompt for migrating an existing project: `docs/CONSUMING.md`.
75
74
 
76
75
  ## License
77
76
 
78
- UNLICENSED — private.
77
+ MIT
79
78
 
80
79
  <!-- brand-ui:gen:readme:end -->
81
80
 
@@ -130,6 +130,7 @@ function withAlpha(hex, alpha) {
130
130
  const base = hex.slice(0, 7);
131
131
  return `${base}${byte(Math.round(clamp01(alpha) * 255))}`;
132
132
  }
133
+ var LINE_HIGHLIGHT_ALPHA = 0.05;
133
134
  function bare(hex) {
134
135
  return hex.replace("#", "").slice(0, 6).padEnd(6, "0");
135
136
  }
@@ -146,6 +147,13 @@ function contrast(a, b) {
146
147
  const lb = luminance(b);
147
148
  return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
148
149
  }
150
+ function flattenOver(overlayHexWithAlpha, groundHex) {
151
+ const overlayBase = overlayHexWithAlpha.slice(0, 7);
152
+ const alphaHex = overlayHexWithAlpha.length >= 9 ? overlayHexWithAlpha.slice(7, 9) : "ff";
153
+ const alpha = clamp01((parseInt(alphaHex, 16) || 0) / 255);
154
+ const blend = (i) => Math.round(channel(overlayBase, i) * alpha + channel(groundHex, i) * (1 - alpha));
155
+ return `#${byte(blend(0))}${byte(blend(1))}${byte(blend(2))}`;
156
+ }
149
157
  function mixHex(hex, target, t) {
150
158
  const lerp = (i) => Math.round(channel(hex, i) + (channel(target, i) - channel(hex, i)) * t);
151
159
  return `#${byte(lerp(0))}${byte(lerp(1))}${byte(lerp(2))}`;
@@ -174,6 +182,8 @@ function buildBrandThemeData(rootEl) {
174
182
  const border = read("--border", muted);
175
183
  const primary = read("--primary", foreground);
176
184
  const ring = read("--ring", primary);
185
+ const ringContour = read("--ring-contour", ring);
186
+ const focusBorder = contrast(ringContour, background) > contrast(ring, background) ? ringContour : ring;
177
187
  const popover = read("--popover", background);
178
188
  const popoverFg = read("--popover-foreground", foreground);
179
189
  const input = read("--input", border);
@@ -183,7 +193,9 @@ function buildBrandThemeData(rootEl) {
183
193
  const chart4 = read("--chart-4", primary);
184
194
  const success = read("--success", chart2);
185
195
  const destructive = read("--destructive", "#ff0000");
186
- const calcResult = ensureReadable(read("--calc-result", primary), background, 4.5);
196
+ const lineHighlight = withAlpha(foreground, LINE_HIGHLIGHT_ALPHA);
197
+ const tokenGround = flattenOver(lineHighlight, background);
198
+ const calcResult = ensureReadable(read("--calc-result", primary), tokenGround, 4.5);
187
199
  const colors = {
188
200
  "editor.background": background,
189
201
  "editor.foreground": foreground,
@@ -194,7 +206,7 @@ function buildBrandThemeData(rootEl) {
194
206
  "editor.selectionBackground": withAlpha(primary, 0.28),
195
207
  "editor.inactiveSelectionBackground": withAlpha(primary, 0.14),
196
208
  "editor.selectionHighlightBackground": withAlpha(primary, 0.14),
197
- "editor.lineHighlightBackground": withAlpha(foreground, 0.05),
209
+ "editor.lineHighlightBackground": lineHighlight,
198
210
  "editor.lineHighlightBorder": "#00000000",
199
211
  "editorIndentGuide.background1": withAlpha(border, 0.6),
200
212
  "editorIndentGuide.activeBackground1": mutedFg,
@@ -220,7 +232,7 @@ function buildBrandThemeData(rootEl) {
220
232
  "input.background": input,
221
233
  "input.foreground": foreground,
222
234
  "input.border": border,
223
- focusBorder: ring,
235
+ focusBorder,
224
236
  "dropdown.background": popover,
225
237
  "dropdown.foreground": popoverFg,
226
238
  "dropdown.border": border,
@@ -261,7 +273,8 @@ function buildBrandThemeData(rootEl) {
261
273
  "diffEditorOverview.removedForeground": withAlpha(destructive, 0.6),
262
274
  "diffEditor.border": border
263
275
  };
264
- const ink = (hex, ratio = 4.5) => bare(ensureReadable(hex, background, ratio));
276
+ const AA_MARGIN = 0.15;
277
+ const ink = (hex, ratio = 4.5) => bare(ensureReadable(hex, tokenGround, ratio + AA_MARGIN));
265
278
  const rules = [
266
279
  { token: "", foreground: bare(foreground), background: bare(background) },
267
280
  { token: "comment", foreground: ink(mutedFg, 3.2), fontStyle: "italic" },
@@ -286,7 +299,61 @@ function buildBrandThemeData(rootEl) {
286
299
  { token: "string.key", foreground: ink(chart1) },
287
300
  { token: "string.value", foreground: ink(chart2) },
288
301
  { token: "invalid", foreground: ink(destructive) },
289
- { token: "namespace", foreground: ink(success) }
302
+ { token: "namespace", foreground: ink(success) },
303
+ // Monaco's built-in `vs`/`vs-dark` bases (inherited at `inherit: true`,
304
+ // below) ship LANGUAGE-SUFFIXED rules — `string.key.json`,
305
+ // `string.value.json`, `keyword.json`, `string.yaml`, `delimiter.html`, …
306
+ // — and Monaco's token-theme trie resolves the DEEPEST matching scope
307
+ // (`ThemeTrieElement.match`), with rules sorted lexicographically before
308
+ // insertion. A shorter brand scope (e.g. `string.key`, above) can
309
+ // therefore NEVER override a longer base scope: every scope a base theme
310
+ // specialises must be re-declared here, or that language renders in
311
+ // stock VS colours (#90). Keep this list in sync with the base themes —
312
+ // `IGNORED_BASE_SCOPES` + the drift guard in `monaco-theme-bridge.test.ts`
313
+ // fail CI if a future `monaco-editor` upgrade adds a new one.
314
+ { token: "string.key.json", foreground: ink(chart1) },
315
+ // pairs with `key`
316
+ { token: "string.value.json", foreground: ink(chart2) },
317
+ // pairs with `string`
318
+ { token: "keyword.json", foreground: ink(primary) },
319
+ // pairs with `keyword`
320
+ // Closes the class, not just JSON — the same base-specialisation gap
321
+ // reaches YAML/HTML/SQL/XML/CSS/SCSS (#90 evidence #7).
322
+ { token: "string.html", foreground: ink(chart2) },
323
+ { token: "string.sql", foreground: ink(chart2) },
324
+ { token: "string.yaml", foreground: ink(chart2) },
325
+ { token: "delimiter.html", foreground: ink(mutedFg, 3.2) },
326
+ { token: "delimiter.xml", foreground: ink(mutedFg, 3.2) },
327
+ { token: "attribute.value.html", foreground: ink(chart2) },
328
+ { token: "attribute.value.xml", foreground: ink(chart2) },
329
+ { token: "attribute.value.number", foreground: ink(chart4) },
330
+ { token: "attribute.value.unit", foreground: ink(chart4) },
331
+ { token: "attribute.value.number.css", foreground: ink(chart4) },
332
+ { token: "attribute.value.unit.css", foreground: ink(chart4) },
333
+ { token: "attribute.value.hex.css", foreground: ink(chart4) },
334
+ { token: "number.hex", foreground: ink(chart4) },
335
+ { token: "keyword.flow", foreground: ink(primary) },
336
+ { token: "keyword.flow.scss", foreground: ink(primary) },
337
+ { token: "operator.scss", foreground: ink(primary) },
338
+ { token: "operator.sql", foreground: ink(primary) },
339
+ { token: "operator.swift", foreground: ink(primary) },
340
+ { token: "predefined.sql", foreground: ink(chart3) },
341
+ { token: "metatag", foreground: ink(chart3) },
342
+ { token: "metatag.html", foreground: ink(chart3) },
343
+ { token: "metatag.xml", foreground: ink(chart3) },
344
+ { token: "metatag.content.html", foreground: ink(chart3) },
345
+ { token: "meta.scss", foreground: ink(chart1) },
346
+ { token: "meta.tag", foreground: ink(chart1) },
347
+ // `CodeEditorProps.language` is a plain, unrestricted `string` passed
348
+ // straight to `monaco.editor.setModelLanguage` (`code-editor.tsx`) — NOT
349
+ // limited to `EDITOR_LANGUAGES` — so a consumer really can reach these
350
+ // pug/handlebars scopes (PR #119 review thread 2). See
351
+ // `IGNORED_BASE_SCOPES` below for the one scope that stays un-overridden.
352
+ { token: "tag.id.pug", foreground: ink(primary) },
353
+ // pairs with `tag`
354
+ { token: "tag.class.pug", foreground: ink(primary) },
355
+ { token: "variable.parameter", foreground: bare(foreground) }
356
+ // pairs with `variable`
290
357
  ];
291
358
  return { base: "vs", inherit: true, colors, rules };
292
359
  }
@@ -912,7 +979,7 @@ var DocumentOutline = forwardRef2(
912
979
  className: cn2(
913
980
  "-ms-px block w-full truncate border-s-2 py-1 pe-2 text-start text-caption",
914
981
  "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",
982
+ "focus-ring-inset",
916
983
  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
984
  actions && "pe-8"
918
985
  ),
@@ -1725,7 +1792,23 @@ var SlashMenu = forwardRef4(function SlashMenu2({
1725
1792
  className
1726
1793
  ),
1727
1794
  ...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: [
1795
+ children: commands.length === 0 ? (
1796
+ // A `role="listbox"` may only own `option`/`group` children (WCAG 1.3.1,
1797
+ // #157): a bare `<div>` here is an unannounced, dangling message — an
1798
+ // AT user who types a query matching nothing gets silence. Shaping it
1799
+ // as a disabled, unselectable option keeps the listbox contract AND
1800
+ // makes the message reachable by an accessible-name query.
1801
+ /* @__PURE__ */ jsx5(
1802
+ "div",
1803
+ {
1804
+ role: "option",
1805
+ "aria-disabled": "true",
1806
+ "aria-selected": "false",
1807
+ className: "cursor-default select-none px-2 py-6 text-center text-caption text-muted-foreground",
1808
+ children: emptyLabel
1809
+ }
1810
+ )
1811
+ ) : groups.map(({ group, commands: groupCommands }) => /* @__PURE__ */ jsxs4("div", { role: "group", "aria-label": group, className: "overflow-hidden p-1", children: [
1729
1812
  /* @__PURE__ */ jsx5("div", { className: "px-2 py-1.5 text-meta font-medium text-muted-foreground", children: group }),
1730
1813
  groupCommands.map((command) => {
1731
1814
  const selected = command.id === activeId;
@@ -2414,6 +2497,7 @@ import { useMemo as useMemo2, useRef as useRef5, useState as useState3 } from "r
2414
2497
  // src/markdown-editor/milkdown-react/use-get-editor.ts
2415
2498
  import { createContext as createContext2, useContext, useEffect as useEffect3, useRef as useRef4 } from "react";
2416
2499
  var editorInfoContext = createContext2({});
2500
+ var pendingDestroys = /* @__PURE__ */ new Set();
2417
2501
  function useGetEditor() {
2418
2502
  const {
2419
2503
  dom,
@@ -2436,7 +2520,11 @@ function useGetEditor() {
2436
2520
  setLoading(false);
2437
2521
  }).catch(console.error);
2438
2522
  return () => {
2439
- editor2.destroy().catch(console.error);
2523
+ const destroyPromise = editor2.destroy().catch(console.error);
2524
+ pendingDestroys.add(destroyPromise);
2525
+ void destroyPromise.finally(() => {
2526
+ pendingDestroys.delete(destroyPromise);
2527
+ });
2440
2528
  };
2441
2529
  }, [dom, editorRef, getEditor, setLoading]);
2442
2530
  return domRef;
@@ -2549,10 +2637,7 @@ function InlineEdit({ value, onCommit, ariaLabel, placeholder, className }) {
2549
2637
  spellCheck: false,
2550
2638
  onBlur: commit,
2551
2639
  onKeyDown,
2552
- className: cn6(
2553
- "brand-inline-edit rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
2554
- className
2555
- )
2640
+ className: cn6("brand-inline-edit rounded-sm focus-ring", className)
2556
2641
  }
2557
2642
  );
2558
2643
  }
@@ -2833,7 +2918,7 @@ function IterationDirectiveView() {
2833
2918
  "data-directive-chrome": "",
2834
2919
  "aria-label": "Iteration actions",
2835
2920
  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",
2921
+ className: "ms-auto inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground focus-ring",
2837
2922
  children: /* @__PURE__ */ jsx10(MoreHorizontal, { className: "size-4", "aria-hidden": "true" })
2838
2923
  }
2839
2924
  ) }),
@@ -3045,7 +3130,7 @@ function makeErrorChip(message) {
3045
3130
  chip.setAttribute("role", "alert");
3046
3131
  chip.setAttribute("aria-live", "assertive");
3047
3132
  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";
3133
+ chip.className = "inline-flex items-center gap-1 rounded px-2 py-0.5 text-caption bg-destructive/10 text-destructive-text border border-destructive/30 select-none";
3049
3134
  const label = document.createElement("span");
3050
3135
  label.textContent = `\u26A0 Upload failed`;
3051
3136
  chip.appendChild(label);
@@ -3259,9 +3344,10 @@ function ToolbarButton({
3259
3344
  className: cn7(
3260
3345
  "inline-flex h-7 items-center gap-1 rounded px-2 text-caption font-medium",
3261
3346
  "border border-border-strong",
3262
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
3347
+ "focus-ring",
3263
3348
  "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"
3349
+ // #124: the button's LABEL text, not a mark ink rung.
3350
+ variant === "destructive" ? "bg-background text-destructive-text hover:bg-destructive/10" : "bg-background text-foreground hover:bg-surface-muted"
3265
3351
  ),
3266
3352
  children
3267
3353
  }
@@ -3591,10 +3677,13 @@ var MarkdownEditor = forwardRef6(
3591
3677
  {
3592
3678
  "data-testid": "markdown-editor",
3593
3679
  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",
3680
+ // No focus-ring utility here (deliberately, #309): the editable
3681
+ // `.ProseMirror` element owns the compound focus indicator itself
3682
+ // (`markdown-editor.css`'s `:focus-visible` rule), so it renders
3683
+ // even when a consumer `className` overrides this wrapper's
3684
+ // classes (`MarkdownWorkspace` does exactly that via
3685
+ // `className="border-0"`) and never doubles up with a wrapper ring.
3686
+ "milkdown-host overflow-auto rounded-md border border-border bg-background text-foreground",
3598
3687
  className
3599
3688
  ),
3600
3689
  style: { ...markdownScaleVars(), ...style },
@@ -3745,4 +3834,4 @@ export {
3745
3834
  CopyButton,
3746
3835
  monacoContentAccess
3747
3836
  };
3748
- //# sourceMappingURL=chunk-LBC5VJBD.js.map
3837
+ //# sourceMappingURL=chunk-C62O7IOQ.js.map