@elabs-ai/components-editor 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/chunk-LBC5VJBD.js +3748 -0
- package/dist/chunk-LBC5VJBD.js.map +1 -0
- package/dist/index.css +233 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +362 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/monaco-environment.d.ts +2 -0
- package/dist/lib/monaco-environment.js +44 -0
- package/dist/lib/monaco-environment.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +11 -0
- package/dist/markdown/frontmatter.js +33 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/index.css +233 -0
- package/dist/markdown/index.css.map +1 -0
- package/dist/markdown/index.d.ts +1486 -0
- package/dist/markdown/index.js +4882 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/markdown/parse.d.ts +25 -0
- package/dist/markdown/parse.js +14 -0
- package/dist/markdown/parse.js.map +1 -0
- package/dist/markdown-editor-DfBZibAn.d.ts +702 -0
- package/package.json +101 -0
- package/src/ai-objects/ai-objects-integration.test.tsx +128 -0
- package/src/ai-objects/ai-objects.stories.tsx +233 -0
- package/src/ai-objects/ai-objects.test.tsx +205 -0
- package/src/ai-objects/decision-card.tsx +178 -0
- package/src/ai-objects/directives.ts +136 -0
- package/src/ai-objects/entity.tsx +181 -0
- package/src/ai-objects/index.ts +34 -0
- package/src/ai-objects/knowledge-card.tsx +143 -0
- package/src/calc-block/calc-block.stories.tsx +297 -0
- package/src/calc-block/calc-block.test.tsx +164 -0
- package/src/calc-block/calc-block.tsx +398 -0
- package/src/calc-block/calc-editor-monaco.ts +214 -0
- package/src/calc-block/calc-editor-prose.ts +128 -0
- package/src/calc-block/calc-editor.css +80 -0
- package/src/calc-block/calc-editor.stories.tsx +272 -0
- package/src/calc-block/calc-editor.test.ts +228 -0
- package/src/calc-block/calc-editor.ts +270 -0
- package/src/calc-block/calc-inline.stories.tsx +44 -0
- package/src/calc-block/calc-inline.test.tsx +44 -0
- package/src/calc-block/calc-inline.tsx +75 -0
- package/src/calc-block/index.ts +20 -0
- package/src/calc-block/types.ts +164 -0
- package/src/code-editor/code-editor.stories.tsx +218 -0
- package/src/code-editor/code-editor.test.tsx +134 -0
- package/src/code-editor/code-editor.tsx +250 -0
- package/src/code-editor/index.ts +6 -0
- package/src/code-workspace/code-workspace.stories.tsx +51 -0
- package/src/code-workspace/code-workspace.test.tsx +146 -0
- package/src/code-workspace/code-workspace.tsx +253 -0
- package/src/code-workspace/index.ts +6 -0
- package/src/copy-button/copy-button.tsx +51 -0
- package/src/copy-button/index.ts +1 -0
- package/src/css.d.ts +3 -0
- package/src/diff-editor/diff-editor.stories.tsx +44 -0
- package/src/diff-editor/diff-editor.test.tsx +68 -0
- package/src/diff-editor/diff-editor.tsx +151 -0
- package/src/diff-editor/index.ts +1 -0
- package/src/editor-context-menu/editor-context-menu.tsx +120 -0
- package/src/editor-context-menu/index.ts +1 -0
- package/src/editor-toolbar/editor-toolbar.tsx +83 -0
- package/src/editor-toolbar/index.ts +1 -0
- package/src/index.ts +48 -0
- package/src/lib/editor-completions-monaco.test.ts +206 -0
- package/src/lib/editor-completions-monaco.ts +127 -0
- package/src/lib/editor-completions.test.ts +156 -0
- package/src/lib/editor-completions.ts +153 -0
- package/src/lib/editor-content-access-prose.test.ts +411 -0
- package/src/lib/editor-content-access-prose.ts +172 -0
- package/src/lib/editor-content-access.test.ts +182 -0
- package/src/lib/editor-content-access.ts +109 -0
- package/src/lib/languages.ts +32 -0
- package/src/lib/markdown/diff.test.ts +80 -0
- package/src/lib/markdown/diff.ts +194 -0
- package/src/lib/markdown/directives.ts +342 -0
- package/src/lib/markdown/frontmatter.ts +50 -0
- package/src/lib/markdown/markdown-scale.test.ts +65 -0
- package/src/lib/markdown/markdown-scale.ts +62 -0
- package/src/lib/markdown/merge.test.ts +64 -0
- package/src/lib/markdown/merge.ts +158 -0
- package/src/lib/markdown/slugify.test.ts +156 -0
- package/src/lib/markdown/slugify.ts +47 -0
- package/src/lib/monaco-environment.ts +103 -0
- package/src/lib/monaco-theme-bridge.ts +293 -0
- package/src/lib/use-data-theme.ts +61 -0
- package/src/markdown/frontmatter.ts +14 -0
- package/src/markdown/index.ts +323 -0
- package/src/markdown/parse.test.ts +60 -0
- package/src/markdown/parse.ts +38 -0
- package/src/markdown-academic/citations.test.ts +77 -0
- package/src/markdown-academic/citations.tsx +442 -0
- package/src/markdown-academic/footnotes.tsx +235 -0
- package/src/markdown-academic/index.ts +32 -0
- package/src/markdown-academic/math.tsx +163 -0
- package/src/markdown-academic/toc.tsx +88 -0
- package/src/markdown-editor/completions/completions-menu.tsx +96 -0
- package/src/markdown-editor/completions/completions-prose.test.ts +356 -0
- package/src/markdown-editor/completions/completions-prose.ts +313 -0
- package/src/markdown-editor/completions/completions-widget.tsx +75 -0
- package/src/markdown-editor/completions/index.ts +37 -0
- package/src/markdown-editor/directive-nodes.ts +151 -0
- package/src/markdown-editor/directive-views.test.tsx +189 -0
- package/src/markdown-editor/directive-views.tsx +795 -0
- package/src/markdown-editor/exit-keymap.test.ts +88 -0
- package/src/markdown-editor/exit-keymap.ts +87 -0
- package/src/markdown-editor/index.ts +6 -0
- package/src/markdown-editor/markdown-editor.css +250 -0
- package/src/markdown-editor/markdown-editor.directives.test.tsx +105 -0
- package/src/markdown-editor/markdown-editor.fill.test.ts +34 -0
- package/src/markdown-editor/markdown-editor.paste-embed.test.tsx +297 -0
- package/src/markdown-editor/markdown-editor.stories.tsx +560 -0
- package/src/markdown-editor/markdown-editor.strictmode.test.tsx +38 -0
- package/src/markdown-editor/markdown-editor.table.test.tsx +287 -0
- package/src/markdown-editor/markdown-editor.tsx +560 -0
- package/src/markdown-editor/milkdown-react/editor.tsx +33 -0
- package/src/markdown-editor/milkdown-react/index.ts +8 -0
- package/src/markdown-editor/milkdown-react/types.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-editor.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-get-editor.ts +56 -0
- package/src/markdown-editor/milkdown-react/use-instance.ts +23 -0
- package/src/markdown-editor/paste-embed.ts +355 -0
- package/src/markdown-editor/slash/brand-slash-commands.test.ts +194 -0
- package/src/markdown-editor/slash/brand-slash-commands.ts +324 -0
- package/src/markdown-editor/slash/brand-slash-plugin.test.ts +254 -0
- package/src/markdown-editor/slash/brand-slash-plugin.ts +385 -0
- package/src/markdown-editor/slash/index.ts +107 -0
- package/src/markdown-editor/slash/insert-directive.test.ts +263 -0
- package/src/markdown-editor/slash/insert-directive.ts +345 -0
- package/src/markdown-editor/slash/monaco-slash-menu.test.tsx +198 -0
- package/src/markdown-editor/slash/monaco-slash-menu.tsx +356 -0
- package/src/markdown-editor/slash/shortcut-monaco.ts +69 -0
- package/src/markdown-editor/slash/shortcut.test.ts +157 -0
- package/src/markdown-editor/slash/shortcut.ts +57 -0
- package/src/markdown-editor/slash/slash-menu.stories.tsx +139 -0
- package/src/markdown-editor/slash/slash-menu.tsx +115 -0
- package/src/markdown-editor/slash/slash-scroll.test.tsx +52 -0
- package/src/markdown-editor/slash/slash-widget.tsx +97 -0
- package/src/markdown-editor/slash/source-slash-trigger.test.ts +42 -0
- package/src/markdown-editor/slash/source-slash-trigger.ts +46 -0
- package/src/markdown-editor/table-view.tsx +249 -0
- package/src/markdown-iteration/directive.tsx +92 -0
- package/src/markdown-iteration/edit-context.ts +56 -0
- package/src/markdown-iteration/index.ts +24 -0
- package/src/markdown-iteration/iteration-block.test.tsx +138 -0
- package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +293 -0
- package/src/markdown-iteration/iteration-builder-dialog.tsx +312 -0
- package/src/markdown-iteration/iteration-builder.test.ts +252 -0
- package/src/markdown-iteration/iteration-builder.ts +303 -0
- package/src/markdown-iteration/iteration.test.ts +56 -0
- package/src/markdown-iteration/iteration.tsx +333 -0
- package/src/markdown-iteration/template-dialog.stories.tsx +63 -0
- package/src/markdown-iteration/template-dialog.test.tsx +104 -0
- package/src/markdown-iteration/template-dialog.tsx +116 -0
- package/src/markdown-outline/document-outline.stories.tsx +76 -0
- package/src/markdown-outline/document-outline.tsx +83 -0
- package/src/markdown-outline/index.ts +2 -0
- package/src/markdown-outline/markdown-outline.test.ts +49 -0
- package/src/markdown-outline/markdown-outline.ts +60 -0
- package/src/markdown-preview/code-fence.tsx +193 -0
- package/src/markdown-preview/index.ts +8 -0
- package/src/markdown-preview/markdown-preview-academic.stories.tsx +203 -0
- package/src/markdown-preview/markdown-preview-academic.test.tsx +257 -0
- package/src/markdown-preview/markdown-preview-iteration-calc.test.tsx +71 -0
- package/src/markdown-preview/markdown-preview-iteration-nested.test.tsx +58 -0
- package/src/markdown-preview/markdown-preview-iteration.stories.tsx +187 -0
- package/src/markdown-preview/markdown-preview-iteration.test.tsx +90 -0
- package/src/markdown-preview/markdown-preview-linking.test.tsx +61 -0
- package/src/markdown-preview/markdown-preview-transclusion.test.tsx +33 -0
- package/src/markdown-preview/markdown-preview.stories.tsx +108 -0
- package/src/markdown-preview/markdown-preview.test.tsx +411 -0
- package/src/markdown-preview/markdown-preview.tsx +1541 -0
- package/src/markdown-toolbar/index.ts +8 -0
- package/src/markdown-toolbar/markdown-commands.test.ts +84 -0
- package/src/markdown-toolbar/markdown-commands.ts +104 -0
- package/src/markdown-toolbar/markdown-toolbar.stories.tsx +56 -0
- package/src/markdown-toolbar/markdown-toolbar.tsx +263 -0
- package/src/markdown-workspace/focus-writing.test.ts +52 -0
- package/src/markdown-workspace/focus-writing.ts +38 -0
- package/src/markdown-workspace/index.ts +6 -0
- package/src/markdown-workspace/markdown-workspace.stories.tsx +688 -0
- package/src/markdown-workspace/markdown-workspace.test.tsx +326 -0
- package/src/markdown-workspace/markdown-workspace.tsx +787 -0
- package/src/mermaid-diagram/index.ts +1 -0
- package/src/mermaid-diagram/mermaid-diagram.stories.tsx +42 -0
- package/src/mermaid-diagram/mermaid-diagram.test.tsx +86 -0
- package/src/mermaid-diagram/mermaid-diagram.tsx +318 -0
- package/src/mermaid-diagram/mermaid-viewer.test.tsx +89 -0
- package/src/mermaid-diagram/mermaid-viewer.tsx +367 -0
- package/src/mermaid-diagram/real-parse.test.ts +42 -0
- package/src/mermaid-diagram/remediate.test.ts +60 -0
- package/src/mermaid-diagram/remediate.ts +124 -0
- package/src/mermaid-workspace/index.ts +1 -0
- package/src/mermaid-workspace/mermaid-workspace.stories.tsx +28 -0
- package/src/mermaid-workspace/mermaid-workspace.tsx +71 -0
- package/src/metric-block/index.ts +1 -0
- package/src/metric-block/metric-block.stories.tsx +40 -0
- package/src/metric-block/metric-block.tsx +14 -0
- package/src/monaco-workers.d.ts +10 -0
- package/src/prose/index.ts +14 -0
- package/src/prose/prose.stories.tsx +44 -0
- package/src/prose/prose.test.tsx +44 -0
- package/src/prose/prose.tsx +21 -0
- package/src/timeline/index.ts +18 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CalcBlock contracts — the shape the consumer's `evaluate` hook returns.
|
|
3
|
+
*
|
|
4
|
+
* The library RENDERS; the consumer EVALUATES. The math engine is app/domain
|
|
5
|
+
* logic and must NOT live in the design system — so these are pure types and the
|
|
6
|
+
* library never bundles a calculator (mirrors how `resolveUrl` keeps URL
|
|
7
|
+
* resolution in the consumer). A consumer produces a `CalcSheet` from a ```calc
|
|
8
|
+
* source; CalcBlock paints it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type CalcValueKind =
|
|
12
|
+
| "number"
|
|
13
|
+
| "quantity"
|
|
14
|
+
| "currency"
|
|
15
|
+
| "date"
|
|
16
|
+
| "duration"
|
|
17
|
+
| "percent"
|
|
18
|
+
| "text"
|
|
19
|
+
| "error";
|
|
20
|
+
|
|
21
|
+
/** A typed result value; `display` is the locale-formatted, tabular-ready string. */
|
|
22
|
+
export interface CalcValue {
|
|
23
|
+
kind: CalcValueKind;
|
|
24
|
+
raw: number | string;
|
|
25
|
+
unit?: string;
|
|
26
|
+
display: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type CalcTokenKind =
|
|
30
|
+
| "number"
|
|
31
|
+
| "unit"
|
|
32
|
+
| "currency"
|
|
33
|
+
| "operator"
|
|
34
|
+
| "function"
|
|
35
|
+
| "constant"
|
|
36
|
+
| "comment"
|
|
37
|
+
| "var-def"
|
|
38
|
+
| "var-ref"
|
|
39
|
+
| "line-ref"
|
|
40
|
+
| "unknown";
|
|
41
|
+
|
|
42
|
+
/** One highlighted span within a source line — 0-based columns, `end` exclusive. */
|
|
43
|
+
export interface CalcToken {
|
|
44
|
+
start: number;
|
|
45
|
+
end: number;
|
|
46
|
+
kind: CalcTokenKind;
|
|
47
|
+
/** `false` → a non-fatal warning style (unknown identifier / dangling ref). */
|
|
48
|
+
resolved: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Presentation hint: a rule (horizontal divider) drawn below a row — a `"dotted"`
|
|
53
|
+
* hairline, a `"single"` solid line, or a `"double"` line (e.g. above a subtotal).
|
|
54
|
+
* Pure presentation: the library renders it; the evaluator decides which rows get one.
|
|
55
|
+
*/
|
|
56
|
+
export type CalcRule = "dotted" | "single" | "double";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Presentation hint: tint a row's background with a semantic status wash
|
|
60
|
+
* (theme-safe `bg-<token>/10`, matching the house status vocabulary). `"muted"`
|
|
61
|
+
* is a neutral, non-status tint. The evaluator decides; the library paints.
|
|
62
|
+
*/
|
|
63
|
+
export type CalcTint = "primary" | "success" | "warning" | "destructive" | "info" | "muted";
|
|
64
|
+
|
|
65
|
+
/** One evaluated source line: its highlight tokens, and a value OR an error. */
|
|
66
|
+
export interface CalcLineResult {
|
|
67
|
+
/** 1-based line within the calc source. */
|
|
68
|
+
line: number;
|
|
69
|
+
tokens: CalcToken[];
|
|
70
|
+
value?: CalcValue;
|
|
71
|
+
/** A per-line error rendered inline; never thrown. */
|
|
72
|
+
error?: { message: string };
|
|
73
|
+
/** Presentation hint: draw a rule (divider) below this row. */
|
|
74
|
+
rule?: CalcRule;
|
|
75
|
+
/** Presentation hint: tint this row's background with a semantic wash. */
|
|
76
|
+
tint?: CalcTint;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CalcSheet {
|
|
80
|
+
results: CalcLineResult[];
|
|
81
|
+
/** Running total for the block. */
|
|
82
|
+
total?: CalcValue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface CalcContext {
|
|
86
|
+
docPath?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The consumer-supplied evaluator (sync, like `resolveUrl`). */
|
|
90
|
+
export type EvaluateCalc = (source: string, ctx?: CalcContext) => CalcSheet;
|
|
91
|
+
|
|
92
|
+
/* ------------------------------------------------------------------------- *
|
|
93
|
+
* Authoring hooks (#220) — the EDITOR side of calc.
|
|
94
|
+
*
|
|
95
|
+
* The render side (`CalcBlock`/`CalcInline`) takes `evaluate`; the editor side
|
|
96
|
+
* adds live highlighting + autocomplete + result inlays inside ```calc fences,
|
|
97
|
+
* in both the Monaco source surface and the Milkdown WYSIWYG surface. Same
|
|
98
|
+
* governing rule: the library DECORATES, the consumer COMPUTES — these are pure
|
|
99
|
+
* function types and the package bundles no calc engine (mirrors `resolveUrl`).
|
|
100
|
+
* ------------------------------------------------------------------------- */
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Tokenize ONE calc source line into highlight spans (the columns the library
|
|
104
|
+
* paints). Called per line, so the returned {@link CalcToken} `start`/`end`
|
|
105
|
+
* columns are relative to `line` — the same shape `evaluate` already returns on
|
|
106
|
+
* each `CalcLineResult.tokens`. Sync, like `resolveUrl`.
|
|
107
|
+
*/
|
|
108
|
+
export type CalcTokenize = (line: string, ctx?: CalcContext) => CalcToken[];
|
|
109
|
+
|
|
110
|
+
/** A completion category — drives the icon (Monaco) / glyph + label (WYSIWYG). */
|
|
111
|
+
export type CalcCompletionKind =
|
|
112
|
+
| "variable"
|
|
113
|
+
| "function"
|
|
114
|
+
| "unit"
|
|
115
|
+
| "currency"
|
|
116
|
+
| "constant"
|
|
117
|
+
| "reference"
|
|
118
|
+
| "keyword"
|
|
119
|
+
| "snippet";
|
|
120
|
+
|
|
121
|
+
/** One autocomplete suggestion offered inside a ```calc fence. */
|
|
122
|
+
export interface CalcCompletion {
|
|
123
|
+
/** Text shown in the completion list. */
|
|
124
|
+
label: string;
|
|
125
|
+
/** Text inserted when the item is chosen (replaces the typed `prefix`). */
|
|
126
|
+
insert: string;
|
|
127
|
+
/** Secondary detail shown muted beside the label (e.g. a type or preview). */
|
|
128
|
+
detail?: string;
|
|
129
|
+
/** Category; selects the suggestion icon. Defaults to `"variable"`. */
|
|
130
|
+
kind?: CalcCompletionKind;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** What the editor knows at the caret when it asks the consumer for completions. */
|
|
134
|
+
export interface CalcCompletionContext extends CalcContext {
|
|
135
|
+
/** The full calc fence body (all lines). */
|
|
136
|
+
source: string;
|
|
137
|
+
/** The current line's text (the line the caret is on). */
|
|
138
|
+
line: string;
|
|
139
|
+
/** 1-based line of the caret within the fence body. */
|
|
140
|
+
lineNumber: number;
|
|
141
|
+
/** 0-based caret column within `line`. */
|
|
142
|
+
column: number;
|
|
143
|
+
/** The identifier characters immediately before the caret (the word typed). */
|
|
144
|
+
prefix: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Suggest completions at the caret inside a ```calc fence (sync). */
|
|
148
|
+
export type CalcComplete = (ctx: CalcCompletionContext) => CalcCompletion[];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The opt-in authoring hooks threaded onto `MarkdownEditor` / `MarkdownWorkspace`
|
|
152
|
+
* via the `calc` prop (off by default). Each capability is independent:
|
|
153
|
+
* - `tokenize` → syntax highlighting (falls back to `evaluate`'s per-line
|
|
154
|
+
* `tokens` when omitted, so a consumer who already wrote `evaluate` gets
|
|
155
|
+
* highlighting for free).
|
|
156
|
+
* - `evaluate` → per-line result inlays (the computed answer, shown after the
|
|
157
|
+
* line). Reuses the existing {@link EvaluateCalc} contract.
|
|
158
|
+
* - `complete` → autocomplete inside the fence (Monaco surface).
|
|
159
|
+
*/
|
|
160
|
+
export interface CalcEditorHooks {
|
|
161
|
+
tokenize?: CalcTokenize;
|
|
162
|
+
complete?: CalcComplete;
|
|
163
|
+
evaluate?: EvaluateCalc;
|
|
164
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, fireEvent, waitFor, within } from "storybook/test";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import * as monaco from "monaco-editor";
|
|
5
|
+
import { CodeEditor } from "./code-editor";
|
|
6
|
+
import { EditorToolbar } from "../editor-toolbar";
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: "Editor/CodeEditor",
|
|
10
|
+
component: CodeEditor,
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: "fullscreen",
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
"A token-themed Monaco editor. The editing surface + widgets are Monaco; " +
|
|
18
|
+
"brand-ui supplies the chrome (toolbar/Select/Button). Colors track the " +
|
|
19
|
+
"active `data-theme` across all three themes via the theming bridge.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
} satisfies Meta<typeof CodeEditor>;
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof meta>;
|
|
27
|
+
|
|
28
|
+
const TS_SAMPLE = `import { useState } from "react";
|
|
29
|
+
|
|
30
|
+
interface CounterProps {
|
|
31
|
+
/** Starting value. */
|
|
32
|
+
start?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Counter({ start = 0 }: CounterProps) {
|
|
36
|
+
const [count, setCount] = useState(start);
|
|
37
|
+
// Increment on click.
|
|
38
|
+
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const JSON_SAMPLE = `{
|
|
43
|
+
"name": "@elabs-ai/components-editor",
|
|
44
|
+
"version": "0.1.0",
|
|
45
|
+
"private": true,
|
|
46
|
+
"keywords": ["monaco", "editor", "tokens"],
|
|
47
|
+
"engines": { "node": ">=20" }
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
export const Default: Story = {
|
|
52
|
+
render: () => (
|
|
53
|
+
<div className="h-[440px] w-full border-border [&]:border">
|
|
54
|
+
<CodeEditor language="typescript" defaultValue={TS_SAMPLE} />
|
|
55
|
+
</div>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const WithToolbar: Story = {
|
|
60
|
+
render: function WithToolbarStory() {
|
|
61
|
+
const [language, setLanguage] = useState("typescript");
|
|
62
|
+
const [value, setValue] = useState(TS_SAMPLE);
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex h-[440px] w-full flex-col overflow-hidden rounded-lg border border-border">
|
|
65
|
+
<EditorToolbar
|
|
66
|
+
filename="counter.tsx"
|
|
67
|
+
language={language}
|
|
68
|
+
onLanguageChange={setLanguage}
|
|
69
|
+
copyValue={value}
|
|
70
|
+
/>
|
|
71
|
+
<div className="min-h-0 flex-1">
|
|
72
|
+
<CodeEditor language={language} value={value} onChange={setValue} />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const JsonReadOnly: Story = {
|
|
80
|
+
name: "JSON (read-only)",
|
|
81
|
+
render: () => (
|
|
82
|
+
<div className="h-[300px] w-full border border-border">
|
|
83
|
+
<CodeEditor language="json" defaultValue={JSON_SAMPLE} readOnly />
|
|
84
|
+
</div>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The minimap (right-edge "mini navigation view") enabled + themed, and the
|
|
90
|
+
* brand-ui context menu (right-click) that replaces Monaco's built-in one. The
|
|
91
|
+
* play test right-clicks the editor and asserts the brand menu items render.
|
|
92
|
+
*/
|
|
93
|
+
export const ContextMenuAndMinimap: Story = {
|
|
94
|
+
name: "Context menu + minimap",
|
|
95
|
+
parameters: {
|
|
96
|
+
docs: {
|
|
97
|
+
description: {
|
|
98
|
+
story:
|
|
99
|
+
"Right-click the editor: the menu is brand-ui's `ContextMenu` (not Monaco's), " +
|
|
100
|
+
"wired to the editor's actions. The minimap on the right edge is enabled via " +
|
|
101
|
+
"`options` and themed from tokens.",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
render: () => (
|
|
106
|
+
<div className="h-[440px] w-full border border-border">
|
|
107
|
+
<CodeEditor
|
|
108
|
+
language="typescript"
|
|
109
|
+
defaultValue={TS_SAMPLE}
|
|
110
|
+
contextMenu="brand"
|
|
111
|
+
options={{ minimap: { enabled: true } }}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
play: async ({ canvasElement }) => {
|
|
116
|
+
// Monaco mounts asynchronously — wait for the editor surface, then right-click.
|
|
117
|
+
const surface = await within(canvasElement).findByTestId("code-editor", {}, { timeout: 10000 });
|
|
118
|
+
fireEvent.contextMenu(surface);
|
|
119
|
+
const menu = within(document.body);
|
|
120
|
+
// `findBy*` retries only until the node EXISTS — but ContextMenuContent carries
|
|
121
|
+
// `data-[state=open]:fade-in-0`, so it starts at opacity 0 and a one-shot
|
|
122
|
+
// `toBeVisible()` races the entrance animation (#51, #278 pattern D). waitFor
|
|
123
|
+
// rides it out — the dropdown-menu.stories.tsx precedent.
|
|
124
|
+
const copy = await menu.findByRole("menuitem", { name: /copy/i });
|
|
125
|
+
await waitFor(() => expect(copy).toBeVisible());
|
|
126
|
+
const palette = await menu.findByRole("menuitem", { name: /command palette/i });
|
|
127
|
+
await waitFor(() => expect(palette).toBeVisible());
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Accessibility: the editor forwards `ariaLabel` / `ariaInvalid` / `ariaDescribedBy`
|
|
133
|
+
* onto Monaco's inner screen-reader `<textarea>` (its real focusable surface) — NOT
|
|
134
|
+
* the wrapper div. This is the locking test for #34, asserted against REAL Monaco in
|
|
135
|
+
* a browser (a jsdom mock that swaps in a `<textarea>` cannot prove the real surface).
|
|
136
|
+
*/
|
|
137
|
+
export const Accessibility: Story = {
|
|
138
|
+
name: "Accessibility (aria forwarding)",
|
|
139
|
+
render: () => (
|
|
140
|
+
<div className="h-[300px] w-full border border-border">
|
|
141
|
+
<CodeEditor
|
|
142
|
+
language="typescript"
|
|
143
|
+
defaultValue={TS_SAMPLE}
|
|
144
|
+
ariaLabel="Source code editor"
|
|
145
|
+
ariaInvalid
|
|
146
|
+
ariaDescribedBy="editor-help"
|
|
147
|
+
/>
|
|
148
|
+
<span id="editor-help" className="sr-only">
|
|
149
|
+
Edit the source code.
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
),
|
|
153
|
+
play: async ({ canvasElement }) => {
|
|
154
|
+
// Wait for Monaco to mount, then assert its inner screen-reader <textarea> (the
|
|
155
|
+
// real focusable surface) carries the aria-* — proving they reached Monaco, not
|
|
156
|
+
// just the wrapper div. We query the element directly (Monaco also stamps
|
|
157
|
+
// role="textbox" on a wrapper div, so a role query is ambiguous).
|
|
158
|
+
const surface = await within(canvasElement).findByTestId("code-editor", {}, { timeout: 10000 });
|
|
159
|
+
let textarea: HTMLTextAreaElement | null = null;
|
|
160
|
+
await waitFor(
|
|
161
|
+
() => {
|
|
162
|
+
textarea = surface.querySelector("textarea");
|
|
163
|
+
if (!textarea) throw new Error("Monaco textarea not mounted yet");
|
|
164
|
+
},
|
|
165
|
+
{ timeout: 10000 },
|
|
166
|
+
);
|
|
167
|
+
await expect(textarea!).toHaveAttribute("aria-label", "Source code editor");
|
|
168
|
+
await expect(textarea!).toHaveAttribute("aria-invalid", "true");
|
|
169
|
+
await expect(textarea!).toHaveAttribute("aria-describedby", "editor-help");
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Demonstrates the `actions` prop: a declarative array of Monaco editor actions
|
|
175
|
+
* that register commands in the keybinding system and the command palette. Press
|
|
176
|
+
* `Ctrl+K` / `Cmd+K` (or open the command palette and search "Say Hi") to
|
|
177
|
+
* trigger the action. The action is memoized to avoid re-registering on every
|
|
178
|
+
* render.
|
|
179
|
+
*/
|
|
180
|
+
export const WithActions: Story = {
|
|
181
|
+
name: "Declarative actions",
|
|
182
|
+
tags: ["autodocs"],
|
|
183
|
+
render: function WithActionsStory() {
|
|
184
|
+
const [feedback, setFeedback] = useState<string | null>(null);
|
|
185
|
+
const actions = useMemo(
|
|
186
|
+
() => [
|
|
187
|
+
{
|
|
188
|
+
id: "demo.sayHi",
|
|
189
|
+
label: "Say Hi",
|
|
190
|
+
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK],
|
|
191
|
+
contextMenuGroupId: "navigation",
|
|
192
|
+
contextMenuOrder: 1,
|
|
193
|
+
run: () => {
|
|
194
|
+
setFeedback("Action fired! (Ctrl+K / Cmd+K)");
|
|
195
|
+
setTimeout(() => setFeedback(null), 2500);
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
[],
|
|
200
|
+
);
|
|
201
|
+
return (
|
|
202
|
+
<div className="flex h-[380px] w-full flex-col gap-2">
|
|
203
|
+
<div className="flex-1 rounded-md border border-border overflow-hidden">
|
|
204
|
+
<CodeEditor
|
|
205
|
+
language="typescript"
|
|
206
|
+
defaultValue={TS_SAMPLE}
|
|
207
|
+
actions={actions}
|
|
208
|
+
contextMenu="monaco"
|
|
209
|
+
ariaLabel="Actions demo editor"
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
<p className="text-caption text-muted-foreground">
|
|
213
|
+
{feedback ?? "Press Ctrl+K / Cmd+K — or open the command palette and search “Say Hi”."}
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { act, cleanup, render } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
// Monaco needs real layout/canvas, so it cannot mount in jsdom — mock the engine
|
|
5
|
+
// and assert the wrapper's lifecycle contract. Real rendering + a11y are covered
|
|
6
|
+
// by the Storybook interaction tests.
|
|
7
|
+
const h = vi.hoisted(() => {
|
|
8
|
+
const state = { changeHandler: undefined as undefined | (() => void), value: "" };
|
|
9
|
+
const model = { dispose: vi.fn() };
|
|
10
|
+
const actionDisposable = { dispose: vi.fn() };
|
|
11
|
+
const editor = {
|
|
12
|
+
onDidChangeModelContent: vi.fn((cb: () => void) => {
|
|
13
|
+
state.changeHandler = cb;
|
|
14
|
+
return { dispose: vi.fn() };
|
|
15
|
+
}),
|
|
16
|
+
getValue: vi.fn(() => state.value),
|
|
17
|
+
setValue: vi.fn((v: string) => {
|
|
18
|
+
state.value = v;
|
|
19
|
+
}),
|
|
20
|
+
getModel: vi.fn(() => model),
|
|
21
|
+
updateOptions: vi.fn(),
|
|
22
|
+
addAction: vi.fn(() => actionDisposable),
|
|
23
|
+
// Minimal DOM node so the aria-forwarding effect can find (or not) a textarea.
|
|
24
|
+
getDomNode: vi.fn(() => null as HTMLElement | null),
|
|
25
|
+
dispose: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
state,
|
|
29
|
+
model,
|
|
30
|
+
actionDisposable,
|
|
31
|
+
editor,
|
|
32
|
+
create: vi.fn(() => editor),
|
|
33
|
+
createModel: vi.fn((value: string) => {
|
|
34
|
+
state.value = value;
|
|
35
|
+
return model;
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
vi.mock("monaco-editor", () => ({
|
|
41
|
+
editor: {
|
|
42
|
+
create: h.create,
|
|
43
|
+
createModel: h.createModel,
|
|
44
|
+
setModelLanguage: vi.fn(),
|
|
45
|
+
defineTheme: vi.fn(),
|
|
46
|
+
setTheme: vi.fn(),
|
|
47
|
+
},
|
|
48
|
+
Uri: { parse: (s: string) => ({ toString: () => s }) },
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
import { CodeEditor } from "./code-editor";
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
h.state.changeHandler = undefined;
|
|
55
|
+
h.state.value = "";
|
|
56
|
+
vi.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
afterEach(cleanup);
|
|
59
|
+
|
|
60
|
+
describe("CodeEditor", () => {
|
|
61
|
+
it("creates a Monaco editor with the initial value + language", () => {
|
|
62
|
+
const { getByTestId } = render(
|
|
63
|
+
<CodeEditor defaultValue="const a = 1;" language="typescript" />,
|
|
64
|
+
);
|
|
65
|
+
expect(getByTestId("code-editor")).toBeInTheDocument();
|
|
66
|
+
expect(h.create).toHaveBeenCalledTimes(1);
|
|
67
|
+
// No `path` → no model URI.
|
|
68
|
+
expect(h.createModel).toHaveBeenCalledWith("const a = 1;", "typescript", undefined);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("creates the model with a URI derived from `path`", () => {
|
|
72
|
+
render(<CodeEditor defaultValue="x" language="json" path="config/app.json" />);
|
|
73
|
+
expect(h.createModel).toHaveBeenCalledWith("x", "json", {
|
|
74
|
+
toString: expect.any(Function),
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("forwards content edits to onChange", () => {
|
|
79
|
+
const onChange = vi.fn();
|
|
80
|
+
render(<CodeEditor defaultValue="" onChange={onChange} />);
|
|
81
|
+
expect(h.state.changeHandler).toBeTypeOf("function");
|
|
82
|
+
act(() => {
|
|
83
|
+
h.state.value = "typed";
|
|
84
|
+
h.state.changeHandler?.();
|
|
85
|
+
});
|
|
86
|
+
expect(onChange).toHaveBeenCalledWith("typed");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("disposes the editor + model on unmount", () => {
|
|
90
|
+
const { unmount } = render(<CodeEditor defaultValue="x" />);
|
|
91
|
+
unmount();
|
|
92
|
+
expect(h.editor.dispose).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(h.model.dispose).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("actions prop", () => {
|
|
97
|
+
it("calls addAction once per descriptor on mount", () => {
|
|
98
|
+
const actions = [
|
|
99
|
+
{ id: "test.one", label: "One", run: vi.fn() },
|
|
100
|
+
{ id: "test.two", label: "Two", run: vi.fn() },
|
|
101
|
+
];
|
|
102
|
+
render(<CodeEditor defaultValue="" actions={actions} />);
|
|
103
|
+
expect(h.editor.addAction).toHaveBeenCalledTimes(2);
|
|
104
|
+
expect(h.editor.addAction).toHaveBeenCalledWith(actions[0]);
|
|
105
|
+
expect(h.editor.addAction).toHaveBeenCalledWith(actions[1]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("disposes old disposables and re-adds when the actions array identity changes", () => {
|
|
109
|
+
const actionsV1 = [{ id: "test.a", label: "A", run: vi.fn() }];
|
|
110
|
+
const actionsV2 = [
|
|
111
|
+
{ id: "test.a", label: "A", run: vi.fn() },
|
|
112
|
+
{ id: "test.b", label: "B", run: vi.fn() },
|
|
113
|
+
];
|
|
114
|
+
const { rerender } = render(<CodeEditor defaultValue="" actions={actionsV1} />);
|
|
115
|
+
expect(h.editor.addAction).toHaveBeenCalledTimes(1);
|
|
116
|
+
|
|
117
|
+
rerender(<CodeEditor defaultValue="" actions={actionsV2} />);
|
|
118
|
+
// Old disposable disposed, then re-added for both new actions.
|
|
119
|
+
expect(h.actionDisposable.dispose).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(h.editor.addAction).toHaveBeenCalledTimes(3); // 1 + 2
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("disposes all action disposables on unmount", () => {
|
|
124
|
+
const actions = [
|
|
125
|
+
{ id: "test.x", label: "X", run: vi.fn() },
|
|
126
|
+
{ id: "test.y", label: "Y", run: vi.fn() },
|
|
127
|
+
];
|
|
128
|
+
const { unmount } = render(<CodeEditor defaultValue="" actions={actions} />);
|
|
129
|
+
expect(h.editor.addAction).toHaveBeenCalledTimes(2);
|
|
130
|
+
unmount();
|
|
131
|
+
expect(h.actionDisposable.dispose).toHaveBeenCalledTimes(2);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|