@elabs-ai/components-editor 4.1.0 → 5.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 (46) hide show
  1. package/dist/{chunk-C62O7IOQ.js → chunk-FO5S3YZM.js} +241 -158
  2. package/dist/chunk-FO5S3YZM.js.map +1 -0
  3. package/dist/index.d.ts +5 -4
  4. package/dist/index.js +57 -39
  5. package/dist/index.js.map +1 -1
  6. package/dist/markdown/index.d.ts +2 -2
  7. package/dist/markdown/index.js +265 -188
  8. package/dist/markdown/index.js.map +1 -1
  9. package/dist/{markdown-editor-CBp_4eDv.d.ts → markdown-editor-Dn-0L_MM.d.ts} +8 -1
  10. package/dist/monaco.d.ts +2 -0
  11. package/dist/monaco.js +9 -0
  12. package/dist/monaco.js.map +1 -0
  13. package/package.json +9 -5
  14. package/src/ai-objects/decision-card.tsx +15 -9
  15. package/src/ai-objects/knowledge-card.tsx +18 -9
  16. package/src/barrel-monaco-lazy.test.ts +50 -0
  17. package/src/calc-block/calc-block.tsx +11 -7
  18. package/src/code-editor/code-editor.test.tsx +141 -14
  19. package/src/code-editor/code-editor.tsx +166 -35
  20. package/src/code-workspace/code-workspace.test.tsx +31 -12
  21. package/src/copy-button/copy-button.tsx +6 -3
  22. package/src/diff-editor/diff-editor.test.tsx +9 -3
  23. package/src/diff-editor/diff-editor.tsx +47 -23
  24. package/src/editor-toolbar/editor-toolbar.tsx +8 -2
  25. package/src/index.ts +4 -3
  26. package/src/markdown-academic/citations.tsx +14 -7
  27. package/src/markdown-academic/footnotes.tsx +1 -1
  28. package/src/markdown-academic/math.tsx +7 -4
  29. package/src/markdown-editor/completions/completions-menu.tsx +5 -2
  30. package/src/markdown-editor/directive-views.tsx +33 -19
  31. package/src/markdown-editor/markdown-editor.stories.tsx +9 -3
  32. package/src/markdown-editor/slash/slash-menu.tsx +5 -2
  33. package/src/markdown-editor/table-view.tsx +28 -15
  34. package/src/markdown-iteration/iteration-builder-dialog.tsx +39 -18
  35. package/src/markdown-iteration/template-dialog.tsx +25 -7
  36. package/src/markdown-outline/document-outline.tsx +6 -2
  37. package/src/markdown-preview/markdown-preview-academic.test.tsx +3 -3
  38. package/src/markdown-toolbar/markdown-toolbar.tsx +42 -24
  39. package/src/markdown-workspace/markdown-workspace.test.tsx +22 -3
  40. package/src/markdown-workspace/markdown-workspace.tsx +6 -3
  41. package/src/mermaid-diagram/mermaid-diagram-fixes.test.tsx +130 -0
  42. package/src/mermaid-diagram/mermaid-diagram.test.tsx +2 -1
  43. package/src/mermaid-diagram/mermaid-diagram.tsx +89 -40
  44. package/src/mermaid-diagram/mermaid-viewer.tsx +21 -20
  45. package/src/monaco.ts +21 -0
  46. package/dist/chunk-C62O7IOQ.js.map +0 -1
@@ -16,7 +16,7 @@
16
16
  * pre-pass over the source) so an inline `[1]` and bibliography entry 1 always agree.
17
17
  */
18
18
  import { cn } from "@elabs-ai/components-ui/lib/cn";
19
- import { Separator } from "@elabs-ai/components-ui";
19
+ import { Separator, useLocale } from "@elabs-ai/components-ui";
20
20
  import {
21
21
  createContext,
22
22
  forwardRef,
@@ -249,6 +249,7 @@ function hoverTitle(data: CitationData): string {
249
249
  }
250
250
 
251
251
  function CiteLink({ entry, label }: { entry: ResolvedCitation; label: string }) {
252
+ const { t } = useLocale();
252
253
  // For numeric style the visible label is a bare "[1]" — give AT a real name
253
254
  // (the `title` tooltip is for mouse users and is not reliably announced).
254
255
  const name = entry.data ? hoverTitle(entry.data) : entry.key;
@@ -256,13 +257,13 @@ function CiteLink({ entry, label }: { entry: ResolvedCitation; label: string })
256
257
  <a
257
258
  href={`#ref-${cssId(entry.key)}`}
258
259
  title={entry.data ? hoverTitle(entry.data) : undefined}
259
- aria-label={`Citation: ${name}`}
260
+ aria-label={t("editor.citations.citationLabel", { name })}
260
261
  // #317/#399 — the on-surface `-text` rung, NOT the `--primary` FILL: an
261
262
  // inline cite is body text inside a paragraph and owes WCAG 1.4.3 AA
262
263
  // (4.5:1), which `--primary` missed at 4.29-4.31:1 in light. The
263
264
  // resting `underline` is the separate 1.4.1 non-colour cue (#317's
264
265
  // link-in-text-block half) — keep both.
265
- className="text-primary-text underline hover:underline focus-visible:rounded-sm focus-ring"
266
+ className="text-link underline hover:underline focus-visible:rounded-sm focus-ring"
266
267
  >
267
268
  {label}
268
269
  </a>
@@ -292,6 +293,7 @@ function readCite(rest: TagProps): CitePayload | null {
292
293
 
293
294
  /** Renderer for the inline `<brand-cite>` element produced by the transform. */
294
295
  export function InlineCite({ node: _n, children: _c, ...rest }: TagProps) {
296
+ const { t } = useLocale();
295
297
  const ctx = useContext(CitationContext);
296
298
  const payload = readCite(rest);
297
299
  if (!payload) return null;
@@ -302,7 +304,7 @@ export function InlineCite({ node: _n, children: _c, ...rest }: TagProps) {
302
304
  if (!anyResolved) {
303
305
  // Graceful: nothing resolved → keep the literal, marked for sighted + AT.
304
306
  return (
305
- <span className="text-muted-foreground" title="Unresolved citation">
307
+ <span className="text-muted-foreground" title={t("editor.citations.unresolvedCitation")}>
306
308
  {payload.original}
307
309
  </span>
308
310
  );
@@ -324,7 +326,10 @@ export function InlineCite({ node: _n, children: _c, ...rest }: TagProps) {
324
326
  {entry?.data ? (
325
327
  <CiteLink entry={entry} label={label} />
326
328
  ) : (
327
- <span className="text-muted-foreground" title={`Unresolved: @${it.key}`}>
329
+ <span
330
+ className="text-muted-foreground"
331
+ title={t("editor.citations.unresolvedKey", { key: it.key })}
332
+ >
328
333
  {label}
329
334
  </span>
330
335
  )}
@@ -387,9 +392,11 @@ export interface BibliographyProps extends Omit<HTMLAttributes<HTMLElement>, "ch
387
392
  * One focal separation gesture — a top `Separator` — over a quiet label + list.
388
393
  */
389
394
  export const Bibliography = forwardRef<HTMLElement, BibliographyProps>(function Bibliography(
390
- { entries, style, title = "References", className, ...props },
395
+ { entries, style, title: titleProp, className, ...props },
391
396
  ref,
392
397
  ) {
398
+ const { t } = useLocale();
399
+ const title = titleProp ?? t("editor.citations.references");
393
400
  const ctx = useContext(CitationContext);
394
401
  const labelId = useId();
395
402
  const list = entries ?? ctx?.order ?? [];
@@ -427,7 +434,7 @@ export const Bibliography = forwardRef<HTMLElement, BibliographyProps>(function
427
434
  rel="noopener noreferrer"
428
435
  // #317/#399 — bibliography DOI/URL is body text: `-text` rung
429
436
  // + resting underline (the non-colour cue).
430
- className="break-words text-primary-text underline underline-offset-2 hover:underline focus-visible:rounded-sm focus-ring"
437
+ className="break-words text-link underline underline-offset-2 hover:underline focus-visible:rounded-sm focus-ring"
431
438
  >
432
439
  {data.url ?? `doi:${data.doi}`}
433
440
  </a>
@@ -165,7 +165,7 @@ export function FootnoteRef({ node: _n, children: _c, ...rest }: TagProps) {
165
165
  data-footnote-ref=""
166
166
  aria-label={`Footnote ${n}`}
167
167
  // #399 — a footnote marker is superscript body text: `-text` rung.
168
- className="px-0.5 font-medium text-primary-text underline tabular-nums hover:underline focus-visible:rounded-sm focus-ring"
168
+ className="px-0.5 font-medium text-link underline tabular-nums hover:underline focus-visible:rounded-sm focus-ring"
169
169
  >
170
170
  {n}
171
171
  </a>
@@ -14,6 +14,7 @@
14
14
  * a contained error, never crashes the page). a11y: `output: "htmlAndMathml"`
15
15
  * emits MathML (read by assistive tech) alongside the visual HTML.
16
16
  */
17
+ import { useLocale } from "@elabs-ai/components-ui";
17
18
  import { cn } from "@elabs-ai/components-ui/lib/cn";
18
19
  import katex from "katex";
19
20
  import { useMemo, type HTMLAttributes } from "react";
@@ -96,13 +97,14 @@ export interface MathProps extends Omit<HTMLAttributes<HTMLElement>, "children">
96
97
 
97
98
  /** Inline math (`$…$`) → KaTeX, in the text flow. */
98
99
  export function MathInline({ tex, className, ...props }: MathProps) {
100
+ const { t } = useLocale();
99
101
  const { html, error } = useMemo(() => renderKatex(tex, false), [tex]);
100
102
  if (error) {
101
103
  return (
102
104
  <code
103
105
  className={cn("text-destructive-text", className)}
104
- aria-label={`Math (could not render): ${tex}`}
105
- title="Could not render math"
106
+ aria-label={t("editor.math.renderErrorLabel", { tex })}
107
+ title={t("editor.math.renderError")}
106
108
  {...props}
107
109
  >
108
110
  {tex}
@@ -125,6 +127,7 @@ export function MathInline({ tex, className, ...props }: MathProps) {
125
127
 
126
128
  /** Block math (`$$…$$`) → centered display KaTeX. */
127
129
  export function MathBlock({ tex, className, ...props }: MathProps) {
130
+ const { t } = useLocale();
128
131
  const { html, error } = useMemo(() => renderKatex(tex, true), [tex]);
129
132
  if (error) {
130
133
  return (
@@ -133,8 +136,8 @@ export function MathBlock({ tex, className, ...props }: MathProps) {
133
136
  "overflow-x-auto rounded-md bg-surface-muted p-3 text-destructive-text",
134
137
  className,
135
138
  )}
136
- aria-label={`Math (could not render): ${tex}`}
137
- title="Could not render math"
139
+ aria-label={t("editor.math.renderErrorLabel", { tex })}
140
+ title={t("editor.math.renderError")}
138
141
  {...props}
139
142
  >
140
143
  <code>{tex}</code>
@@ -8,6 +8,7 @@
8
8
  * Same visual grammar (bg-popover, accent selection) for consistency with the
9
9
  * slash popup and Monaco's own themed suggest widget.
10
10
  */
11
+ import { useLocale } from "@elabs-ai/components-ui";
11
12
  import { cn } from "@elabs-ai/components-ui/lib/cn";
12
13
  import { forwardRef, type HTMLAttributes } from "react";
13
14
 
@@ -38,17 +39,19 @@ export const CompletionMenu = forwardRef<HTMLDivElement, CompletionMenuProps>(
38
39
  activeIndex,
39
40
  onSelect,
40
41
  idPrefix = "brand-completions",
41
- emptyLabel = "No suggestions",
42
+ emptyLabel: emptyLabelProp,
42
43
  className,
43
44
  ...props
44
45
  },
45
46
  ref,
46
47
  ) {
48
+ const { t } = useLocale();
49
+ const emptyLabel = emptyLabelProp ?? t("editor.completions.noSuggestions");
47
50
  return (
48
51
  <div
49
52
  ref={ref}
50
53
  role="listbox"
51
- aria-label="Suggestions"
54
+ aria-label={t("editor.completions.suggestions")}
52
55
  className={cn(
53
56
  "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",
54
57
  className,
@@ -46,6 +46,7 @@ import {
46
46
  DropdownMenuSubContent,
47
47
  DropdownMenuSubTrigger,
48
48
  DropdownMenuTrigger,
49
+ useLocale,
49
50
  } from "@elabs-ai/components-ui";
50
51
  import { cn } from "@elabs-ai/components-ui/lib/cn";
51
52
  import { editorViewCtx, parserCtx, serializerCtx } from "@milkdown/kit/core";
@@ -177,6 +178,7 @@ function InlineEdit({ value, onCommit, ariaLabel, placeholder, className }: Inli
177
178
 
178
179
  /** `:::name` block directives → live @brand component with an editable body. */
179
180
  function ContainerDirectiveView() {
181
+ const { t } = useLocale();
180
182
  const { contentRef } = useNodeViewContext();
181
183
  const { name, attributes, update } = useDirectiveAttrs();
182
184
 
@@ -189,8 +191,8 @@ function ContainerDirectiveView() {
189
191
  <CardHeader className="pb-3">
190
192
  <CardTitle>
191
193
  <InlineEdit
192
- ariaLabel="Card title"
193
- placeholder="Card title"
194
+ ariaLabel={t("editor.directiveViews.cardTitle")}
195
+ placeholder={t("editor.directiveViews.cardTitle")}
194
196
  value={attributes.title ?? ""}
195
197
  onCommit={(v) => update("title", v)}
196
198
  />
@@ -213,7 +215,7 @@ function ContainerDirectiveView() {
213
215
  flow, so its label must not join the document heading outline (see #21). */}
214
216
  <div className="mb-1 font-medium leading-none tracking-tight">
215
217
  <InlineEdit
216
- ariaLabel="Callout title"
218
+ ariaLabel={t("editor.directiveViews.calloutTitle")}
217
219
  placeholder={capitalize(attributes.type ?? "note")}
218
220
  value={attributes.title ?? ""}
219
221
  onCommit={(v) => update("title", v)}
@@ -250,7 +252,9 @@ function ContainerDirectiveView() {
250
252
  className="brand-directive brand-directive--unknown"
251
253
  data-brand-directive={name}
252
254
  >
253
- <div className="mb-1 font-medium leading-none tracking-tight">Unknown block: {name}</div>
255
+ <div className="mb-1 font-medium leading-none tracking-tight">
256
+ {t("editor.directiveViews.unknownBlock", { name })}
257
+ </div>
254
258
  <AlertDescription>{body}</AlertDescription>
255
259
  </Alert>
256
260
  );
@@ -492,6 +496,7 @@ function IterationMenuItems({
492
496
  * inline (today's behaviour, unchanged).
493
497
  */
494
498
  function IterationDirectiveView() {
499
+ const { t } = useLocale();
495
500
  const { contentRef, node, getPos, setAttrs } = useNodeViewContext();
496
501
  const { name, attributes } = useDirectiveAttrs();
497
502
  const [, getInstance] = useInstance();
@@ -577,7 +582,7 @@ function IterationDirectiveView() {
577
582
  attributes: attributes as Record<string, string>,
578
583
  }).cells.length > 0;
579
584
 
580
- const disabledHint = "— needs embedded values";
585
+ const disabledHint = t("editor.directiveViews.needsEmbeddedValues");
581
586
 
582
587
  const convertToStatic = () => {
583
588
  const template = readBodyMarkdown(getInstance as GetEditor, node);
@@ -589,14 +594,14 @@ function IterationDirectiveView() {
589
594
  {
590
595
  type: "item",
591
596
  id: "edit",
592
- label: "Edit iteration…",
597
+ label: t("editor.directiveViews.editIteration"),
593
598
  icon: <Pencil className="size-4" aria-hidden="true" />,
594
599
  onSelect: requestEdit,
595
600
  },
596
601
  {
597
602
  type: "layout",
598
603
  id: "layout",
599
- label: "Change layout",
604
+ label: t("editor.directiveViews.changeLayout"),
600
605
  icon: <LayoutGrid className="size-4" aria-hidden="true" />,
601
606
  value: (attributes.layout as IterationLayout) || ITERATION_LAYOUTS[kind][0]!,
602
607
  options: ITERATION_LAYOUTS[kind],
@@ -607,7 +612,9 @@ function IterationDirectiveView() {
607
612
  {
608
613
  type: "item",
609
614
  id: "transpose",
610
- label: hasEmbeddedData ? "Transpose" : `Transpose ${disabledHint}`,
615
+ label: hasEmbeddedData
616
+ ? t("editor.directiveViews.transpose")
617
+ : `${t("editor.directiveViews.transpose")} ${disabledHint}`,
611
618
  icon: <ArrowLeftRight className="size-4" aria-hidden="true" />,
612
619
  onSelect: transpose,
613
620
  disabled: !hasEmbeddedData,
@@ -617,7 +624,9 @@ function IterationDirectiveView() {
617
624
  {
618
625
  type: "item",
619
626
  id: "convert-to-static",
620
- label: hasEmbeddedData ? "Convert to static" : `Convert to static ${disabledHint}`,
627
+ label: hasEmbeddedData
628
+ ? t("editor.directiveViews.convertToStatic")
629
+ : `${t("editor.directiveViews.convertToStatic")} ${disabledHint}`,
621
630
  icon: <FileText className="size-4" aria-hidden="true" />,
622
631
  onSelect: convertToStatic,
623
632
  disabled: !hasEmbeddedData,
@@ -627,11 +636,15 @@ function IterationDirectiveView() {
627
636
  const header = (
628
637
  <div className="mb-1.5 flex items-center gap-1.5 text-meta font-medium text-info-text">
629
638
  <Icon className="size-3.5 shrink-0" aria-hidden="true" />
630
- <span>{isPivot ? "Pivot" : "Iterate"}</span>
639
+ <span>{isPivot ? t("editor.directiveViews.pivot") : t("editor.directiveViews.iterate")}</span>
631
640
  {!isPivot && attributes.as ? (
632
- <span className="font-normal text-muted-foreground">· per {attributes.as}</span>
641
+ <span className="font-normal text-muted-foreground">
642
+ {t("editor.directiveViews.perItem", { as: attributes.as })}
643
+ </span>
633
644
  ) : null}
634
- <span className="font-normal text-muted-foreground">— template</span>
645
+ <span className="font-normal text-muted-foreground">
646
+ {t("editor.directiveViews.templateSuffix")}
647
+ </span>
635
648
  {onEdit ? (
636
649
  <DropdownMenu>
637
650
  <DropdownMenuTrigger asChild>
@@ -639,8 +652,8 @@ function IterationDirectiveView() {
639
652
  type="button"
640
653
  // `data-directive-chrome` routes the click to the browser, not ProseMirror.
641
654
  data-directive-chrome=""
642
- aria-label="Iteration actions"
643
- title="Iteration actions…"
655
+ aria-label={t("editor.directiveViews.iterationActions")}
656
+ title={t("editor.directiveViews.iterationActionsTitle")}
644
657
  className="ms-auto inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground focus-ring"
645
658
  >
646
659
  <MoreHorizontal className="size-4" aria-hidden="true" />
@@ -700,6 +713,7 @@ function IterationDirectiveView() {
700
713
 
701
714
  /** `::name` leaf directives (e.g. `::metric`) → live, atomic @brand component. */
702
715
  function LeafDirectiveView() {
716
+ const { t } = useLocale();
703
717
  const { name, attributes, update } = useDirectiveAttrs();
704
718
 
705
719
  if (name !== "metric") {
@@ -708,7 +722,7 @@ function LeafDirectiveView() {
708
722
  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"
709
723
  data-brand-leaf={name}
710
724
  >
711
- Unknown inline block: <code>::{name}</code>
725
+ {t("editor.directiveViews.unknownInlineBlock")} <code>::{name}</code>
712
726
  </div>
713
727
  );
714
728
  }
@@ -720,16 +734,16 @@ function LeafDirectiveView() {
720
734
  data-brand-leaf="metric"
721
735
  label={
722
736
  <InlineEdit
723
- ariaLabel="Metric label"
724
- placeholder="Label"
737
+ ariaLabel={t("editor.directiveViews.metricLabel")}
738
+ placeholder={t("editor.directiveViews.metricLabelPlaceholder")}
725
739
  value={attributes.label ?? ""}
726
740
  onCommit={(v) => update("label", v)}
727
741
  />
728
742
  }
729
743
  value={
730
744
  <InlineEdit
731
- ariaLabel="Metric value"
732
- placeholder="0"
745
+ ariaLabel={t("editor.directiveViews.metricValue")}
746
+ placeholder={t("editor.directiveViews.metricValuePlaceholder")}
733
747
  value={attributes.value ?? ""}
734
748
  onCommit={(v) => update("value", v)}
735
749
  className="min-w-[1ch]"
@@ -530,6 +530,7 @@ export const IterationContextMenu: Story = {
530
530
  * to localStorage across runs.
531
531
  */
532
532
  export const NodeMenuDark: Story = {
533
+ tags: ["!dev"],
533
534
  name: "Iteration node menu (⋯ dropdown) — dark",
534
535
  decorators: [
535
536
  (Story) => (
@@ -543,6 +544,7 @@ export const NodeMenuDark: Story = {
543
544
  };
544
545
 
545
546
  export const NodeMenuHighDecoration: Story = {
547
+ tags: ["!dev"],
546
548
  name: "Iteration node menu (⋯ dropdown) — high decoration",
547
549
  globals: { decoration: "10" },
548
550
  render: NodeMenu.render,
@@ -550,6 +552,7 @@ export const NodeMenuHighDecoration: Story = {
550
552
  };
551
553
 
552
554
  export const IterationContextMenuDark: Story = {
555
+ tags: ["!dev"],
553
556
  name: "Iteration node menu (right-click) — dark",
554
557
  decorators: [
555
558
  (Story) => (
@@ -563,6 +566,7 @@ export const IterationContextMenuDark: Story = {
563
566
  };
564
567
 
565
568
  export const IterationContextMenuHighDecoration: Story = {
569
+ tags: ["!dev"],
566
570
  name: "Iteration node menu (right-click) — high decoration",
567
571
  globals: { decoration: "10" },
568
572
  render: IterationContextMenu.render,
@@ -580,7 +584,7 @@ export const IterationContextMenuHighDecoration: Story = {
580
584
  * stylesheet TEXT; this is the rendered-surface proof.
581
585
  */
582
586
  export const FocusIndicator: Story = {
583
- name: "Focus indicator (#309)",
587
+ name: "Keyboard focus",
584
588
  render: () => (
585
589
  <div className="mx-auto max-w-3xl p-6">
586
590
  <MarkdownEditor
@@ -661,7 +665,8 @@ export const FocusIndicator: Story = {
661
665
  };
662
666
 
663
667
  export const FocusIndicatorDark: Story = {
664
- name: "Focus indicator (#309) — dark",
668
+ tags: ["!dev"],
669
+ name: "Keyboard focus — dark",
665
670
  decorators: [
666
671
  (Story) => (
667
672
  <ThemeProvider defaultTheme="dark" storageKey={null}>
@@ -674,7 +679,8 @@ export const FocusIndicatorDark: Story = {
674
679
  };
675
680
 
676
681
  export const FocusIndicatorHighDecoration: Story = {
677
- name: "Focus indicator (#309) — high decoration",
682
+ tags: ["!dev"],
683
+ name: "Keyboard focus — high decoration",
678
684
  globals: { decoration: "10" },
679
685
  render: FocusIndicator.render,
680
686
  play: FocusIndicator.play,
@@ -14,6 +14,7 @@
14
14
  * Semantic tokens only; matches the `Command` popover look (bg-popover, accent
15
15
  * selection, muted group headings).
16
16
  */
17
+ import { useLocale } from "@elabs-ai/components-ui";
17
18
  import { cn } from "@elabs-ai/components-ui/lib/cn";
18
19
  import { forwardRef, type HTMLAttributes } from "react";
19
20
 
@@ -43,12 +44,14 @@ export const SlashMenu = forwardRef<HTMLDivElement, SlashMenuProps>(function Sla
43
44
  activeId,
44
45
  onSelect,
45
46
  idPrefix = "brand-slash",
46
- emptyLabel = "No matching blocks",
47
+ emptyLabel: emptyLabelProp,
47
48
  className,
48
49
  ...props
49
50
  },
50
51
  ref,
51
52
  ) {
53
+ const { t } = useLocale();
54
+ const emptyLabel = emptyLabelProp ?? t("editor.slashMenu.noMatchingBlocks");
52
55
  const groups = groupSlashCommands(commands);
53
56
 
54
57
  return (
@@ -57,7 +60,7 @@ export const SlashMenu = forwardRef<HTMLDivElement, SlashMenuProps>(function Sla
57
60
  // The editor surface carries `role="textbox"`; this list is its popup. The
58
61
  // plugin sets `aria-controls`/`aria-activedescendant` on the textbox.
59
62
  role="listbox"
60
- aria-label="Insert block"
63
+ aria-label={t("editor.slashMenu.insertBlock")}
61
64
  className={cn(
62
65
  "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",
63
66
  className,
@@ -42,6 +42,7 @@ import { Plugin, PluginKey } from "@milkdown/kit/prose/state";
42
42
  import type { EditorView } from "@milkdown/kit/prose/view";
43
43
  import { $prose } from "@milkdown/kit/utils";
44
44
  import { isInTable } from "@milkdown/kit/prose/tables";
45
+ import { useLocale } from "@elabs-ai/components-ui";
45
46
  import { cn } from "@elabs-ai/components-ui/lib/cn";
46
47
  import { usePluginViewContext } from "@prosemirror-adapter/react";
47
48
  import type { usePluginViewFactory } from "@prosemirror-adapter/react";
@@ -137,39 +138,49 @@ function ToolbarDivider() {
137
138
  */
138
139
  function TableControlsView() {
139
140
  const { view } = usePluginViewContext();
141
+ const { t } = useLocale();
140
142
  const inTable = isInTable(view.state);
141
143
  const run = (key: Parameters<typeof dispatchCommand>[1]) => () => dispatchCommand(view, key);
142
144
 
143
145
  if (!inTable) return null;
144
146
 
147
+ const addRowAbove = t("editor.tableView.addRowAbove");
148
+ const addRowBelow = t("editor.tableView.addRowBelow");
149
+ const deleteRow = t("editor.tableView.deleteRow");
150
+ const addColLeft = t("editor.tableView.addColumnLeft");
151
+ const addColRight = t("editor.tableView.addColumnRight");
152
+ const deleteCol = t("editor.tableView.deleteColumn");
153
+
145
154
  return (
146
155
  <div
147
156
  role="toolbar"
148
- aria-label="Table controls"
157
+ aria-label={t("editor.tableView.tableControls")}
149
158
  className={cn(
150
159
  "flex flex-wrap items-center gap-1 px-2 py-1.5",
151
160
  "border-t border-border-strong bg-surface-muted",
152
161
  )}
153
162
  >
154
163
  {/* Row group */}
155
- <span className="mr-1 select-none text-caption text-muted-foreground">Row</span>
164
+ <span className="mr-1 select-none text-caption text-muted-foreground">
165
+ {t("editor.tableView.row")}
166
+ </span>
156
167
  <ToolbarButton
157
- ariaLabel="Add row above"
158
- title="Add row above"
168
+ ariaLabel={addRowAbove}
169
+ title={addRowAbove}
159
170
  onClick={run(addRowBeforeCommand.key)}
160
171
  >
161
172
  ↑+
162
173
  </ToolbarButton>
163
174
  <ToolbarButton
164
- ariaLabel="Add row below"
165
- title="Add row below"
175
+ ariaLabel={addRowBelow}
176
+ title={addRowBelow}
166
177
  onClick={run(addRowAfterCommand.key)}
167
178
  >
168
179
  ↓+
169
180
  </ToolbarButton>
170
181
  <ToolbarButton
171
- ariaLabel="Delete row"
172
- title="Delete row"
182
+ ariaLabel={deleteRow}
183
+ title={deleteRow}
173
184
  variant="destructive"
174
185
  onClick={run(deleteSelectedCellsCommand.key)}
175
186
  >
@@ -179,24 +190,26 @@ function TableControlsView() {
179
190
  <ToolbarDivider />
180
191
 
181
192
  {/* Column group */}
182
- <span className="mr-1 select-none text-caption text-muted-foreground">Col</span>
193
+ <span className="mr-1 select-none text-caption text-muted-foreground">
194
+ {t("editor.tableView.col")}
195
+ </span>
183
196
  <ToolbarButton
184
- ariaLabel="Add column left"
185
- title="Add column left"
197
+ ariaLabel={addColLeft}
198
+ title={addColLeft}
186
199
  onClick={run(addColBeforeCommand.key)}
187
200
  >
188
201
  ←+
189
202
  </ToolbarButton>
190
203
  <ToolbarButton
191
- ariaLabel="Add column right"
192
- title="Add column right"
204
+ ariaLabel={addColRight}
205
+ title={addColRight}
193
206
  onClick={run(addColAfterCommand.key)}
194
207
  >
195
208
  →+
196
209
  </ToolbarButton>
197
210
  <ToolbarButton
198
- ariaLabel="Delete column"
199
- title="Delete column"
211
+ ariaLabel={deleteCol}
212
+ title={deleteCol}
200
213
  variant="destructive"
201
214
  onClick={run(deleteSelectedCellsCommand.key)}
202
215
  >