@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,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ContextMenu,
|
|
5
|
+
ContextMenuContent,
|
|
6
|
+
ContextMenuItem,
|
|
7
|
+
ContextMenuSeparator,
|
|
8
|
+
ContextMenuShortcut,
|
|
9
|
+
ContextMenuTrigger,
|
|
10
|
+
} from "@elabs-ai/components-ui";
|
|
11
|
+
import type * as Monaco from "monaco-editor";
|
|
12
|
+
import { type ReactNode } from "react";
|
|
13
|
+
|
|
14
|
+
export interface EditorContextMenuProps {
|
|
15
|
+
/** The Monaco editor instance to act on (null until mounted). */
|
|
16
|
+
editor: Monaco.editor.IStandaloneCodeEditor | null;
|
|
17
|
+
/** Hide editing items (cut/paste/format) for read-only editors. */
|
|
18
|
+
readOnly?: boolean;
|
|
19
|
+
/** The editor element wrapped as the right-click trigger. */
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Replaces Monaco's built-in context menu with brand-ui's `ContextMenu`, wired
|
|
25
|
+
* to the editor instance. This is the "reuse our components instead of Monaco's
|
|
26
|
+
* built-in ones" path for the right-click menu — set `contextMenu="brand"` on
|
|
27
|
+
* `CodeEditor` (the default) and it disables Monaco's menu and renders this.
|
|
28
|
+
*
|
|
29
|
+
* Clipboard ops use the async Clipboard API + `executeEdits` (reliable in the
|
|
30
|
+
* browser); other entries delegate to the editor's own actions.
|
|
31
|
+
*/
|
|
32
|
+
export function EditorContextMenu({ editor, readOnly = false, children }: EditorContextMenuProps) {
|
|
33
|
+
const selection = () => {
|
|
34
|
+
if (!editor) return null;
|
|
35
|
+
const model = editor.getModel();
|
|
36
|
+
const sel = editor.getSelection();
|
|
37
|
+
return model && sel ? { editor, model, sel } : null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const copy = async () => {
|
|
41
|
+
const ctx = selection();
|
|
42
|
+
if (!ctx) return;
|
|
43
|
+
const text = ctx.model.getValueInRange(ctx.sel);
|
|
44
|
+
if (text && typeof navigator !== "undefined" && navigator.clipboard) {
|
|
45
|
+
await navigator.clipboard.writeText(text);
|
|
46
|
+
}
|
|
47
|
+
ctx.editor.focus();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const cut = async () => {
|
|
51
|
+
const ctx = selection();
|
|
52
|
+
if (!ctx) return;
|
|
53
|
+
const text = ctx.model.getValueInRange(ctx.sel);
|
|
54
|
+
if (text && typeof navigator !== "undefined" && navigator.clipboard) {
|
|
55
|
+
await navigator.clipboard.writeText(text);
|
|
56
|
+
ctx.editor.executeEdits("brand-cut", [{ range: ctx.sel, text: "", forceMoveMarkers: true }]);
|
|
57
|
+
}
|
|
58
|
+
ctx.editor.focus();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const paste = async () => {
|
|
62
|
+
const ctx = selection();
|
|
63
|
+
if (!ctx || typeof navigator === "undefined" || !navigator.clipboard) return;
|
|
64
|
+
const text = await navigator.clipboard.readText();
|
|
65
|
+
ctx.editor.executeEdits("brand-paste", [{ range: ctx.sel, text, forceMoveMarkers: true }]);
|
|
66
|
+
ctx.editor.focus();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const selectAll = () => {
|
|
70
|
+
const ctx = selection();
|
|
71
|
+
if (!ctx) return;
|
|
72
|
+
ctx.editor.setSelection(ctx.model.getFullModelRange());
|
|
73
|
+
ctx.editor.focus();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const runAction = (id: string) => {
|
|
77
|
+
editor?.focus();
|
|
78
|
+
void editor?.getAction(id)?.run();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<ContextMenu>
|
|
83
|
+
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
|
84
|
+
<ContextMenuContent className="w-52">
|
|
85
|
+
{!readOnly ? (
|
|
86
|
+
<ContextMenuItem onSelect={() => void cut()}>
|
|
87
|
+
Cut
|
|
88
|
+
<ContextMenuShortcut>⌘X</ContextMenuShortcut>
|
|
89
|
+
</ContextMenuItem>
|
|
90
|
+
) : null}
|
|
91
|
+
<ContextMenuItem onSelect={() => void copy()}>
|
|
92
|
+
Copy
|
|
93
|
+
<ContextMenuShortcut>⌘C</ContextMenuShortcut>
|
|
94
|
+
</ContextMenuItem>
|
|
95
|
+
{!readOnly ? (
|
|
96
|
+
<ContextMenuItem onSelect={() => void paste()}>
|
|
97
|
+
Paste
|
|
98
|
+
<ContextMenuShortcut>⌘V</ContextMenuShortcut>
|
|
99
|
+
</ContextMenuItem>
|
|
100
|
+
) : null}
|
|
101
|
+
<ContextMenuSeparator />
|
|
102
|
+
<ContextMenuItem onSelect={selectAll}>
|
|
103
|
+
Select all
|
|
104
|
+
<ContextMenuShortcut>⌘A</ContextMenuShortcut>
|
|
105
|
+
</ContextMenuItem>
|
|
106
|
+
{!readOnly ? (
|
|
107
|
+
<ContextMenuItem onSelect={() => runAction("editor.action.formatDocument")}>
|
|
108
|
+
Format document
|
|
109
|
+
<ContextMenuShortcut>⇧⌥F</ContextMenuShortcut>
|
|
110
|
+
</ContextMenuItem>
|
|
111
|
+
) : null}
|
|
112
|
+
<ContextMenuSeparator />
|
|
113
|
+
<ContextMenuItem onSelect={() => runAction("editor.action.quickCommand")}>
|
|
114
|
+
Command palette
|
|
115
|
+
<ContextMenuShortcut>F1</ContextMenuShortcut>
|
|
116
|
+
</ContextMenuItem>
|
|
117
|
+
</ContextMenuContent>
|
|
118
|
+
</ContextMenu>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { EditorContextMenu, type EditorContextMenuProps } from "./editor-context-menu";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "@elabs-ai/components-ui";
|
|
10
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
11
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
12
|
+
import { CopyButton } from "../copy-button";
|
|
13
|
+
import { EDITOR_LANGUAGES, type EditorLanguage } from "../lib/languages";
|
|
14
|
+
|
|
15
|
+
export interface EditorToolbarProps extends HTMLAttributes<HTMLDivElement> {
|
|
16
|
+
/** Filename shown on the left (e.g. "index.ts"). */
|
|
17
|
+
filename?: ReactNode;
|
|
18
|
+
/** Active language id; shows a language picker when `onLanguageChange` is set. */
|
|
19
|
+
language?: string;
|
|
20
|
+
/** Called when the user picks a language. Omit to hide the picker. */
|
|
21
|
+
onLanguageChange?: (language: string) => void;
|
|
22
|
+
/** Languages offered in the picker. Defaults to {@link EDITOR_LANGUAGES}. */
|
|
23
|
+
languages?: EditorLanguage[];
|
|
24
|
+
/** Text copied by the copy button. Omit to hide the button. */
|
|
25
|
+
copyValue?: string;
|
|
26
|
+
/** Extra actions rendered before the copy button (brand-ui buttons, etc.). */
|
|
27
|
+
actions?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Brand-ui chrome for the editors — the part that genuinely reuses our
|
|
32
|
+
* components. Renders a filename label, an optional language `Select` and a copy
|
|
33
|
+
* `Button`. The editing surface itself is Monaco; this is the frame around it.
|
|
34
|
+
*/
|
|
35
|
+
export const EditorToolbar = forwardRef<HTMLDivElement, EditorToolbarProps>(function EditorToolbar(
|
|
36
|
+
{
|
|
37
|
+
filename,
|
|
38
|
+
language,
|
|
39
|
+
onLanguageChange,
|
|
40
|
+
languages = EDITOR_LANGUAGES,
|
|
41
|
+
copyValue,
|
|
42
|
+
actions,
|
|
43
|
+
className,
|
|
44
|
+
...props
|
|
45
|
+
},
|
|
46
|
+
ref,
|
|
47
|
+
) {
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
ref={ref}
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex h-10 shrink-0 items-center gap-2 border-b border-border bg-surface px-3",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{filename != null ? (
|
|
58
|
+
<span className="truncate text-sm font-medium text-foreground">{filename}</span>
|
|
59
|
+
) : null}
|
|
60
|
+
|
|
61
|
+
<div className="ml-auto flex items-center gap-1.5">
|
|
62
|
+
{actions}
|
|
63
|
+
|
|
64
|
+
{onLanguageChange ? (
|
|
65
|
+
<Select value={language} onValueChange={onLanguageChange}>
|
|
66
|
+
<SelectTrigger size="sm" className="h-7 w-[140px]" aria-label="Language">
|
|
67
|
+
<SelectValue placeholder="Language" />
|
|
68
|
+
</SelectTrigger>
|
|
69
|
+
<SelectContent>
|
|
70
|
+
{languages.map((l) => (
|
|
71
|
+
<SelectItem key={l.id} value={l.id}>
|
|
72
|
+
{l.label}
|
|
73
|
+
</SelectItem>
|
|
74
|
+
))}
|
|
75
|
+
</SelectContent>
|
|
76
|
+
</Select>
|
|
77
|
+
) : null}
|
|
78
|
+
|
|
79
|
+
{copyValue != null ? <CopyButton value={copyValue} /> : null}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { EditorToolbar, type EditorToolbarProps } from "./editor-toolbar";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @elabs-ai/components-editor — a token-themed Monaco (VS Code) editor wrapped as brand-ui
|
|
3
|
+
* components.
|
|
4
|
+
*
|
|
5
|
+
* Monaco is a self-rendering editor engine; this package wraps it the way
|
|
6
|
+
* @elabs-ai/components-flow wraps React Flow and @elabs-ai/components-data wraps TanStack. The editing
|
|
7
|
+
* surface + widgets are Monaco's, recolored from the active brand theme via the
|
|
8
|
+
* theming bridge; the chrome (toolbar, file tabs, copy button) is brand-ui.
|
|
9
|
+
*
|
|
10
|
+
* Worker setup (for completions/diagnostics) — import ONCE at the app entry:
|
|
11
|
+
* import "@elabs-ai/components-editor/monaco-environment"; // Vite apps
|
|
12
|
+
*/
|
|
13
|
+
export {
|
|
14
|
+
CodeEditor,
|
|
15
|
+
type CodeEditorProps,
|
|
16
|
+
type MonacoCodeEditor,
|
|
17
|
+
type EditorAction,
|
|
18
|
+
} from "./code-editor";
|
|
19
|
+
export { DiffEditor, type DiffEditorProps, type MonacoDiffEditor } from "./diff-editor";
|
|
20
|
+
export {
|
|
21
|
+
MarkdownEditor,
|
|
22
|
+
type MarkdownEditorProps,
|
|
23
|
+
type MarkdownEditorHandle,
|
|
24
|
+
type EmbedAssetFn,
|
|
25
|
+
} from "./markdown-editor";
|
|
26
|
+
export {
|
|
27
|
+
CodeWorkspace,
|
|
28
|
+
type CodeWorkspaceHandle,
|
|
29
|
+
type CodeWorkspaceProps,
|
|
30
|
+
type EditorFile,
|
|
31
|
+
} from "./code-workspace";
|
|
32
|
+
export {
|
|
33
|
+
monacoContentAccess,
|
|
34
|
+
type EditorContentAccess,
|
|
35
|
+
type EditorSelection,
|
|
36
|
+
} from "./lib/editor-content-access";
|
|
37
|
+
export { EditorToolbar, type EditorToolbarProps } from "./editor-toolbar";
|
|
38
|
+
export { EditorContextMenu, type EditorContextMenuProps } from "./editor-context-menu";
|
|
39
|
+
export { CopyButton, type CopyButtonProps } from "./copy-button";
|
|
40
|
+
|
|
41
|
+
// Theming bridge + language helpers (advanced consumers / custom chrome).
|
|
42
|
+
export { applyBrandTheme, buildBrandThemeData, brandThemeId } from "./lib/monaco-theme-bridge";
|
|
43
|
+
export { useDataTheme, type DataThemeState } from "./lib/use-data-theme";
|
|
44
|
+
export { EDITOR_LANGUAGES, languageLabel, type EditorLanguage } from "./lib/languages";
|
|
45
|
+
|
|
46
|
+
// Re-export the monaco namespace so consumers can build custom editors / wire
|
|
47
|
+
// commands without adding a direct dependency.
|
|
48
|
+
export * as monaco from "monaco-editor";
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the Monaco registration lifecycle (#283) —
|
|
3
|
+
* `attachCompletionsMonaco`'s refcount/dispose behavior + the per-model
|
|
4
|
+
* scoping that keeps suggestions out of markdown models `@elabs-ai/components-editor` never
|
|
5
|
+
* attached. Monaco can't render in jsdom, so `monaco-editor` is mocked (the
|
|
6
|
+
* `markdown-workspace.test.tsx` / `calc-editor-monaco.ts` precedent); the real
|
|
7
|
+
* suggest-widget UI is verified via `test-storybook`.
|
|
8
|
+
*
|
|
9
|
+
* These tests run as ONE ordered sequence (not `vi.resetModules()` per test) so
|
|
10
|
+
* they exercise the REAL module-singleton refcount across "two workspaces
|
|
11
|
+
* mounted" the same way the app would: attach, attach again, detach, detach —
|
|
12
|
+
* asserting `registerCompletionItemProvider` is called exactly once per live
|
|
13
|
+
* span and disposed only once the last consumer detaches.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it, vi } from "vitest";
|
|
16
|
+
|
|
17
|
+
const { registerCompletionItemProvider } = vi.hoisted(() => ({
|
|
18
|
+
registerCompletionItemProvider: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("monaco-editor", () => ({
|
|
22
|
+
languages: {
|
|
23
|
+
registerCompletionItemProvider,
|
|
24
|
+
CompletionItemKind: { Text: 18 },
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { attachCompletionsMonaco } from "./editor-completions-monaco";
|
|
29
|
+
import type { EditorCompletionProvider } from "./editor-completions";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Minimal Monaco editor mock — mirrors the shape used by
|
|
33
|
+
// markdown-workspace.test.tsx, extended with a mutable model + trigger().
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
function makeModel(id: string, value = "", lineText = "") {
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
getValue: vi.fn(() => value),
|
|
40
|
+
getLineContent: vi.fn(() => lineText),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeEditor(initialModel: ReturnType<typeof makeModel>) {
|
|
45
|
+
let model = initialModel;
|
|
46
|
+
const contentHandlers: Array<(e: { changes: { text: string }[] }) => void> = [];
|
|
47
|
+
const modelHandlers: Array<() => void> = [];
|
|
48
|
+
const disposeContent = vi.fn();
|
|
49
|
+
const disposeModel = vi.fn();
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
getModel: vi.fn(() => model),
|
|
53
|
+
setModel: (m: ReturnType<typeof makeModel>) => {
|
|
54
|
+
model = m;
|
|
55
|
+
},
|
|
56
|
+
onDidChangeModelContent: vi.fn((cb: (e: { changes: { text: string }[] }) => void) => {
|
|
57
|
+
contentHandlers.push(cb);
|
|
58
|
+
return { dispose: disposeContent };
|
|
59
|
+
}),
|
|
60
|
+
onDidChangeModel: vi.fn((cb: () => void) => {
|
|
61
|
+
modelHandlers.push(cb);
|
|
62
|
+
return { dispose: disposeModel };
|
|
63
|
+
}),
|
|
64
|
+
trigger: vi.fn(),
|
|
65
|
+
fireContentChange: (text: string) =>
|
|
66
|
+
contentHandlers.forEach((cb) => cb({ changes: [{ text }] })),
|
|
67
|
+
fireModelChange: () => modelHandlers.forEach((cb) => cb()),
|
|
68
|
+
disposeContent,
|
|
69
|
+
disposeModel,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const wikilinkProvider: EditorCompletionProvider = {
|
|
74
|
+
id: "wikilink",
|
|
75
|
+
triggerCharacters: ["["],
|
|
76
|
+
provide: () => [{ label: "note-one", insertText: "note-one]]" }],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// NOTE: no `beforeEach` mock reset here — this suite is intentionally ONE
|
|
80
|
+
// ordered sequence sharing `registerCompletionItemProvider`'s call history, to
|
|
81
|
+
// exercise the real module-singleton refcount across "two workspaces mounted"
|
|
82
|
+
// exactly like the app would (see file doc).
|
|
83
|
+
describe("attachCompletionsMonaco — refcount + dispose lifecycle", () => {
|
|
84
|
+
const registrationDispose = vi.fn();
|
|
85
|
+
registerCompletionItemProvider.mockReturnValue({ dispose: registrationDispose });
|
|
86
|
+
|
|
87
|
+
const modelA = makeModel("A", "[[no", "[[no");
|
|
88
|
+
const editorA = makeEditor(modelA);
|
|
89
|
+
const modelB = makeModel("B", "", "");
|
|
90
|
+
const editorB = makeEditor(modelB);
|
|
91
|
+
|
|
92
|
+
let detachA: () => void;
|
|
93
|
+
let detachB: () => void;
|
|
94
|
+
// Mutable so the "rebuilt provider list" test can swap it out from UNDER the
|
|
95
|
+
// already-attached editorA — exactly what a re-rendered `completions` prop
|
|
96
|
+
// (a fresh array identity) does, read live via this same getter.
|
|
97
|
+
let editorAProviders: EditorCompletionProvider[] = [wikilinkProvider];
|
|
98
|
+
|
|
99
|
+
it("registers the global provider exactly once for the first attach", () => {
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
detachA = attachCompletionsMonaco(editorA as any, () => editorAProviders);
|
|
102
|
+
expect(registerCompletionItemProvider).toHaveBeenCalledTimes(1);
|
|
103
|
+
expect(registerCompletionItemProvider).toHaveBeenCalledWith("markdown", expect.any(Object));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("a second attach (a second mounted workspace) does NOT re-register", () => {
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
detachB = attachCompletionsMonaco(editorB as any, () => undefined);
|
|
109
|
+
expect(registerCompletionItemProvider).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("scopes suggestions to models THIS module attached — modelA gets suggestions, an unattached model doesn't", async () => {
|
|
113
|
+
const provideCompletionItems = registerCompletionItemProvider.mock.calls[0]?.[1]
|
|
114
|
+
.provideCompletionItems as (
|
|
115
|
+
model: unknown,
|
|
116
|
+
position: unknown,
|
|
117
|
+
) => Promise<{ suggestions: unknown[] }>;
|
|
118
|
+
const position = { lineNumber: 1, column: 5 };
|
|
119
|
+
|
|
120
|
+
const resultA = await provideCompletionItems(modelA, position);
|
|
121
|
+
expect(resultA.suggestions).toHaveLength(1);
|
|
122
|
+
expect((resultA.suggestions[0] as { insertText: string }).insertText).toBe("note-one]]");
|
|
123
|
+
|
|
124
|
+
const unattached = makeModel("unattached", "[[no", "[[no");
|
|
125
|
+
const resultUnattached = await provideCompletionItems(unattached, position);
|
|
126
|
+
expect(resultUnattached.suggestions).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("reads a rebuilt provider list live — no re-registration needed", async () => {
|
|
130
|
+
const provideCompletionItems = registerCompletionItemProvider.mock.calls[0]?.[1]
|
|
131
|
+
.provideCompletionItems as (
|
|
132
|
+
model: unknown,
|
|
133
|
+
position: unknown,
|
|
134
|
+
) => Promise<{ suggestions: unknown[] }>;
|
|
135
|
+
const position = { lineNumber: 1, column: 5 };
|
|
136
|
+
|
|
137
|
+
const before = await provideCompletionItems(modelA, position);
|
|
138
|
+
expect(before.suggestions).toHaveLength(1);
|
|
139
|
+
|
|
140
|
+
// Rebuild the candidate list (a fresh array identity, like a re-rendered
|
|
141
|
+
// `completions` prop) WITHOUT calling attach again — the SAME attach from
|
|
142
|
+
// the first test reads this live through the `editorAProviders` closure.
|
|
143
|
+
editorAProviders = [
|
|
144
|
+
wikilinkProvider,
|
|
145
|
+
{ id: "second", triggerCharacters: ["["], provide: () => [{ label: "x", insertText: "x" }] },
|
|
146
|
+
];
|
|
147
|
+
const after = await provideCompletionItems(modelA, position);
|
|
148
|
+
expect(after.suggestions).toHaveLength(2);
|
|
149
|
+
expect(registerCompletionItemProvider).toHaveBeenCalledTimes(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("force-opens the suggest widget when a typed char matches a trigger character", () => {
|
|
153
|
+
editorA.fireContentChange("[");
|
|
154
|
+
expect(editorA.trigger).toHaveBeenCalledWith(
|
|
155
|
+
"brand-completions",
|
|
156
|
+
"editor.action.triggerSuggest",
|
|
157
|
+
{},
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("does NOT force-open the suggest widget for a non-trigger character", () => {
|
|
162
|
+
editorA.trigger.mockClear();
|
|
163
|
+
editorA.fireContentChange("x");
|
|
164
|
+
expect(editorA.trigger).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("moving to a new model re-scopes the registry (old model drops out)", async () => {
|
|
168
|
+
const provideCompletionItems = registerCompletionItemProvider.mock.calls[0]?.[1]
|
|
169
|
+
.provideCompletionItems as (
|
|
170
|
+
model: unknown,
|
|
171
|
+
position: unknown,
|
|
172
|
+
) => Promise<{ suggestions: unknown[] }>;
|
|
173
|
+
const position = { lineNumber: 1, column: 5 };
|
|
174
|
+
|
|
175
|
+
const modelA2 = makeModel("A2", "[[no", "[[no");
|
|
176
|
+
editorA.setModel(modelA2);
|
|
177
|
+
editorA.fireModelChange();
|
|
178
|
+
|
|
179
|
+
const resultOld = await provideCompletionItems(modelA, position);
|
|
180
|
+
expect(resultOld.suggestions).toHaveLength(0);
|
|
181
|
+
// 2 providers by this point (the previous test rebuilt `editorAProviders`
|
|
182
|
+
// to include a second one) — the exact count isn't the point, only that
|
|
183
|
+
// the NEW model is now scoped in.
|
|
184
|
+
const resultNew = await provideCompletionItems(modelA2, position);
|
|
185
|
+
expect(resultNew.suggestions.length).toBeGreaterThan(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("detaching a NON-last consumer does not dispose the global registration", () => {
|
|
189
|
+
detachB();
|
|
190
|
+
expect(registrationDispose).not.toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("detaching the LAST consumer disposes the global registration (no leak)", () => {
|
|
194
|
+
detachA();
|
|
195
|
+
expect(registrationDispose).toHaveBeenCalledTimes(1);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("a later attach re-registers fresh once refcount has dropped to zero", () => {
|
|
199
|
+
const modelC = makeModel("C", "", "");
|
|
200
|
+
const editorC = makeEditor(modelC);
|
|
201
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
202
|
+
const detachC = attachCompletionsMonaco(editorC as any, () => [wikilinkProvider]);
|
|
203
|
+
expect(registerCompletionItemProvider).toHaveBeenCalledTimes(2);
|
|
204
|
+
detachC();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Monaco registration lifecycle for the declarative completion-provider API
|
|
5
|
+
* (#283) — the engine that backs the `completions` prop on `MarkdownWorkspace`.
|
|
6
|
+
*
|
|
7
|
+
* `monaco.languages.registerCompletionItemProvider` is GLOBAL PER LANGUAGE, not
|
|
8
|
+
* per editor instance (the root cause #283 exists to fix — see the issue). So
|
|
9
|
+
* this module owns a single, REFCOUNTED registration for the "markdown"
|
|
10
|
+
* language: the first `attachCompletionsMonaco` call registers it, each
|
|
11
|
+
* subsequent call (a second mounted `MarkdownWorkspace`) bumps the refcount,
|
|
12
|
+
* and the registration is disposed ONLY when the LAST attached editor's
|
|
13
|
+
* disposer runs — no leak, no double-registration (#283 acceptance).
|
|
14
|
+
*
|
|
15
|
+
* Suggestions are scoped to the models THIS module attached (a `REGISTRY` keyed
|
|
16
|
+
* by `ITextModel`) — any other "markdown" model in the app (one the host built
|
|
17
|
+
* itself, outside `@elabs-ai/components-editor`) is ignored, never suggested into.
|
|
18
|
+
*
|
|
19
|
+
* `getProviders` is read fresh from the registry on every `provideCompletionItems`
|
|
20
|
+
* call — NOT captured as a closure at registration time — so a rebuilt provider
|
|
21
|
+
* list (a new `completions` array identity from React state/props) is picked up
|
|
22
|
+
* WITHOUT re-registering (#283 acceptance). Mirrors the calc layer's
|
|
23
|
+
* `REGISTRY`/`getHooks` pattern in `calc-block/calc-editor-monaco.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import * as monaco from "monaco-editor";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
collectCompletions,
|
|
29
|
+
resolveReplaceRange,
|
|
30
|
+
type EditorCompletionProvider,
|
|
31
|
+
} from "./editor-completions";
|
|
32
|
+
|
|
33
|
+
/** Resolver of the latest provider list for a model (read fresh on every call). */
|
|
34
|
+
type ProvidersGetter = () => EditorCompletionProvider[] | undefined;
|
|
35
|
+
|
|
36
|
+
/** Per-model provider registry — scopes suggestions to OUR editor instances. */
|
|
37
|
+
const REGISTRY = new Map<monaco.editor.ITextModel, ProvidersGetter>();
|
|
38
|
+
|
|
39
|
+
let refCount = 0;
|
|
40
|
+
let registration: monaco.IDisposable | null = null;
|
|
41
|
+
|
|
42
|
+
/** Register the "markdown" completion provider once (idempotent while refs > 0). */
|
|
43
|
+
function ensureRegistered(): void {
|
|
44
|
+
if (registration) return;
|
|
45
|
+
registration = monaco.languages.registerCompletionItemProvider("markdown", {
|
|
46
|
+
provideCompletionItems(model, position) {
|
|
47
|
+
const getProviders = REGISTRY.get(model);
|
|
48
|
+
if (!getProviders) return { suggestions: [] };
|
|
49
|
+
const providers = getProviders();
|
|
50
|
+
if (!providers || providers.length === 0) return { suggestions: [] };
|
|
51
|
+
const lineText = model.getLineContent(position.lineNumber);
|
|
52
|
+
const ctx = {
|
|
53
|
+
source: model.getValue(),
|
|
54
|
+
line: position.lineNumber,
|
|
55
|
+
column: position.column,
|
|
56
|
+
lineText,
|
|
57
|
+
};
|
|
58
|
+
return collectCompletions(providers, ctx).then((matches) => ({
|
|
59
|
+
suggestions: matches.map(({ provider, item }) => ({
|
|
60
|
+
label: item.label,
|
|
61
|
+
kind: monaco.languages.CompletionItemKind.Text,
|
|
62
|
+
insertText: item.insertText,
|
|
63
|
+
detail: item.detail,
|
|
64
|
+
range: resolveReplaceRange(item, position, lineText, provider.triggerCharacters),
|
|
65
|
+
})),
|
|
66
|
+
}));
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Attach the declarative completion providers to a Monaco editor instance.
|
|
73
|
+
* `getProviders` is read live on every suggestion request (see module doc) —
|
|
74
|
+
* pass a ref-backed getter so a fresh `completions` array identity on every
|
|
75
|
+
* render never forces a re-attach.
|
|
76
|
+
*
|
|
77
|
+
* Also force-opens Monaco's native suggest widget (`editor.action.triggerSuggest`)
|
|
78
|
+
* whenever the user types a character matching one of the CURRENTLY-configured
|
|
79
|
+
* providers' `triggerCharacters` — read live, so changing that set never needs a
|
|
80
|
+
* re-registration either. Markdown punctuation like `[` doesn't extend a "word",
|
|
81
|
+
* so without this Monaco's own quick-suggestions heuristic would never invoke a
|
|
82
|
+
* provider on it.
|
|
83
|
+
*
|
|
84
|
+
* Returns a disposer. Call it on unmount / when `completions` is removed —
|
|
85
|
+
* refcounted, so the GLOBAL "markdown" registration is only torn down once the
|
|
86
|
+
* LAST attached editor calls its disposer (#283 acceptance: no leak, no
|
|
87
|
+
* double-registration with two workspaces mounted).
|
|
88
|
+
*/
|
|
89
|
+
export function attachCompletionsMonaco(
|
|
90
|
+
editor: monaco.editor.IStandaloneCodeEditor,
|
|
91
|
+
getProviders: ProvidersGetter,
|
|
92
|
+
): () => void {
|
|
93
|
+
ensureRegistered();
|
|
94
|
+
refCount++;
|
|
95
|
+
|
|
96
|
+
let model = editor.getModel();
|
|
97
|
+
if (model) REGISTRY.set(model, getProviders);
|
|
98
|
+
|
|
99
|
+
const contentSub = editor.onDidChangeModelContent((e) => {
|
|
100
|
+
const providers = getProviders();
|
|
101
|
+
if (!providers || providers.length === 0) return;
|
|
102
|
+
for (const change of e.changes) {
|
|
103
|
+
if (change.text.length !== 1) continue;
|
|
104
|
+
if (providers.some((p) => p.triggerCharacters?.includes(change.text))) {
|
|
105
|
+
editor.trigger("brand-completions", "editor.action.triggerSuggest", {});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const modelSub = editor.onDidChangeModel(() => {
|
|
112
|
+
if (model) REGISTRY.delete(model);
|
|
113
|
+
model = editor.getModel();
|
|
114
|
+
if (model) REGISTRY.set(model, getProviders);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
contentSub.dispose();
|
|
119
|
+
modelSub.dispose();
|
|
120
|
+
if (model) REGISTRY.delete(model);
|
|
121
|
+
refCount = Math.max(0, refCount - 1);
|
|
122
|
+
if (refCount === 0) {
|
|
123
|
+
registration?.dispose();
|
|
124
|
+
registration = null;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|