@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.
- package/README.md +6 -7
- package/dist/{chunk-LBC5VJBD.js → chunk-FO5S3YZM.js} +349 -177
- package/dist/chunk-FO5S3YZM.js.map +1 -0
- package/dist/index.css +4 -2
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +112 -57
- package/dist/index.js.map +1 -1
- package/dist/markdown/index.css +4 -2
- package/dist/markdown/index.css.map +1 -1
- package/dist/markdown/index.d.ts +2 -2
- package/dist/markdown/index.js +268 -191
- package/dist/markdown/index.js.map +1 -1
- package/dist/{markdown-editor-DfBZibAn.d.ts → markdown-editor-Dn-0L_MM.d.ts} +9 -2
- package/dist/monaco.d.ts +2 -0
- package/dist/monaco.js +9 -0
- package/dist/monaco.js.map +1 -0
- package/package.json +10 -6
- package/src/ai-objects/decision-card.tsx +15 -9
- package/src/ai-objects/entity.tsx +1 -1
- package/src/ai-objects/knowledge-card.tsx +18 -9
- package/src/barrel-monaco-lazy.test.ts +50 -0
- package/src/calc-block/calc-block.tsx +10 -4
- package/src/calc-block/calc-editor.css +1 -1
- package/src/code-editor/code-editor.stories.tsx +76 -1
- package/src/code-editor/code-editor.test.tsx +141 -14
- package/src/code-editor/code-editor.tsx +167 -36
- package/src/code-workspace/code-workspace.test.tsx +113 -11
- package/src/code-workspace/code-workspace.tsx +73 -20
- package/src/copy-button/copy-button.tsx +6 -3
- package/src/diff-editor/diff-editor.stories.tsx +9 -1
- package/src/diff-editor/diff-editor.test.tsx +9 -3
- package/src/diff-editor/diff-editor.tsx +47 -23
- package/src/editor-toolbar/editor-toolbar.tsx +8 -2
- package/src/index.ts +4 -3
- package/src/lib/monaco-deep-imports.d.ts +59 -0
- package/src/lib/monaco-theme-bridge.test.ts +307 -0
- package/src/lib/monaco-theme-bridge.ts +154 -10
- package/src/markdown-academic/citations.tsx +14 -7
- package/src/markdown-academic/footnotes.tsx +2 -2
- package/src/markdown-academic/math.tsx +7 -4
- package/src/markdown-academic/toc.tsx +1 -1
- package/src/markdown-editor/completions/completions-menu.tsx +5 -2
- package/src/markdown-editor/directive-views.tsx +35 -24
- package/src/markdown-editor/markdown-editor.css +26 -3
- package/src/markdown-editor/markdown-editor.focus.test.ts +49 -0
- package/src/markdown-editor/markdown-editor.stories.tsx +128 -7
- package/src/markdown-editor/markdown-editor.tsx +8 -5
- package/src/markdown-editor/milkdown-react/use-get-editor.timer-leak.test.ts +80 -0
- package/src/markdown-editor/milkdown-react/use-get-editor.ts +37 -1
- package/src/markdown-editor/paste-embed.ts +2 -1
- package/src/markdown-editor/slash/slash-menu.stories.tsx +8 -3
- package/src/markdown-editor/slash/slash-menu.test.tsx +27 -0
- package/src/markdown-editor/slash/slash-menu.tsx +18 -3
- package/src/markdown-editor/table-view.tsx +31 -17
- package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +57 -19
- package/src/markdown-iteration/iteration-builder-dialog.tsx +39 -18
- package/src/markdown-iteration/template-dialog.tsx +25 -7
- package/src/markdown-outline/document-outline.stories.tsx +1 -1
- package/src/markdown-outline/document-outline.tsx +7 -3
- package/src/markdown-preview/markdown-preview-academic.test.tsx +3 -3
- package/src/markdown-preview/markdown-preview-transclusion.test.tsx +1 -1
- package/src/markdown-preview/markdown-preview.stories.tsx +24 -2
- package/src/markdown-preview/markdown-preview.test.tsx +20 -3
- package/src/markdown-toolbar/markdown-toolbar.stories.tsx +3 -0
- package/src/markdown-toolbar/markdown-toolbar.tsx +42 -24
- package/src/markdown-workspace/markdown-workspace.test.tsx +53 -4
- package/src/markdown-workspace/markdown-workspace.tsx +6 -3
- package/src/mermaid-diagram/mermaid-diagram-fixes.test.tsx +130 -0
- package/src/mermaid-diagram/mermaid-diagram.test.tsx +2 -1
- package/src/mermaid-diagram/mermaid-diagram.tsx +89 -40
- package/src/mermaid-diagram/mermaid-viewer.tsx +22 -21
- package/src/monaco.ts +21 -0
- package/src/prose/prose.stories.tsx +3 -0
- package/src/prose/prose.test.ts +54 -0
- package/src/prose/prose.tsx +7 -0
- package/dist/chunk-LBC5VJBD.js.map +0 -1
|
@@ -9,6 +9,13 @@ import { Command } from '@milkdown/kit/prose/state';
|
|
|
9
9
|
type MonacoCodeEditor = monaco.editor.IStandaloneCodeEditor;
|
|
10
10
|
/** A Monaco editor action (system command + optional hotkey + palette/menu entry). */
|
|
11
11
|
type EditorAction = monaco.editor.IActionDescriptor;
|
|
12
|
+
/**
|
|
13
|
+
* The runtime value type handed back by `import("monaco-editor")` — same
|
|
14
|
+
* namespace as the type-only `monaco` import above (`typeof` a type-only
|
|
15
|
+
* namespace import resolves to the module's own type), used for `monacoRef`
|
|
16
|
+
* and `onMount`'s second argument.
|
|
17
|
+
*/
|
|
18
|
+
type MonacoNamespace = typeof monaco;
|
|
12
19
|
interface CodeEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> {
|
|
13
20
|
/** Controlled content. Pair with `onChange`. */
|
|
14
21
|
value?: string;
|
|
@@ -44,7 +51,7 @@ interface CodeEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange
|
|
|
44
51
|
*/
|
|
45
52
|
contextMenu?: "brand" | "monaco" | "none";
|
|
46
53
|
/** Called once the editor instance + monaco namespace are ready. */
|
|
47
|
-
onMount?: (editor: MonacoCodeEditor, monacoApi:
|
|
54
|
+
onMount?: (editor: MonacoCodeEditor, monacoApi: MonacoNamespace) => void;
|
|
48
55
|
/**
|
|
49
56
|
* Declarative Monaco editor actions — each registers a command (run on its
|
|
50
57
|
* `keybindings`, in the command palette, and optionally the context menu via
|
|
@@ -62,7 +69,7 @@ interface CodeEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange
|
|
|
62
69
|
* A token-themed Monaco editor wrapped as a brand-ui React component. Monaco
|
|
63
70
|
* renders its own editing surface + widgets; this wrapper owns lifecycle,
|
|
64
71
|
* controlled/uncontrolled value, and applies the brand theme bridge so the
|
|
65
|
-
* editor matches the active `data-theme` (
|
|
72
|
+
* editor matches the active `data-theme` (every theme).
|
|
66
73
|
*
|
|
67
74
|
* Workers (for completions/diagnostics) are wired by importing
|
|
68
75
|
* `@elabs-ai/components-editor/monaco-environment` once at the app entry.
|
package/dist/monaco.d.ts
ADDED
package/dist/monaco.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/monaco.ts"],"sourcesContent":["\"use client\";\n\n/**\n * The `monaco-editor` namespace, as its own subpath (ADR 0006 subpath exports).\n *\n * The root barrel (`.`) exports lightweight chrome — `CopyButton`,\n * `EDITOR_LANGUAGES`, `languageLabel` — that a consumer should be able to\n * import WITHOUT pulling Monaco in at all: the editing surfaces\n * (`CodeEditor`, `DiffEditor`, `MarkdownEditor`) already own lazy-loading the\n * engine themselves. Re-exporting the whole `monaco-editor` namespace from\n * that SAME barrel defeated that split — `import { CopyButton } from\n * \"@elabs-ai/components-editor\"` pulled megabytes of Monaco and touched\n * browser globals at import time, breaking SSR/RSC for a component that never\n * renders the editor.\n *\n * A consumer that needs to build a custom editor, or wire Monaco commands\n * directly, imports this subpath instead:\n *\n * import { monaco } from \"@elabs-ai/components-editor/monaco\";\n */\nexport * as monaco from \"monaco-editor\";\n"],"mappings":";;;;AAoBA,YAAY,YAAY;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elabs-ai/components-editor",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"./monaco-environment": {
|
|
37
37
|
"types": "./dist/lib/monaco-environment.d.ts",
|
|
38
38
|
"default": "./dist/lib/monaco-environment.js"
|
|
39
|
+
},
|
|
40
|
+
"./monaco": {
|
|
41
|
+
"types": "./dist/monaco.d.ts",
|
|
42
|
+
"default": "./dist/monaco.js"
|
|
39
43
|
}
|
|
40
44
|
},
|
|
41
45
|
"publishConfig": {
|
|
@@ -65,8 +69,8 @@
|
|
|
65
69
|
"monaco-editor": "^0.55.1",
|
|
66
70
|
"react": "^18.2.0 || ^19.0.0",
|
|
67
71
|
"react-dom": "^18.2.0 || ^19.0.0",
|
|
68
|
-
"@elabs-ai/components-tokens": "4.
|
|
69
|
-
"@elabs-ai/components-ui": "4.
|
|
72
|
+
"@elabs-ai/components-tokens": "4.2.0",
|
|
73
|
+
"@elabs-ai/components-ui": "4.2.0"
|
|
70
74
|
},
|
|
71
75
|
"devDependencies": {
|
|
72
76
|
"@testing-library/jest-dom": "^6.6.3",
|
|
@@ -85,10 +89,10 @@
|
|
|
85
89
|
"tsup": "^8.3.5",
|
|
86
90
|
"typescript": "^5.7.3",
|
|
87
91
|
"vitest": "^3.0.2",
|
|
92
|
+
"@elabs-ai/components-eslint-config": "0.1.0",
|
|
93
|
+
"@elabs-ai/components-tokens": "4.2.0",
|
|
88
94
|
"@elabs-ai/components-typescript-config": "0.1.0",
|
|
89
|
-
"@elabs-ai/components-
|
|
90
|
-
"@elabs-ai/components-ui": "4.0.0",
|
|
91
|
-
"@elabs-ai/components-eslint-config": "0.1.0"
|
|
95
|
+
"@elabs-ai/components-ui": "4.2.0"
|
|
92
96
|
},
|
|
93
97
|
"scripts": {
|
|
94
98
|
"build": "rm -rf dist && tsup && node ../../scripts/link-dist-css.mjs",
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
CardContent,
|
|
25
25
|
CardHeader,
|
|
26
26
|
Separator,
|
|
27
|
+
useLocale,
|
|
27
28
|
type BadgeProps,
|
|
28
29
|
} from "@elabs-ai/components-ui";
|
|
29
30
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
@@ -51,11 +52,12 @@ const STATUS_BADGE_VARIANT: Record<DecisionStatus, BadgeProps["variant"]> = {
|
|
|
51
52
|
superseded: "secondary",
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
/** Status → translation key (see `editor.decisionCard.status.*` in messages.ts). */
|
|
56
|
+
const STATUS_LABEL_KEYS: Record<DecisionStatus, string> = {
|
|
57
|
+
accepted: "editor.decisionCard.statusAccepted",
|
|
58
|
+
rejected: "editor.decisionCard.statusRejected",
|
|
59
|
+
proposed: "editor.decisionCard.statusProposed",
|
|
60
|
+
superseded: "editor.decisionCard.statusSuperseded",
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
const STATUS_ICONS: Record<DecisionStatus, ComponentType<SVGProps<SVGSVGElement>>> = {
|
|
@@ -113,11 +115,12 @@ export const DecisionCard = forwardRef<HTMLElement, DecisionCardProps>(function
|
|
|
113
115
|
{ status: rawStatus, date, alternatives, children, className, ...props },
|
|
114
116
|
ref,
|
|
115
117
|
) {
|
|
118
|
+
const { t } = useLocale();
|
|
116
119
|
const status: DecisionStatus = isDecisionStatus(rawStatus ?? "")
|
|
117
120
|
? (rawStatus as DecisionStatus)
|
|
118
121
|
: "proposed";
|
|
119
122
|
const badgeVariant = STATUS_BADGE_VARIANT[status];
|
|
120
|
-
const label =
|
|
123
|
+
const label = t(STATUS_LABEL_KEYS[status]);
|
|
121
124
|
const Icon = STATUS_ICONS[status];
|
|
122
125
|
|
|
123
126
|
const altItems = alternatives
|
|
@@ -130,7 +133,7 @@ export const DecisionCard = forwardRef<HTMLElement, DecisionCardProps>(function
|
|
|
130
133
|
return (
|
|
131
134
|
<section
|
|
132
135
|
ref={ref}
|
|
133
|
-
aria-label={
|
|
136
|
+
aria-label={t("editor.decisionCard.label", { label })}
|
|
134
137
|
className={cn("not-prose", className)}
|
|
135
138
|
{...props}
|
|
136
139
|
>
|
|
@@ -158,9 +161,12 @@ export const DecisionCard = forwardRef<HTMLElement, DecisionCardProps>(function
|
|
|
158
161
|
<Separator />
|
|
159
162
|
<div className="px-6 py-4">
|
|
160
163
|
<p className="mb-2 text-meta font-medium text-muted-foreground">
|
|
161
|
-
|
|
164
|
+
{t("editor.decisionCard.alternativesConsidered")}
|
|
162
165
|
</p>
|
|
163
|
-
<ul
|
|
166
|
+
<ul
|
|
167
|
+
className="flex flex-wrap gap-1.5"
|
|
168
|
+
aria-label={t("editor.decisionCard.alternativesConsidered")}
|
|
169
|
+
>
|
|
164
170
|
{altItems.map((alt) => (
|
|
165
171
|
<li key={alt}>
|
|
166
172
|
<Badge variant="outline" className="text-meta">
|
|
@@ -62,7 +62,7 @@ function kindMeta(kind: string | undefined): KindMeta {
|
|
|
62
62
|
export const entityChipVariants = cva(
|
|
63
63
|
// inline-flex + align-middle keeps the chip in the text baseline;
|
|
64
64
|
// no `block` wrapper so it is valid inside <p>.
|
|
65
|
-
"inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 text-meta font-medium align-middle focus-
|
|
65
|
+
"inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 text-meta font-medium align-middle focus-ring",
|
|
66
66
|
{
|
|
67
67
|
variants: {
|
|
68
68
|
kind: {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `KnowledgeCard` is standalone (no `resolve` → sources rendered as plain labels).
|
|
19
19
|
* The `knowledgeDirective({ resolve })` factory wires the hook for directive use.
|
|
20
20
|
*/
|
|
21
|
-
import { Card, CardContent, CardFooter } from "@elabs-ai/components-ui";
|
|
21
|
+
import { Card, CardContent, CardFooter, useLocale } from "@elabs-ai/components-ui";
|
|
22
22
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
23
23
|
import { BookOpen, FileText } from "lucide-react";
|
|
24
24
|
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
@@ -49,6 +49,7 @@ interface SourceRowProps {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function SourceRow({ path, resolve }: SourceRowProps) {
|
|
52
|
+
const { t } = useLocale();
|
|
52
53
|
const resolved = resolve ? resolve(path) : null;
|
|
53
54
|
const display = resolved?.title ?? path;
|
|
54
55
|
|
|
@@ -58,9 +59,9 @@ function SourceRow({ path, resolve }: SourceRowProps) {
|
|
|
58
59
|
href={resolved.href}
|
|
59
60
|
rel="noopener noreferrer"
|
|
60
61
|
target="_blank"
|
|
61
|
-
// #399 — a source link is TEXT:
|
|
62
|
-
className="flex min-w-0 items-center gap-1.5 text-meta text-
|
|
63
|
-
aria-label={
|
|
62
|
+
// #399 — a source link is TEXT: the `--link` ink, not the fill.
|
|
63
|
+
className="flex min-w-0 items-center gap-1.5 text-meta text-link underline-offset-2 hover:underline focus-ring"
|
|
64
|
+
aria-label={t("editor.knowledgeCard.source", { name: display })}
|
|
64
65
|
>
|
|
65
66
|
<FileText className="size-3 shrink-0" aria-hidden="true" />
|
|
66
67
|
<span className="truncate">{display}</span>
|
|
@@ -71,7 +72,7 @@ function SourceRow({ path, resolve }: SourceRowProps) {
|
|
|
71
72
|
return (
|
|
72
73
|
<span
|
|
73
74
|
className="flex min-w-0 items-center gap-1.5 text-meta text-muted-foreground"
|
|
74
|
-
aria-label={
|
|
75
|
+
aria-label={t("editor.knowledgeCard.sourceUnresolved", { name: display })}
|
|
75
76
|
>
|
|
76
77
|
<FileText className="size-3 shrink-0" aria-hidden="true" />
|
|
77
78
|
<span className="truncate">{display}</span>
|
|
@@ -102,6 +103,7 @@ export const KnowledgeCard = forwardRef<HTMLElement, KnowledgeCardProps>(functio
|
|
|
102
103
|
{ sources, resolve, children, className, ...props },
|
|
103
104
|
ref,
|
|
104
105
|
) {
|
|
106
|
+
const { t } = useLocale();
|
|
105
107
|
const sourcePaths = sources
|
|
106
108
|
? sources
|
|
107
109
|
.split(",")
|
|
@@ -112,7 +114,7 @@ export const KnowledgeCard = forwardRef<HTMLElement, KnowledgeCardProps>(functio
|
|
|
112
114
|
return (
|
|
113
115
|
<section
|
|
114
116
|
ref={ref}
|
|
115
|
-
aria-label="
|
|
117
|
+
aria-label={t("editor.knowledgeCard.label")}
|
|
116
118
|
className={cn("not-prose", className)}
|
|
117
119
|
{...props}
|
|
118
120
|
>
|
|
@@ -120,15 +122,22 @@ export const KnowledgeCard = forwardRef<HTMLElement, KnowledgeCardProps>(functio
|
|
|
120
122
|
<CardContent className="pt-4">
|
|
121
123
|
<div className="mb-2 flex items-center gap-1.5">
|
|
122
124
|
<BookOpen className="size-3.5 shrink-0 text-info-text" aria-hidden="true" />
|
|
123
|
-
<span className="text-meta font-medium text-info-text">
|
|
125
|
+
<span className="text-meta font-medium text-info-text">
|
|
126
|
+
{t("editor.knowledgeCard.heading")}
|
|
127
|
+
</span>
|
|
124
128
|
</div>
|
|
125
129
|
<div className="text-body text-foreground">{children}</div>
|
|
126
130
|
</CardContent>
|
|
127
131
|
|
|
128
132
|
{sourcePaths.length > 0 ? (
|
|
129
133
|
<CardFooter className="flex-col items-start gap-1 border-t border-border pt-3">
|
|
130
|
-
<p className="text-meta font-medium text-muted-foreground">
|
|
131
|
-
|
|
134
|
+
<p className="text-meta font-medium text-muted-foreground">
|
|
135
|
+
{t("editor.knowledgeCard.sources")}
|
|
136
|
+
</p>
|
|
137
|
+
<ul
|
|
138
|
+
className="flex w-full flex-col gap-1"
|
|
139
|
+
aria-label={t("editor.knowledgeCard.sources")}
|
|
140
|
+
>
|
|
132
141
|
{sourcePaths.map((path) => (
|
|
133
142
|
<li key={path} className="min-w-0">
|
|
134
143
|
<SourceRow path={path} resolve={resolve} />
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The root barrel (`.`) exports lightweight chrome (`CopyButton`,
|
|
5
|
+
* `EDITOR_LANGUAGES`, `languageLabel`) alongside the Monaco-backed editing
|
|
6
|
+
* surfaces (`CodeEditor`, `DiffEditor`, `CodeWorkspace`). A consumer that only
|
|
7
|
+
* wants the chrome must never pay for Monaco: `monaco-editor` touches browser
|
|
8
|
+
* globals at import time and is megabytes on the wire, breaking SSR/RSC for a
|
|
9
|
+
* component that never renders an editor.
|
|
10
|
+
*
|
|
11
|
+
* `code-editor.tsx`/`diff-editor.tsx` used to `import * as monaco from
|
|
12
|
+
* "monaco-editor"` at the top of the module — a VALUE import, evaluated the
|
|
13
|
+
* moment anything imports the barrel (native ESM instantiates every module in
|
|
14
|
+
* the graph, regardless of which named export is actually read). This test
|
|
15
|
+
* locks the fix: mock `monaco-editor` to THROW on evaluation, then import the
|
|
16
|
+
* barrel and prove the lightweight exports still work. If either editing
|
|
17
|
+
* surface regresses to a top-level value import, this fails at the `import`
|
|
18
|
+
* statement itself.
|
|
19
|
+
*/
|
|
20
|
+
vi.mock("monaco-editor", () => {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"monaco-editor must not be evaluated merely by importing the @elabs-ai/components-editor barrel",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("@elabs-ai/components-editor barrel", () => {
|
|
27
|
+
it("importing the barrel never evaluates monaco-editor", async () => {
|
|
28
|
+
await expect(import("./index")).resolves.toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("CopyButton is usable without evaluating monaco-editor", async () => {
|
|
32
|
+
const { CopyButton } = await import("./index");
|
|
33
|
+
expect(CopyButton).toBeTypeOf("function");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("EDITOR_LANGUAGES / languageLabel are usable without evaluating monaco-editor", async () => {
|
|
37
|
+
const { EDITOR_LANGUAGES, languageLabel } = await import("./index");
|
|
38
|
+
expect(Array.isArray(EDITOR_LANGUAGES)).toBe(true);
|
|
39
|
+
expect(EDITOR_LANGUAGES.length).toBeGreaterThan(0);
|
|
40
|
+
expect(languageLabel(EDITOR_LANGUAGES[0]!.id)).toBeTypeOf("string");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("EditorToolbar + EditorContextMenu are usable (rendered) without evaluating monaco-editor", async () => {
|
|
44
|
+
const { EditorToolbar, EditorContextMenu } = await import("./index");
|
|
45
|
+
// Both are `forwardRef`/context components — objects, not plain functions;
|
|
46
|
+
// assert they're valid React element types instead of a bare `typeof`.
|
|
47
|
+
expect(EditorToolbar).toBeTruthy();
|
|
48
|
+
expect(EditorContextMenu).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* color is never the only signal (var-def = weight, unresolved = dotted underline),
|
|
33
33
|
* so roles stay distinct in the high-contrast theme.
|
|
34
34
|
*/
|
|
35
|
+
import { useLocale } from "@elabs-ai/components-ui";
|
|
35
36
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
36
37
|
import { TriangleAlert } from "lucide-react";
|
|
37
38
|
import { forwardRef, useId, useMemo, type HTMLAttributes, type ReactNode } from "react";
|
|
@@ -239,6 +240,7 @@ function renderSource(text: string, tokens: CalcToken[]): ReactNode {
|
|
|
239
240
|
|
|
240
241
|
/** The right-hand cell: a value, a calm error marker, or nothing. */
|
|
241
242
|
function ResultCell({ result }: { result: CalcLineResult }): ReactNode {
|
|
243
|
+
const { t } = useLocale();
|
|
242
244
|
if (result.error) {
|
|
243
245
|
// Native `title` (not a Radix Tooltip) so CalcBlock is self-contained — it
|
|
244
246
|
// renders inside MarkdownPreview / a story with no TooltipProvider ancestor.
|
|
@@ -246,7 +248,7 @@ function ResultCell({ result }: { result: CalcLineResult }): ReactNode {
|
|
|
246
248
|
<span
|
|
247
249
|
className="inline-flex shrink-0 text-calc-warning"
|
|
248
250
|
title={result.error.message}
|
|
249
|
-
aria-label={
|
|
251
|
+
aria-label={t("editor.calcBlock.error", { message: result.error.message })}
|
|
250
252
|
>
|
|
251
253
|
<TriangleAlert className="size-3.5" aria-hidden="true" />
|
|
252
254
|
</span>
|
|
@@ -255,7 +257,7 @@ function ResultCell({ result }: { result: CalcLineResult }): ReactNode {
|
|
|
255
257
|
if (result.value) {
|
|
256
258
|
return (
|
|
257
259
|
<div className="brand-calc-tok--result shrink-0 tabular-nums text-calc-result">
|
|
258
|
-
<span className="sr-only">equals
|
|
260
|
+
<span className="sr-only">{t("editor.calcBlock.equals")}</span>
|
|
259
261
|
<span>{result.value.display}</span>
|
|
260
262
|
</div>
|
|
261
263
|
);
|
|
@@ -270,7 +272,7 @@ export const CalcBlock = forwardRef<HTMLDivElement, CalcBlockProps>(function Cal
|
|
|
270
272
|
title,
|
|
271
273
|
showTotal = true,
|
|
272
274
|
total,
|
|
273
|
-
totalLabel
|
|
275
|
+
totalLabel: totalLabelProp,
|
|
274
276
|
markers = true,
|
|
275
277
|
readOnly: _readOnly,
|
|
276
278
|
className,
|
|
@@ -278,6 +280,8 @@ export const CalcBlock = forwardRef<HTMLDivElement, CalcBlockProps>(function Cal
|
|
|
278
280
|
},
|
|
279
281
|
ref,
|
|
280
282
|
) {
|
|
283
|
+
const { t } = useLocale();
|
|
284
|
+
const totalLabel = totalLabelProp ?? t("editor.calcBlock.total");
|
|
281
285
|
// Author markers are stripped BEFORE evaluation: the math engine sees clean
|
|
282
286
|
// lines, and because markers are trailing, token columns stay aligned with the
|
|
283
287
|
// rendered (cleaned) text. `lines`/`evalSource` are the marker-free versions.
|
|
@@ -376,7 +380,9 @@ export const CalcBlock = forwardRef<HTMLDivElement, CalcBlockProps>(function Cal
|
|
|
376
380
|
</div>
|
|
377
381
|
|
|
378
382
|
{empty ? (
|
|
379
|
-
<p className="px-4 py-6 text-body text-muted-foreground">
|
|
383
|
+
<p className="px-4 py-6 text-body text-muted-foreground">
|
|
384
|
+
{t("editor.calcBlock.emptyBlock")}
|
|
385
|
+
</p>
|
|
380
386
|
) : (
|
|
381
387
|
<div className="flex flex-col gap-y-1 px-4 py-3 font-mono text-code leading-relaxed">
|
|
382
388
|
{rows}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Colors come from the `--calc-*` syntax tokens — the SAME tokens `CalcBlock`
|
|
7
7
|
* paints with (#221), already AA-verified against `--background` (the editor
|
|
8
|
-
* surface) and `--card` in
|
|
8
|
+
* surface) and `--card` in every theme — so highlighting tracks the theme via
|
|
9
9
|
* the cascade with no hardcoded hex. The compound `.brand-calc-tok.brand-calc-tok--x`
|
|
10
10
|
* selector raises specificity above Monaco's own token color class (`.mtkN`) so
|
|
11
11
|
* the calc color wins regardless of stylesheet load order (no `!important`).
|
|
@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
|
|
|
4
4
|
import * as monaco from "monaco-editor";
|
|
5
5
|
import { CodeEditor } from "./code-editor";
|
|
6
6
|
import { EditorToolbar } from "../editor-toolbar";
|
|
7
|
+
import { buildBrandThemeData } from "../lib/monaco-theme-bridge";
|
|
7
8
|
|
|
8
9
|
const meta = {
|
|
9
10
|
title: "Editor/CodeEditor",
|
|
@@ -14,9 +15,12 @@ const meta = {
|
|
|
14
15
|
docs: {
|
|
15
16
|
description: {
|
|
16
17
|
component:
|
|
18
|
+
"The real Monaco EDITING surface; a read-only highlighted block is `AI/CodeBlock` " +
|
|
19
|
+
"and a tool-output viewer is `AI/Sandbox` — see " +
|
|
20
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
17
21
|
"A token-themed Monaco editor. The editing surface + widgets are Monaco; " +
|
|
18
22
|
"brand-ui supplies the chrome (toolbar/Select/Button). Colors track the " +
|
|
19
|
-
"active `data-theme` across
|
|
23
|
+
"active `data-theme` across every theme via the theming bridge.",
|
|
20
24
|
},
|
|
21
25
|
},
|
|
22
26
|
},
|
|
@@ -76,6 +80,15 @@ export const WithToolbar: Story = {
|
|
|
76
80
|
},
|
|
77
81
|
};
|
|
78
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Locks #90: Monaco's `vs`/`vs-dark` base themes ship LANGUAGE-SUFFIXED JSON
|
|
85
|
+
* scopes (`string.key.json`, `string.value.json`, `keyword.json`) that beat the
|
|
86
|
+
* bridge's shorter, unsuffixed rules in Monaco's token-theme trie (deepest scope
|
|
87
|
+
* wins) — so JSON rendered in stock VS Code colors instead of brand colors. The
|
|
88
|
+
* play function reads REAL computed colors off the rendered `.mtk*` token spans
|
|
89
|
+
* and asserts none of them is a stock JSON color, in whichever theme this story
|
|
90
|
+
* is rendered under (`globals=theme:light|dark`).
|
|
91
|
+
*/
|
|
79
92
|
export const JsonReadOnly: Story = {
|
|
80
93
|
name: "JSON (read-only)",
|
|
81
94
|
render: () => (
|
|
@@ -83,6 +96,68 @@ export const JsonReadOnly: Story = {
|
|
|
83
96
|
<CodeEditor language="json" defaultValue={JSON_SAMPLE} readOnly />
|
|
84
97
|
</div>
|
|
85
98
|
),
|
|
99
|
+
play: async ({ canvasElement }) => {
|
|
100
|
+
// Monaco mounts asynchronously, and syntax tokenization is a SEPARATE,
|
|
101
|
+
// later pass: the editor first paints one un-highlighted `.mtk*` span per
|
|
102
|
+
// line, then re-renders with per-scope spans once the JSON monarch
|
|
103
|
+
// tokenizer finishes (observed to take ~1-3s in a headless test browser).
|
|
104
|
+
// Waiting for "any `.mtk*` span" resolves on that FIRST, un-highlighted
|
|
105
|
+
// paint — a false green that passes identically whether the fix is
|
|
106
|
+
// present or not (#90 fix-round-1 finding). Wait for the actual `"name"`
|
|
107
|
+
// key to have tokenized into its own span instead — that only exists once
|
|
108
|
+
// real per-scope highlighting has landed.
|
|
109
|
+
const surface = await within(canvasElement).findByTestId("code-editor", {}, { timeout: 10000 });
|
|
110
|
+
let spans: HTMLSpanElement[] = [];
|
|
111
|
+
await waitFor(
|
|
112
|
+
() => {
|
|
113
|
+
spans = Array.from(
|
|
114
|
+
surface.querySelectorAll<HTMLSpanElement>('.view-line span[class*="mtk"]'),
|
|
115
|
+
);
|
|
116
|
+
const keySpan = spans.some((span) => span.textContent?.trim() === '"name"');
|
|
117
|
+
if (!keySpan) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Monaco has not tokenized the "name" key into its own span yet (spans=${spans.length})`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{ timeout: 20000, interval: 100 },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Stock `vs`/`vs-dark` colors for string.key.json / string.value.json /
|
|
127
|
+
// keyword.json (extracted straight from monaco-editor's own base themes —
|
|
128
|
+
// see monaco-theme-bridge.test.ts's "Monaco real trie resolution" block for
|
|
129
|
+
// the same values as raw hex). If the bridge's brand rules ever stop beating
|
|
130
|
+
// the base theme's suffixed scopes, a token renders one of these again.
|
|
131
|
+
const stockColors = new Set([
|
|
132
|
+
"rgb(163, 21, 21)", // vs: string.key.json / string (maroon)
|
|
133
|
+
"rgb(4, 81, 165)", // vs: string.value.json / keyword.json (navy)
|
|
134
|
+
"rgb(156, 220, 254)", // vs-dark: string.key.json (cyan)
|
|
135
|
+
"rgb(206, 145, 120)", // vs-dark: string.value.json / keyword.json / string (orange)
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
const offenders = spans
|
|
139
|
+
.map((span) => getComputedStyle(span).color)
|
|
140
|
+
.filter((color) => stockColors.has(color));
|
|
141
|
+
await expect(offenders).toEqual([]);
|
|
142
|
+
|
|
143
|
+
// Positive check, not just an absence check: the "name" key span must be
|
|
144
|
+
// colored with the bridge's OWN live `string.key.json` brand ink (read
|
|
145
|
+
// straight off the currently-applied CSS custom properties), not merely
|
|
146
|
+
// "some color that isn't stock" — a fix that mapped the scope to the
|
|
147
|
+
// wrong token would pass the negative check above but fail this one.
|
|
148
|
+
const hexToRgb = (hex: string) => {
|
|
149
|
+
const n = Number.parseInt(hex.replace("#", ""), 16);
|
|
150
|
+
return `rgb(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255})`;
|
|
151
|
+
};
|
|
152
|
+
const liveRules = buildBrandThemeData().rules ?? [];
|
|
153
|
+
const expectedKeyForeground = liveRules.find(
|
|
154
|
+
(rule) => rule.token === "string.key.json",
|
|
155
|
+
)?.foreground;
|
|
156
|
+
await expect(expectedKeyForeground).toBeDefined();
|
|
157
|
+
const keySpan = spans.find((span) => span.textContent?.trim() === '"name"');
|
|
158
|
+
await expect(keySpan).toBeDefined();
|
|
159
|
+
await expect(getComputedStyle(keySpan!).color).toEqual(hexToRgb(expectedKeyForeground!));
|
|
160
|
+
},
|
|
86
161
|
};
|
|
87
162
|
|
|
88
163
|
/**
|