@elabs-ai/components-editor 4.0.0 → 4.2.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 (77) hide show
  1. package/README.md +6 -7
  2. package/dist/{chunk-LBC5VJBD.js → chunk-FO5S3YZM.js} +349 -177
  3. package/dist/chunk-FO5S3YZM.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 +5 -4
  7. package/dist/index.js +112 -57
  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 +268 -191
  13. package/dist/markdown/index.js.map +1 -1
  14. package/dist/{markdown-editor-DfBZibAn.d.ts → markdown-editor-Dn-0L_MM.d.ts} +9 -2
  15. package/dist/monaco.d.ts +2 -0
  16. package/dist/monaco.js +9 -0
  17. package/dist/monaco.js.map +1 -0
  18. package/package.json +10 -6
  19. package/src/ai-objects/decision-card.tsx +15 -9
  20. package/src/ai-objects/entity.tsx +1 -1
  21. package/src/ai-objects/knowledge-card.tsx +18 -9
  22. package/src/barrel-monaco-lazy.test.ts +50 -0
  23. package/src/calc-block/calc-block.tsx +10 -4
  24. package/src/calc-block/calc-editor.css +1 -1
  25. package/src/code-editor/code-editor.stories.tsx +76 -1
  26. package/src/code-editor/code-editor.test.tsx +141 -14
  27. package/src/code-editor/code-editor.tsx +167 -36
  28. package/src/code-workspace/code-workspace.test.tsx +113 -11
  29. package/src/code-workspace/code-workspace.tsx +73 -20
  30. package/src/copy-button/copy-button.tsx +6 -3
  31. package/src/diff-editor/diff-editor.stories.tsx +9 -1
  32. package/src/diff-editor/diff-editor.test.tsx +9 -3
  33. package/src/diff-editor/diff-editor.tsx +47 -23
  34. package/src/editor-toolbar/editor-toolbar.tsx +8 -2
  35. package/src/index.ts +4 -3
  36. package/src/lib/monaco-deep-imports.d.ts +59 -0
  37. package/src/lib/monaco-theme-bridge.test.ts +307 -0
  38. package/src/lib/monaco-theme-bridge.ts +154 -10
  39. package/src/markdown-academic/citations.tsx +14 -7
  40. package/src/markdown-academic/footnotes.tsx +2 -2
  41. package/src/markdown-academic/math.tsx +7 -4
  42. package/src/markdown-academic/toc.tsx +1 -1
  43. package/src/markdown-editor/completions/completions-menu.tsx +5 -2
  44. package/src/markdown-editor/directive-views.tsx +35 -24
  45. package/src/markdown-editor/markdown-editor.css +26 -3
  46. package/src/markdown-editor/markdown-editor.focus.test.ts +49 -0
  47. package/src/markdown-editor/markdown-editor.stories.tsx +128 -7
  48. package/src/markdown-editor/markdown-editor.tsx +8 -5
  49. package/src/markdown-editor/milkdown-react/use-get-editor.timer-leak.test.ts +80 -0
  50. package/src/markdown-editor/milkdown-react/use-get-editor.ts +37 -1
  51. package/src/markdown-editor/paste-embed.ts +2 -1
  52. package/src/markdown-editor/slash/slash-menu.stories.tsx +8 -3
  53. package/src/markdown-editor/slash/slash-menu.test.tsx +27 -0
  54. package/src/markdown-editor/slash/slash-menu.tsx +18 -3
  55. package/src/markdown-editor/table-view.tsx +31 -17
  56. package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +57 -19
  57. package/src/markdown-iteration/iteration-builder-dialog.tsx +39 -18
  58. package/src/markdown-iteration/template-dialog.tsx +25 -7
  59. package/src/markdown-outline/document-outline.stories.tsx +1 -1
  60. package/src/markdown-outline/document-outline.tsx +7 -3
  61. package/src/markdown-preview/markdown-preview-academic.test.tsx +3 -3
  62. package/src/markdown-preview/markdown-preview-transclusion.test.tsx +1 -1
  63. package/src/markdown-preview/markdown-preview.stories.tsx +24 -2
  64. package/src/markdown-preview/markdown-preview.test.tsx +20 -3
  65. package/src/markdown-toolbar/markdown-toolbar.stories.tsx +3 -0
  66. package/src/markdown-toolbar/markdown-toolbar.tsx +42 -24
  67. package/src/markdown-workspace/markdown-workspace.test.tsx +53 -4
  68. package/src/markdown-workspace/markdown-workspace.tsx +6 -3
  69. package/src/mermaid-diagram/mermaid-diagram-fixes.test.tsx +130 -0
  70. package/src/mermaid-diagram/mermaid-diagram.test.tsx +2 -1
  71. package/src/mermaid-diagram/mermaid-diagram.tsx +89 -40
  72. package/src/mermaid-diagram/mermaid-viewer.tsx +22 -21
  73. package/src/monaco.ts +21 -0
  74. package/src/prose/prose.stories.tsx +3 -0
  75. package/src/prose/prose.test.ts +54 -0
  76. package/src/prose/prose.tsx +7 -0
  77. package/dist/chunk-LBC5VJBD.js.map +0 -1
@@ -11,12 +11,12 @@ import {
11
11
  } from "@elabs-ai/components-ui";
12
12
  import "react";
13
13
  import { jsx, jsxs } from "react/jsx-runtime";
14
- function EditorContextMenu({ editor: editor2, readOnly = false, children }) {
14
+ function EditorContextMenu({ editor, readOnly = false, children }) {
15
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;
16
+ if (!editor) return null;
17
+ const model = editor.getModel();
18
+ const sel = editor.getSelection();
19
+ return model && sel ? { editor, model, sel } : null;
20
20
  };
21
21
  const copy = async () => {
22
22
  const ctx2 = selection();
@@ -51,8 +51,8 @@ function EditorContextMenu({ editor: editor2, readOnly = false, children }) {
51
51
  ctx2.editor.focus();
52
52
  };
53
53
  const runAction = (id) => {
54
- editor2?.focus();
55
- void editor2?.getAction(id)?.run();
54
+ editor?.focus();
55
+ void editor?.getAction(id)?.run();
56
56
  };
57
57
  return /* @__PURE__ */ jsxs(ContextMenu, { children: [
58
58
  /* @__PURE__ */ jsx(ContextMenuTrigger, { asChild: true, children }),
@@ -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,19 +299,73 @@ 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
  }
293
360
  function brandThemeId(theme) {
294
361
  return `brand-${theme}`;
295
362
  }
296
- function applyBrandTheme(monaco2, theme, rootEl) {
363
+ function applyBrandTheme(monaco, theme, rootEl) {
297
364
  const id = brandThemeId(theme);
298
365
  const data = buildBrandThemeData(rootEl);
299
366
  data.base = builtinBase(rootEl);
300
- monaco2.editor.defineTheme(id, data);
301
- monaco2.editor.setTheme(id);
367
+ monaco.editor.defineTheme(id, data);
368
+ monaco.editor.setTheme(id);
302
369
  return id;
303
370
  }
304
371
 
@@ -335,7 +402,6 @@ function useDataTheme(target) {
335
402
  }
336
403
 
337
404
  // src/code-editor/code-editor.tsx
338
- import * as monaco from "monaco-editor";
339
405
  import { cn } from "@elabs-ai/components-ui/lib/cn";
340
406
  import {
341
407
  forwardRef,
@@ -345,6 +411,20 @@ import {
345
411
  useState as useState2
346
412
  } from "react";
347
413
  import { jsx as jsx2 } from "react/jsx-runtime";
414
+ function computeMinimalEdit(oldValue, newValue) {
415
+ const maxCommon = Math.min(oldValue.length, newValue.length);
416
+ let start = 0;
417
+ while (start < maxCommon && oldValue.charCodeAt(start) === newValue.charCodeAt(start)) {
418
+ start++;
419
+ }
420
+ let endOld = oldValue.length;
421
+ let endNew = newValue.length;
422
+ while (endOld > start && endNew > start && oldValue.charCodeAt(endOld - 1) === newValue.charCodeAt(endNew - 1)) {
423
+ endOld--;
424
+ endNew--;
425
+ }
426
+ return { start, endOld, text: newValue.slice(start, endNew) };
427
+ }
348
428
  var BASE_OPTIONS = {
349
429
  automaticLayout: true,
350
430
  minimap: { enabled: false },
@@ -376,70 +456,123 @@ var CodeEditor = forwardRef(function CodeEditor2({
376
456
  ...props
377
457
  }, ref) {
378
458
  const containerRef = useRef(null);
379
- const [editor2, setEditor] = useState2(null);
459
+ const [editor, setEditor] = useState2(null);
380
460
  const { theme, revision } = useDataTheme();
461
+ const modelRef = useRef(null);
462
+ const monacoRef = useRef(null);
381
463
  const onChangeRef = useRef(onChange);
382
464
  onChangeRef.current = onChange;
383
465
  const onMountRef = useRef(onMount);
384
466
  onMountRef.current = onMount;
385
- useImperativeHandle(ref, () => editor2, [
386
- editor2
467
+ useImperativeHandle(ref, () => editor, [
468
+ editor
387
469
  ]);
388
470
  useEffect2(() => {
389
471
  const container = containerRef.current;
390
472
  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());
473
+ let cancelled = false;
474
+ let instance = null;
475
+ let model = null;
476
+ let sub = null;
477
+ import("monaco-editor").then((monacoApi) => {
478
+ if (cancelled) return;
479
+ monacoRef.current = monacoApi;
480
+ model = monacoApi.editor.createModel(
481
+ value ?? defaultValue ?? "",
482
+ language,
483
+ path ? monacoApi.Uri.parse(`inmemory://brand/${path}`) : void 0
484
+ );
485
+ modelRef.current = model;
486
+ instance = monacoApi.editor.create(container, {
487
+ ...BASE_OPTIONS,
488
+ readOnly,
489
+ // Disable Monaco's own menu unless explicitly opted into; "brand" renders
490
+ // brand-ui's ContextMenu around the editor instead.
491
+ contextmenu: contextMenu === "monaco",
492
+ // Monaco's accessible name comes from this construction option (it writes it
493
+ // onto its inner screen-reader <textarea>), not from a wrapper-div attribute.
494
+ ...ariaLabel !== void 0 ? { ariaLabel } : null,
495
+ model,
496
+ ...options
497
+ });
498
+ sub = instance.onDidChangeModelContent(() => {
499
+ onChangeRef.current?.(instance.getValue());
500
+ });
501
+ const textarea = instance.getDomNode()?.querySelector("textarea");
502
+ if (textarea) {
503
+ if (ariaLabel !== void 0) textarea.setAttribute("aria-label", ariaLabel);
504
+ if (ariaInvalid !== void 0) textarea.setAttribute("aria-invalid", String(ariaInvalid));
505
+ if (ariaDescribedBy !== void 0)
506
+ textarea.setAttribute("aria-describedby", ariaDescribedBy);
507
+ }
508
+ setEditor(instance);
509
+ onMountRef.current?.(instance, monacoApi);
410
510
  });
411
- setEditor(instance);
412
- onMountRef.current?.(instance, monaco);
413
511
  return () => {
414
- sub.dispose();
415
- instance.dispose();
416
- model.dispose();
512
+ cancelled = true;
513
+ sub?.dispose();
514
+ instance?.dispose();
515
+ model?.dispose();
516
+ modelRef.current = null;
517
+ monacoRef.current = null;
417
518
  setEditor(null);
418
519
  };
419
520
  }, []);
420
521
  useEffect2(() => {
421
- if (!editor2 || value === void 0) return;
422
- if (value !== editor2.getValue()) editor2.setValue(value);
423
- }, [editor2, value]);
522
+ const monacoApi = monacoRef.current;
523
+ if (!editor || !monacoApi) return;
524
+ const current = modelRef.current;
525
+ const currentUri = current?.uri?.toString();
526
+ const nextUri = path ? monacoApi.Uri.parse(`inmemory://brand/${path}`).toString() : void 0;
527
+ if (currentUri === nextUri) return;
528
+ const nextModel = monacoApi.editor.createModel(
529
+ current?.getValue() ?? value ?? defaultValue ?? "",
530
+ language,
531
+ path ? monacoApi.Uri.parse(`inmemory://brand/${path}`) : void 0
532
+ );
533
+ editor.setModel(nextModel);
534
+ modelRef.current = nextModel;
535
+ current?.dispose();
536
+ }, [editor, path]);
537
+ useEffect2(() => {
538
+ const monacoApi = monacoRef.current;
539
+ if (!editor || !monacoApi || value === void 0) return;
540
+ const model = editor.getModel();
541
+ if (!model) return;
542
+ const current = model.getValue();
543
+ if (value === current) return;
544
+ const { start, endOld, text } = computeMinimalEdit(current, value);
545
+ const range = monacoApi.Range.fromPositions(
546
+ model.getPositionAt(start),
547
+ model.getPositionAt(endOld)
548
+ );
549
+ editor.executeEdits("controlled-value-sync", [{ range, text }]);
550
+ }, [editor, value]);
424
551
  useEffect2(() => {
425
- const model = editor2?.getModel();
426
- if (model) monaco.editor.setModelLanguage(model, language);
427
- }, [editor2, language]);
552
+ const monacoApi = monacoRef.current;
553
+ const model = editor?.getModel();
554
+ if (model && monacoApi) monacoApi.editor.setModelLanguage(model, language);
555
+ }, [editor, language]);
428
556
  useEffect2(() => {
429
- editor2?.updateOptions({ readOnly, contextmenu: contextMenu === "monaco" });
430
- }, [editor2, readOnly, contextMenu]);
557
+ editor?.updateOptions({ readOnly, contextmenu: contextMenu === "monaco" });
558
+ }, [editor, readOnly, contextMenu]);
431
559
  useEffect2(() => {
432
- if (!editor2) return;
560
+ if (!editor || !options) return;
561
+ editor.updateOptions(options);
562
+ }, [editor, options]);
563
+ useEffect2(() => {
564
+ const monacoApi = monacoRef.current;
565
+ if (!editor || !monacoApi) return;
433
566
  try {
434
- applyBrandTheme(monaco, theme);
567
+ applyBrandTheme(monacoApi, theme);
435
568
  } catch (err) {
436
569
  console.error("[@elabs-ai/components-editor] failed to apply brand theme", err);
437
570
  }
438
- }, [editor2, theme, revision]);
571
+ }, [editor, theme, revision]);
439
572
  useEffect2(() => {
440
- if (!editor2) return;
441
- if (ariaLabel !== void 0) editor2.updateOptions({ ariaLabel });
442
- const textarea = editor2.getDomNode()?.querySelector("textarea");
573
+ if (!editor) return;
574
+ if (ariaLabel !== void 0) editor.updateOptions({ ariaLabel });
575
+ const textarea = editor.getDomNode()?.querySelector("textarea");
443
576
  if (!textarea) return;
444
577
  if (ariaLabel === void 0) textarea.removeAttribute("aria-label");
445
578
  else textarea.setAttribute("aria-label", ariaLabel);
@@ -447,12 +580,12 @@ var CodeEditor = forwardRef(function CodeEditor2({
447
580
  else textarea.setAttribute("aria-invalid", String(ariaInvalid));
448
581
  if (ariaDescribedBy === void 0) textarea.removeAttribute("aria-describedby");
449
582
  else textarea.setAttribute("aria-describedby", ariaDescribedBy);
450
- }, [editor2, ariaLabel, ariaInvalid, ariaDescribedBy]);
583
+ }, [editor, ariaLabel, ariaInvalid, ariaDescribedBy]);
451
584
  useEffect2(() => {
452
- if (!editor2 || !actions || actions.length === 0) return;
453
- const disposables = actions.map((a) => editor2.addAction(a));
585
+ if (!editor || !actions || actions.length === 0) return;
586
+ const disposables = actions.map((a) => editor.addAction(a));
454
587
  return () => disposables.forEach((d) => d.dispose());
455
- }, [editor2, actions]);
588
+ }, [editor, actions]);
456
589
  const resolvedStyle = {
457
590
  height: typeof height === "number" ? `${height}px` : height,
458
591
  ...style
@@ -468,7 +601,7 @@ var CodeEditor = forwardRef(function CodeEditor2({
468
601
  }
469
602
  );
470
603
  if (contextMenu === "brand") {
471
- return /* @__PURE__ */ jsx2(EditorContextMenu, { editor: editor2, readOnly, children: editorEl });
604
+ return /* @__PURE__ */ jsx2(EditorContextMenu, { editor, readOnly, children: editorEl });
472
605
  }
473
606
  return editorEl;
474
607
  });
@@ -883,6 +1016,7 @@ function parseMarkdownOutline(markdown) {
883
1016
  }
884
1017
 
885
1018
  // src/markdown-outline/document-outline.tsx
1019
+ import { useLocale } from "@elabs-ai/components-ui";
886
1020
  import { cn as cn2 } from "@elabs-ai/components-ui/lib/cn";
887
1021
  import { forwardRef as forwardRef2, useMemo } from "react";
888
1022
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -891,12 +1025,13 @@ function useMarkdownOutline(markdown) {
891
1025
  }
892
1026
  var DocumentOutline = forwardRef2(
893
1027
  function DocumentOutline2({ items, activeId, onSelect, itemActions, className, ...props }, ref) {
1028
+ const { t } = useLocale();
894
1029
  const minLevel = items.reduce((min, it) => Math.min(min, it.level), 6);
895
1030
  return /* @__PURE__ */ jsxs2(
896
1031
  "nav",
897
1032
  {
898
1033
  ref,
899
- "aria-label": "Document outline",
1034
+ "aria-label": t("editor.documentOutline.label"),
900
1035
  className: cn2("text-body", className),
901
1036
  ...props,
902
1037
  children: [
@@ -912,7 +1047,7 @@ var DocumentOutline = forwardRef2(
912
1047
  className: cn2(
913
1048
  "-ms-px block w-full truncate border-s-2 py-1 pe-2 text-start text-caption",
914
1049
  "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",
1050
+ "focus-ring-inset",
916
1051
  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
1052
  actions && "pe-8"
918
1053
  ),
@@ -923,7 +1058,7 @@ var DocumentOutline = forwardRef2(
923
1058
  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
1059
  ] }, item.id);
925
1060
  }) }),
926
- items.length === 0 ? /* @__PURE__ */ jsx3("p", { className: "px-2 py-1 text-caption text-muted-foreground", children: "No headings yet." }) : null
1061
+ items.length === 0 ? /* @__PURE__ */ jsx3("p", { className: "px-2 py-1 text-caption text-muted-foreground", children: t("editor.documentOutline.empty") }) : null
927
1062
  ]
928
1063
  }
929
1064
  );
@@ -1698,6 +1833,7 @@ function groupSlashCommands(commands) {
1698
1833
  }
1699
1834
 
1700
1835
  // src/markdown-editor/slash/slash-menu.tsx
1836
+ import { useLocale as useLocale2 } from "@elabs-ai/components-ui";
1701
1837
  import { cn as cn4 } from "@elabs-ai/components-ui/lib/cn";
1702
1838
  import { forwardRef as forwardRef4 } from "react";
1703
1839
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -1709,23 +1845,41 @@ var SlashMenu = forwardRef4(function SlashMenu2({
1709
1845
  activeId,
1710
1846
  onSelect,
1711
1847
  idPrefix = "brand-slash",
1712
- emptyLabel = "No matching blocks",
1848
+ emptyLabel: emptyLabelProp,
1713
1849
  className,
1714
1850
  ...props
1715
1851
  }, ref) {
1852
+ const { t } = useLocale2();
1853
+ const emptyLabel = emptyLabelProp ?? t("editor.slashMenu.noMatchingBlocks");
1716
1854
  const groups = groupSlashCommands(commands);
1717
1855
  return /* @__PURE__ */ jsx5(
1718
1856
  "div",
1719
1857
  {
1720
1858
  ref,
1721
1859
  role: "listbox",
1722
- "aria-label": "Insert block",
1860
+ "aria-label": t("editor.slashMenu.insertBlock"),
1723
1861
  className: cn4(
1724
1862
  "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
1863
  className
1726
1864
  ),
1727
1865
  ...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: [
1866
+ children: commands.length === 0 ? (
1867
+ // A `role="listbox"` may only own `option`/`group` children (WCAG 1.3.1,
1868
+ // #157): a bare `<div>` here is an unannounced, dangling message — an
1869
+ // AT user who types a query matching nothing gets silence. Shaping it
1870
+ // as a disabled, unselectable option keeps the listbox contract AND
1871
+ // makes the message reachable by an accessible-name query.
1872
+ /* @__PURE__ */ jsx5(
1873
+ "div",
1874
+ {
1875
+ role: "option",
1876
+ "aria-disabled": "true",
1877
+ "aria-selected": "false",
1878
+ className: "cursor-default select-none px-2 py-6 text-center text-caption text-muted-foreground",
1879
+ children: emptyLabel
1880
+ }
1881
+ )
1882
+ ) : groups.map(({ group, commands: groupCommands }) => /* @__PURE__ */ jsxs4("div", { role: "group", "aria-label": group, className: "overflow-hidden p-1", children: [
1729
1883
  /* @__PURE__ */ jsx5("div", { className: "px-2 py-1.5 text-meta font-medium text-muted-foreground", children: group }),
1730
1884
  groupCommands.map((command) => {
1731
1885
  const selected = command.id === activeId;
@@ -2252,6 +2406,7 @@ import { useWidgetViewContext as useWidgetViewContext2 } from "@prosemirror-adap
2252
2406
  import { useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
2253
2407
 
2254
2408
  // src/markdown-editor/completions/completions-menu.tsx
2409
+ import { useLocale as useLocale3 } from "@elabs-ai/components-ui";
2255
2410
  import { cn as cn5 } from "@elabs-ai/components-ui/lib/cn";
2256
2411
  import { forwardRef as forwardRef5 } from "react";
2257
2412
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -2264,16 +2419,18 @@ var CompletionMenu = forwardRef5(
2264
2419
  activeIndex,
2265
2420
  onSelect,
2266
2421
  idPrefix = "brand-completions",
2267
- emptyLabel = "No suggestions",
2422
+ emptyLabel: emptyLabelProp,
2268
2423
  className,
2269
2424
  ...props
2270
2425
  }, ref) {
2426
+ const { t } = useLocale3();
2427
+ const emptyLabel = emptyLabelProp ?? t("editor.completions.noSuggestions");
2271
2428
  return /* @__PURE__ */ jsx7(
2272
2429
  "div",
2273
2430
  {
2274
2431
  ref,
2275
2432
  role: "listbox",
2276
- "aria-label": "Suggestions",
2433
+ "aria-label": t("editor.completions.suggestions"),
2277
2434
  className: cn5(
2278
2435
  "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
2436
  className
@@ -2391,7 +2548,8 @@ import {
2391
2548
  DropdownMenuSub,
2392
2549
  DropdownMenuSubContent,
2393
2550
  DropdownMenuSubTrigger,
2394
- DropdownMenuTrigger
2551
+ DropdownMenuTrigger,
2552
+ useLocale as useLocale4
2395
2553
  } from "@elabs-ai/components-ui";
2396
2554
  import { cn as cn6 } from "@elabs-ai/components-ui/lib/cn";
2397
2555
  import { editorViewCtx as editorViewCtx2, parserCtx as parserCtx2, serializerCtx } from "@milkdown/kit/core";
@@ -2414,6 +2572,7 @@ import { useMemo as useMemo2, useRef as useRef5, useState as useState3 } from "r
2414
2572
  // src/markdown-editor/milkdown-react/use-get-editor.ts
2415
2573
  import { createContext as createContext2, useContext, useEffect as useEffect3, useRef as useRef4 } from "react";
2416
2574
  var editorInfoContext = createContext2({});
2575
+ var pendingDestroys = /* @__PURE__ */ new Set();
2417
2576
  function useGetEditor() {
2418
2577
  const {
2419
2578
  dom,
@@ -2427,16 +2586,20 @@ function useGetEditor() {
2427
2586
  if (!getEditor) return;
2428
2587
  if (!div) return;
2429
2588
  dom.current = div;
2430
- const editor2 = getEditor(div);
2431
- if (!editor2) return;
2589
+ const editor = getEditor(div);
2590
+ if (!editor) return;
2432
2591
  setLoading(true);
2433
- editor2.create().then((editor3) => {
2434
- editorRef.current = editor3;
2592
+ editor.create().then((editor2) => {
2593
+ editorRef.current = editor2;
2435
2594
  }).finally(() => {
2436
2595
  setLoading(false);
2437
2596
  }).catch(console.error);
2438
2597
  return () => {
2439
- editor2.destroy().catch(console.error);
2598
+ const destroyPromise = editor.destroy().catch(console.error);
2599
+ pendingDestroys.add(destroyPromise);
2600
+ void destroyPromise.finally(() => {
2601
+ pendingDestroys.delete(destroyPromise);
2602
+ });
2440
2603
  };
2441
2604
  }, [dom, editorRef, getEditor, setLoading]);
2442
2605
  return domRef;
@@ -2451,10 +2614,10 @@ var Milkdown = () => {
2451
2614
  var MilkdownProvider = ({ children }) => {
2452
2615
  const dom = useRef5(void 0);
2453
2616
  const [editorFactory, setEditorFactory] = useState3(void 0);
2454
- const editor2 = useRef5(void 0);
2617
+ const editor = useRef5(void 0);
2455
2618
  const [loading, setLoading] = useState3(true);
2456
2619
  const editorInfoCtx = useMemo2(
2457
- () => ({ loading, dom, editor: editor2, setLoading, editorFactory, setEditorFactory }),
2620
+ () => ({ loading, dom, editor, setLoading, editorFactory, setEditorFactory }),
2458
2621
  [loading, editorFactory]
2459
2622
  );
2460
2623
  return /* @__PURE__ */ jsx9(editorInfoContext.Provider, { value: editorInfoCtx, children });
@@ -2549,14 +2712,12 @@ function InlineEdit({ value, onCommit, ariaLabel, placeholder, className }) {
2549
2712
  spellCheck: false,
2550
2713
  onBlur: commit,
2551
2714
  onKeyDown,
2552
- className: cn6(
2553
- "brand-inline-edit rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
2554
- className
2555
- )
2715
+ className: cn6("brand-inline-edit rounded-sm focus-ring", className)
2556
2716
  }
2557
2717
  );
2558
2718
  }
2559
2719
  function ContainerDirectiveView() {
2720
+ const { t } = useLocale4();
2560
2721
  const { contentRef } = useNodeViewContext();
2561
2722
  const { name, attributes, update } = useDirectiveAttrs();
2562
2723
  const body = /* @__PURE__ */ jsx10("div", { className: "brand-directive__body", ref: contentRef });
@@ -2565,8 +2726,8 @@ function ContainerDirectiveView() {
2565
2726
  /* @__PURE__ */ jsx10(CardHeader, { className: "pb-3", children: /* @__PURE__ */ jsx10(CardTitle, { children: /* @__PURE__ */ jsx10(
2566
2727
  InlineEdit,
2567
2728
  {
2568
- ariaLabel: "Card title",
2569
- placeholder: "Card title",
2729
+ ariaLabel: t("editor.directiveViews.cardTitle"),
2730
+ placeholder: t("editor.directiveViews.cardTitle"),
2570
2731
  value: attributes.title ?? "",
2571
2732
  onCommit: (v) => update("title", v)
2572
2733
  }
@@ -2586,7 +2747,7 @@ function ContainerDirectiveView() {
2586
2747
  /* @__PURE__ */ jsx10("div", { className: "mb-1 font-medium leading-none tracking-tight", children: /* @__PURE__ */ jsx10(
2587
2748
  InlineEdit,
2588
2749
  {
2589
- ariaLabel: "Callout title",
2750
+ ariaLabel: t("editor.directiveViews.calloutTitle"),
2590
2751
  placeholder: capitalize(attributes.type ?? "note"),
2591
2752
  value: attributes.title ?? "",
2592
2753
  onCommit: (v) => update("title", v)
@@ -2611,10 +2772,7 @@ function ContainerDirectiveView() {
2611
2772
  className: "brand-directive brand-directive--unknown",
2612
2773
  "data-brand-directive": name,
2613
2774
  children: [
2614
- /* @__PURE__ */ jsxs6("div", { className: "mb-1 font-medium leading-none tracking-tight", children: [
2615
- "Unknown block: ",
2616
- name
2617
- ] }),
2775
+ /* @__PURE__ */ jsx10("div", { className: "mb-1 font-medium leading-none tracking-tight", children: t("editor.directiveViews.unknownBlock", { name }) }),
2618
2776
  /* @__PURE__ */ jsx10(AlertDescription, { children: body })
2619
2777
  ]
2620
2778
  }
@@ -2622,9 +2780,9 @@ function ContainerDirectiveView() {
2622
2780
  }
2623
2781
  function readBodyMarkdown(getInstance, node) {
2624
2782
  try {
2625
- const editor2 = getInstance();
2626
- if (!editor2) return node.textContent;
2627
- return editor2.action((ctx2) => {
2783
+ const editor = getInstance();
2784
+ if (!editor) return node.textContent;
2785
+ return editor.action((ctx2) => {
2628
2786
  const serialize = ctx2.get(
2629
2787
  serializerCtx
2630
2788
  );
@@ -2637,10 +2795,10 @@ function readBodyMarkdown(getInstance, node) {
2637
2795
  }
2638
2796
  function writeBodyMarkdown(getInstance, getPos, template) {
2639
2797
  try {
2640
- const editor2 = getInstance();
2798
+ const editor = getInstance();
2641
2799
  const pos = getPos();
2642
- if (!editor2 || pos == null) return;
2643
- editor2.action((ctx2) => {
2800
+ if (!editor || pos == null) return;
2801
+ editor.action((ctx2) => {
2644
2802
  const parse = ctx2.get(
2645
2803
  parserCtx2
2646
2804
  );
@@ -2668,10 +2826,10 @@ function mergeAttrsOmittingEmpty(attrs, next) {
2668
2826
  function replaceNodeWithMarkdown(getInstance, getPos, markdown) {
2669
2827
  if (!markdown.trim()) return;
2670
2828
  try {
2671
- const editor2 = getInstance();
2829
+ const editor = getInstance();
2672
2830
  const pos = getPos();
2673
- if (!editor2 || pos == null) return;
2674
- editor2.action((ctx2) => {
2831
+ if (!editor || pos == null) return;
2832
+ editor.action((ctx2) => {
2675
2833
  const parse = ctx2.get(
2676
2834
  parserCtx2
2677
2835
  );
@@ -2721,6 +2879,7 @@ function IterationMenuItems({
2721
2879
  }) });
2722
2880
  }
2723
2881
  function IterationDirectiveView() {
2882
+ const { t } = useLocale4();
2724
2883
  const { contentRef, node, getPos, setAttrs } = useNodeViewContext();
2725
2884
  const { name, attributes } = useDirectiveAttrs();
2726
2885
  const [, getInstance] = useInstance();
@@ -2775,7 +2934,7 @@ function IterationDirectiveView() {
2775
2934
  as: attributes.as || "item",
2776
2935
  attributes
2777
2936
  }).cells.length > 0;
2778
- const disabledHint = "\u2014 needs embedded values";
2937
+ const disabledHint = t("editor.directiveViews.needsEmbeddedValues");
2779
2938
  const convertToStatic = () => {
2780
2939
  const template = readBodyMarkdown(getInstance, node);
2781
2940
  const value = builderValueFromParts(kind, attributes, template);
@@ -2785,14 +2944,14 @@ function IterationDirectiveView() {
2785
2944
  {
2786
2945
  type: "item",
2787
2946
  id: "edit",
2788
- label: "Edit iteration\u2026",
2947
+ label: t("editor.directiveViews.editIteration"),
2789
2948
  icon: /* @__PURE__ */ jsx10(Pencil, { className: "size-4", "aria-hidden": "true" }),
2790
2949
  onSelect: requestEdit
2791
2950
  },
2792
2951
  {
2793
2952
  type: "layout",
2794
2953
  id: "layout",
2795
- label: "Change layout",
2954
+ label: t("editor.directiveViews.changeLayout"),
2796
2955
  icon: /* @__PURE__ */ jsx10(LayoutGrid, { className: "size-4", "aria-hidden": "true" }),
2797
2956
  value: attributes.layout || ITERATION_LAYOUTS[kind][0],
2798
2957
  options: ITERATION_LAYOUTS[kind],
@@ -2802,7 +2961,7 @@ function IterationDirectiveView() {
2802
2961
  {
2803
2962
  type: "item",
2804
2963
  id: "transpose",
2805
- label: hasEmbeddedData ? "Transpose" : `Transpose ${disabledHint}`,
2964
+ label: hasEmbeddedData ? t("editor.directiveViews.transpose") : `${t("editor.directiveViews.transpose")} ${disabledHint}`,
2806
2965
  icon: /* @__PURE__ */ jsx10(ArrowLeftRight, { className: "size-4", "aria-hidden": "true" }),
2807
2966
  onSelect: transpose,
2808
2967
  disabled: !hasEmbeddedData
@@ -2811,7 +2970,7 @@ function IterationDirectiveView() {
2811
2970
  {
2812
2971
  type: "item",
2813
2972
  id: "convert-to-static",
2814
- label: hasEmbeddedData ? "Convert to static" : `Convert to static ${disabledHint}`,
2973
+ label: hasEmbeddedData ? t("editor.directiveViews.convertToStatic") : `${t("editor.directiveViews.convertToStatic")} ${disabledHint}`,
2815
2974
  icon: /* @__PURE__ */ jsx10(FileText, { className: "size-4", "aria-hidden": "true" }),
2816
2975
  onSelect: convertToStatic,
2817
2976
  disabled: !hasEmbeddedData
@@ -2819,21 +2978,18 @@ function IterationDirectiveView() {
2819
2978
  ];
2820
2979
  const header = /* @__PURE__ */ jsxs6("div", { className: "mb-1.5 flex items-center gap-1.5 text-meta font-medium text-info-text", children: [
2821
2980
  /* @__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" }),
2981
+ /* @__PURE__ */ jsx10("span", { children: isPivot ? t("editor.directiveViews.pivot") : t("editor.directiveViews.iterate") }),
2982
+ !isPivot && attributes.as ? /* @__PURE__ */ jsx10("span", { className: "font-normal text-muted-foreground", children: t("editor.directiveViews.perItem", { as: attributes.as }) }) : null,
2983
+ /* @__PURE__ */ jsx10("span", { className: "font-normal text-muted-foreground", children: t("editor.directiveViews.templateSuffix") }),
2828
2984
  onEdit ? /* @__PURE__ */ jsxs6(DropdownMenu, { children: [
2829
2985
  /* @__PURE__ */ jsx10(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx10(
2830
2986
  "button",
2831
2987
  {
2832
2988
  type: "button",
2833
2989
  "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",
2990
+ "aria-label": t("editor.directiveViews.iterationActions"),
2991
+ title: t("editor.directiveViews.iterationActionsTitle"),
2992
+ 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
2993
  children: /* @__PURE__ */ jsx10(MoreHorizontal, { className: "size-4", "aria-hidden": "true" })
2838
2994
  }
2839
2995
  ) }),
@@ -2873,6 +3029,7 @@ function IterationDirectiveView() {
2873
3029
  ] });
2874
3030
  }
2875
3031
  function LeafDirectiveView() {
3032
+ const { t } = useLocale4();
2876
3033
  const { name, attributes, update } = useDirectiveAttrs();
2877
3034
  if (name !== "metric") {
2878
3035
  return /* @__PURE__ */ jsxs6(
@@ -2881,7 +3038,8 @@ function LeafDirectiveView() {
2881
3038
  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
3039
  "data-brand-leaf": name,
2883
3040
  children: [
2884
- "Unknown inline block: ",
3041
+ t("editor.directiveViews.unknownInlineBlock"),
3042
+ " ",
2885
3043
  /* @__PURE__ */ jsxs6("code", { children: [
2886
3044
  "::",
2887
3045
  name
@@ -2899,8 +3057,8 @@ function LeafDirectiveView() {
2899
3057
  label: /* @__PURE__ */ jsx10(
2900
3058
  InlineEdit,
2901
3059
  {
2902
- ariaLabel: "Metric label",
2903
- placeholder: "Label",
3060
+ ariaLabel: t("editor.directiveViews.metricLabel"),
3061
+ placeholder: t("editor.directiveViews.metricLabelPlaceholder"),
2904
3062
  value: attributes.label ?? "",
2905
3063
  onCommit: (v) => update("label", v)
2906
3064
  }
@@ -2908,8 +3066,8 @@ function LeafDirectiveView() {
2908
3066
  value: /* @__PURE__ */ jsx10(
2909
3067
  InlineEdit,
2910
3068
  {
2911
- ariaLabel: "Metric value",
2912
- placeholder: "0",
3069
+ ariaLabel: t("editor.directiveViews.metricValue"),
3070
+ placeholder: t("editor.directiveViews.metricValuePlaceholder"),
2913
3071
  value: attributes.value ?? "",
2914
3072
  onCommit: (v) => update("value", v),
2915
3073
  className: "min-w-[1ch]"
@@ -3045,7 +3203,7 @@ function makeErrorChip(message) {
3045
3203
  chip.setAttribute("role", "alert");
3046
3204
  chip.setAttribute("aria-live", "assertive");
3047
3205
  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";
3206
+ 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
3207
  const label = document.createElement("span");
3050
3208
  label.textContent = `\u26A0 Upload failed`;
3051
3209
  chip.appendChild(label);
@@ -3228,6 +3386,7 @@ import {
3228
3386
  import { Plugin as Plugin7, PluginKey as PluginKey6 } from "@milkdown/kit/prose/state";
3229
3387
  import { $prose as $prose7 } from "@milkdown/kit/utils";
3230
3388
  import { isInTable } from "@milkdown/kit/prose/tables";
3389
+ import { useLocale as useLocale5 } from "@elabs-ai/components-ui";
3231
3390
  import { cn as cn7 } from "@elabs-ai/components-ui/lib/cn";
3232
3391
  import { usePluginViewContext } from "@prosemirror-adapter/react";
3233
3392
  import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -3259,9 +3418,10 @@ function ToolbarButton({
3259
3418
  className: cn7(
3260
3419
  "inline-flex h-7 items-center gap-1 rounded px-2 text-caption font-medium",
3261
3420
  "border border-border-strong",
3262
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
3421
+ "focus-ring",
3263
3422
  "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"
3423
+ // #124: the button's LABEL text, not a mark ink rung.
3424
+ variant === "destructive" ? "bg-background text-destructive-text hover:bg-destructive/10" : "bg-background text-foreground hover:bg-surface-muted"
3265
3425
  ),
3266
3426
  children
3267
3427
  }
@@ -3272,25 +3432,32 @@ function ToolbarDivider() {
3272
3432
  }
3273
3433
  function TableControlsView() {
3274
3434
  const { view } = usePluginViewContext();
3435
+ const { t } = useLocale5();
3275
3436
  const inTable = isInTable(view.state);
3276
3437
  const run = (key) => () => dispatchCommand(view, key);
3277
3438
  if (!inTable) return null;
3439
+ const addRowAbove = t("editor.tableView.addRowAbove");
3440
+ const addRowBelow = t("editor.tableView.addRowBelow");
3441
+ const deleteRow = t("editor.tableView.deleteRow");
3442
+ const addColLeft = t("editor.tableView.addColumnLeft");
3443
+ const addColRight = t("editor.tableView.addColumnRight");
3444
+ const deleteCol = t("editor.tableView.deleteColumn");
3278
3445
  return /* @__PURE__ */ jsxs7(
3279
3446
  "div",
3280
3447
  {
3281
3448
  role: "toolbar",
3282
- "aria-label": "Table controls",
3449
+ "aria-label": t("editor.tableView.tableControls"),
3283
3450
  className: cn7(
3284
3451
  "flex flex-wrap items-center gap-1 px-2 py-1.5",
3285
3452
  "border-t border-border-strong bg-surface-muted"
3286
3453
  ),
3287
3454
  children: [
3288
- /* @__PURE__ */ jsx11("span", { className: "mr-1 select-none text-caption text-muted-foreground", children: "Row" }),
3455
+ /* @__PURE__ */ jsx11("span", { className: "mr-1 select-none text-caption text-muted-foreground", children: t("editor.tableView.row") }),
3289
3456
  /* @__PURE__ */ jsx11(
3290
3457
  ToolbarButton,
3291
3458
  {
3292
- ariaLabel: "Add row above",
3293
- title: "Add row above",
3459
+ ariaLabel: addRowAbove,
3460
+ title: addRowAbove,
3294
3461
  onClick: run(addRowBeforeCommand.key),
3295
3462
  children: "\u2191+"
3296
3463
  }
@@ -3298,8 +3465,8 @@ function TableControlsView() {
3298
3465
  /* @__PURE__ */ jsx11(
3299
3466
  ToolbarButton,
3300
3467
  {
3301
- ariaLabel: "Add row below",
3302
- title: "Add row below",
3468
+ ariaLabel: addRowBelow,
3469
+ title: addRowBelow,
3303
3470
  onClick: run(addRowAfterCommand.key),
3304
3471
  children: "\u2193+"
3305
3472
  }
@@ -3307,20 +3474,20 @@ function TableControlsView() {
3307
3474
  /* @__PURE__ */ jsx11(
3308
3475
  ToolbarButton,
3309
3476
  {
3310
- ariaLabel: "Delete row",
3311
- title: "Delete row",
3477
+ ariaLabel: deleteRow,
3478
+ title: deleteRow,
3312
3479
  variant: "destructive",
3313
3480
  onClick: run(deleteSelectedCellsCommand.key),
3314
3481
  children: "\xD7row"
3315
3482
  }
3316
3483
  ),
3317
3484
  /* @__PURE__ */ jsx11(ToolbarDivider, {}),
3318
- /* @__PURE__ */ jsx11("span", { className: "mr-1 select-none text-caption text-muted-foreground", children: "Col" }),
3485
+ /* @__PURE__ */ jsx11("span", { className: "mr-1 select-none text-caption text-muted-foreground", children: t("editor.tableView.col") }),
3319
3486
  /* @__PURE__ */ jsx11(
3320
3487
  ToolbarButton,
3321
3488
  {
3322
- ariaLabel: "Add column left",
3323
- title: "Add column left",
3489
+ ariaLabel: addColLeft,
3490
+ title: addColLeft,
3324
3491
  onClick: run(addColBeforeCommand.key),
3325
3492
  children: "\u2190+"
3326
3493
  }
@@ -3328,8 +3495,8 @@ function TableControlsView() {
3328
3495
  /* @__PURE__ */ jsx11(
3329
3496
  ToolbarButton,
3330
3497
  {
3331
- ariaLabel: "Add column right",
3332
- title: "Add column right",
3498
+ ariaLabel: addColRight,
3499
+ title: addColRight,
3333
3500
  onClick: run(addColAfterCommand.key),
3334
3501
  children: "\u2192+"
3335
3502
  }
@@ -3337,8 +3504,8 @@ function TableControlsView() {
3337
3504
  /* @__PURE__ */ jsx11(
3338
3505
  ToolbarButton,
3339
3506
  {
3340
- ariaLabel: "Delete column",
3341
- title: "Delete column",
3507
+ ariaLabel: deleteCol,
3508
+ title: deleteCol,
3342
3509
  variant: "destructive",
3343
3510
  onClick: run(deleteSelectedCellsCommand.key),
3344
3511
  children: "\xD7col"
@@ -3374,8 +3541,8 @@ function tableViewPlugins(pluginViewFactory) {
3374
3541
 
3375
3542
  // src/markdown-editor/markdown-editor.tsx
3376
3543
  import { jsx as jsx12 } from "react/jsx-runtime";
3377
- function scrollHeadingBySlug(editor2, slug) {
3378
- editor2.action((ctx2) => {
3544
+ function scrollHeadingBySlug(editor, slug) {
3545
+ editor.action((ctx2) => {
3379
3546
  const view = ctx2.get(editorViewCtx3);
3380
3547
  const { doc } = view.state;
3381
3548
  const used = /* @__PURE__ */ new Map();
@@ -3435,7 +3602,7 @@ var MarkdownEditorView = forwardRef6(function MarkdownEditorView2({
3435
3602
  const embedPlugin = pasteEmbedPlugin(
3436
3603
  onEmbedAssetRef.current ? (file) => onEmbedAssetRef.current(file) : void 0
3437
3604
  );
3438
- let editor2 = Editor.make().config((ctx2) => {
3605
+ let editor = Editor.make().config((ctx2) => {
3439
3606
  ctx2.set(rootCtx, root);
3440
3607
  ctx2.set(defaultValueCtx, lastMarkdown.current);
3441
3608
  ctx2.update(editorViewOptionsCtx, (prev) => ({
@@ -3456,7 +3623,7 @@ var MarkdownEditorView = forwardRef6(function MarkdownEditorView2({
3456
3623
  )
3457
3624
  );
3458
3625
  if (slashEnabled) {
3459
- editor2 = editor2.use(
3626
+ editor = editor.use(
3460
3627
  brandSlashViewPlugins(widgetViewFactory, {
3461
3628
  ...slashConfigRef.current,
3462
3629
  getIterationEditHandler: () => iterationEditHandlerRef.current
@@ -3464,14 +3631,14 @@ var MarkdownEditorView = forwardRef6(function MarkdownEditorView2({
3464
3631
  );
3465
3632
  }
3466
3633
  if (calcEnabled) {
3467
- editor2 = editor2.use(calcProsePlugins(() => calcRef.current));
3634
+ editor = editor.use(calcProsePlugins(() => calcRef.current));
3468
3635
  }
3469
3636
  if (completionsEnabled) {
3470
- editor2 = editor2.use(
3637
+ editor = editor.use(
3471
3638
  completionsViewPlugins(widgetViewFactory, () => completionsRef.current)
3472
3639
  );
3473
3640
  }
3474
- return editor2;
3641
+ return editor;
3475
3642
  },
3476
3643
  [readOnly, ariaLabel, slashEnabled, calcEnabled, completionsEnabled]
3477
3644
  );
@@ -3479,21 +3646,21 @@ var MarkdownEditorView = forwardRef6(function MarkdownEditorView2({
3479
3646
  useEffect5(() => {
3480
3647
  if (loading || value === void 0) return;
3481
3648
  if (value === lastMarkdown.current) return;
3482
- const editor2 = getInstance();
3483
- if (!editor2) return;
3649
+ const editor = getInstance();
3650
+ if (!editor) return;
3484
3651
  lastMarkdown.current = value;
3485
- editor2.action(replaceAll(value));
3652
+ editor.action(replaceAll(value));
3486
3653
  }, [loading, value, getInstance]);
3487
3654
  useImperativeHandle2(
3488
3655
  ref,
3489
3656
  () => {
3490
3657
  const serializeSlice = (view) => {
3491
- const editor2 = getInstance();
3492
- if (!editor2) return "";
3658
+ const editor = getInstance();
3659
+ if (!editor) return "";
3493
3660
  const { selection } = view.state;
3494
3661
  if (selection.empty) return "";
3495
3662
  try {
3496
- return editor2.action((ctx2) => {
3663
+ return editor.action((ctx2) => {
3497
3664
  const serialize = ctx2.get(serializerCtx2);
3498
3665
  const doc = selection.content().content;
3499
3666
  const wrapper = view.state.doc.type.schema.topNodeType.create(null, doc);
@@ -3505,10 +3672,10 @@ var MarkdownEditorView = forwardRef6(function MarkdownEditorView2({
3505
3672
  };
3506
3673
  serializeSliceRef.current = serializeSlice;
3507
3674
  const parseAndReplace = (view, md) => {
3508
- const editor2 = getInstance();
3509
- if (!editor2) return;
3675
+ const editor = getInstance();
3676
+ if (!editor) return;
3510
3677
  try {
3511
- editor2.action((ctx2) => {
3678
+ editor.action((ctx2) => {
3512
3679
  const parse = ctx2.get(parserCtx3);
3513
3680
  const parsed = parse(md);
3514
3681
  if (!parsed) {
@@ -3526,10 +3693,10 @@ var MarkdownEditorView = forwardRef6(function MarkdownEditorView2({
3526
3693
  }
3527
3694
  };
3528
3695
  const getView = () => {
3529
- const editor2 = getInstance();
3530
- if (!editor2) return null;
3696
+ const editor = getInstance();
3697
+ if (!editor) return null;
3531
3698
  try {
3532
- return editor2.action((ctx2) => ctx2.get(editorViewCtx3));
3699
+ return editor.action((ctx2) => ctx2.get(editorViewCtx3));
3533
3700
  } catch {
3534
3701
  return null;
3535
3702
  }
@@ -3553,15 +3720,15 @@ var MarkdownEditorView = forwardRef6(function MarkdownEditorView2({
3553
3720
  getMarkdown: () => getInstance()?.action(getMarkdown()) ?? lastMarkdown.current,
3554
3721
  serialized: () => getInstance()?.action(getMarkdown()) ?? null,
3555
3722
  scrollToHeading: (slug) => {
3556
- const editor2 = getInstance();
3557
- if (editor2) scrollHeadingBySlug(editor2, slug);
3723
+ const editor = getInstance();
3724
+ if (editor) scrollHeadingBySlug(editor, slug);
3558
3725
  },
3559
3726
  revealLine: (line, _opts) => {
3560
- const editor2 = getInstance();
3561
- if (!editor2) return;
3562
- const md = editor2.action(getMarkdown());
3727
+ const editor = getInstance();
3728
+ if (!editor) return;
3729
+ const md = editor.action(getMarkdown());
3563
3730
  const preceding = parseMarkdownOutline(md).filter((item) => item.line <= line).at(-1);
3564
- if (preceding) scrollHeadingBySlug(editor2, preceding.id);
3731
+ if (preceding) scrollHeadingBySlug(editor, preceding.id);
3565
3732
  }
3566
3733
  };
3567
3734
  },
@@ -3591,10 +3758,13 @@ var MarkdownEditor = forwardRef6(
3591
3758
  {
3592
3759
  "data-testid": "markdown-editor",
3593
3760
  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",
3761
+ // No focus-ring utility here (deliberately, #309): the editable
3762
+ // `.ProseMirror` element owns the compound focus indicator itself
3763
+ // (`markdown-editor.css`'s `:focus-visible` rule), so it renders
3764
+ // even when a consumer `className` overrides this wrapper's
3765
+ // classes (`MarkdownWorkspace` does exactly that via
3766
+ // `className="border-0"`) and never doubles up with a wrapper ring.
3767
+ "milkdown-host overflow-auto rounded-md border border-border bg-background text-foreground",
3598
3768
  className
3599
3769
  ),
3600
3770
  style: { ...markdownScaleVars(), ...style },
@@ -3620,16 +3790,18 @@ var MarkdownEditor = forwardRef6(
3620
3790
  );
3621
3791
 
3622
3792
  // src/copy-button/copy-button.tsx
3623
- import { Button, useCopyToClipboard } from "@elabs-ai/components-ui";
3793
+ import { Button, useCopyToClipboard, useLocale as useLocale6 } from "@elabs-ai/components-ui";
3624
3794
  import { cn as cn9 } from "@elabs-ai/components-ui/lib/cn";
3625
3795
  import { CheckIcon, CopyIcon } from "lucide-react";
3626
3796
  import { useCallback as useCallback3 } from "react";
3627
3797
  import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
3628
3798
  function CopyButton({ value, label = true, className, ...props }) {
3629
3799
  const { copied, copy } = useCopyToClipboard();
3800
+ const { t } = useLocale6();
3630
3801
  const onClick = useCallback3(() => {
3631
3802
  void copy(value);
3632
3803
  }, [copy, value]);
3804
+ const text = copied ? t("editor.copyButton.copied") : t("copy");
3633
3805
  return /* @__PURE__ */ jsxs8(
3634
3806
  Button,
3635
3807
  {
@@ -3639,7 +3811,7 @@ function CopyButton({ value, label = true, className, ...props }) {
3639
3811
  type: "button",
3640
3812
  className: cn9("h-7 gap-1.5", className),
3641
3813
  onClick,
3642
- "aria-label": copied ? "Copied" : "Copy",
3814
+ "aria-label": text,
3643
3815
  children: [
3644
3816
  copied ? /* @__PURE__ */ jsx13(
3645
3817
  CheckIcon,
@@ -3649,37 +3821,37 @@ function CopyButton({ value, label = true, className, ...props }) {
3649
3821
  },
3650
3822
  String(copied)
3651
3823
  ) : /* @__PURE__ */ jsx13(CopyIcon, { className: "size-4", "aria-hidden": "true" }),
3652
- label ? /* @__PURE__ */ jsx13("span", { className: "text-xs", children: copied ? "Copied" : "Copy" }) : null
3824
+ label ? /* @__PURE__ */ jsx13("span", { className: "text-xs", children: text }) : null
3653
3825
  ]
3654
3826
  }
3655
3827
  );
3656
3828
  }
3657
3829
 
3658
3830
  // src/lib/editor-content-access.ts
3659
- function monacoContentAccess(editor2) {
3831
+ function monacoContentAccess(editor) {
3660
3832
  const readSelection = () => {
3661
- const model = editor2.getModel();
3662
- const selection = editor2.getSelection();
3833
+ const model = editor.getModel();
3834
+ const selection = editor.getSelection();
3663
3835
  if (!model || !selection) return { text: "", empty: true };
3664
3836
  const text = model.getValueInRange(selection);
3665
3837
  return { text, empty: selection.isEmpty() };
3666
3838
  };
3667
3839
  const applyAtSelection = (text) => {
3668
- const selection = editor2.getSelection();
3840
+ const selection = editor.getSelection();
3669
3841
  if (!selection) return;
3670
- editor2.executeEdits("editor-content-access", [
3842
+ editor.executeEdits("editor-content-access", [
3671
3843
  { range: selection, text, forceMoveMarkers: true }
3672
3844
  ]);
3673
- editor2.pushUndoStop();
3845
+ editor.pushUndoStop();
3674
3846
  };
3675
3847
  return {
3676
- getText: () => editor2.getValue(),
3848
+ getText: () => editor.getValue(),
3677
3849
  getSelection: readSelection,
3678
3850
  replaceSelection: applyAtSelection,
3679
3851
  insertAtCursor: applyAtSelection,
3680
- focus: () => editor2.focus(),
3852
+ focus: () => editor.focus(),
3681
3853
  onSelectionChange: (listener2) => {
3682
- const sub = editor2.onDidChangeCursorSelection(() => listener2(readSelection()));
3854
+ const sub = editor.onDidChangeCursorSelection(() => listener2(readSelection()));
3683
3855
  return () => sub.dispose();
3684
3856
  }
3685
3857
  };
@@ -3745,4 +3917,4 @@ export {
3745
3917
  CopyButton,
3746
3918
  monacoContentAccess
3747
3919
  };
3748
- //# sourceMappingURL=chunk-LBC5VJBD.js.map
3920
+ //# sourceMappingURL=chunk-FO5S3YZM.js.map