@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,688 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent, waitFor, within } from "storybook/test";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import { Button, ToggleGroup, ToggleGroupItem } from "@elabs-ai/components-ui";
|
|
5
|
+
import type { EditorCompletionProvider } from "../lib/editor-completions";
|
|
6
|
+
import { BRAND_SLASH_COMMANDS, type SlashCommand } from "../markdown-editor/slash";
|
|
7
|
+
import { MarkdownWorkspace, type MarkdownWorkspaceHandle } from "./markdown-workspace";
|
|
8
|
+
|
|
9
|
+
const SAMPLE = `# Migration Plan
|
|
10
|
+
|
|
11
|
+
Author on the left, branded preview on the right.
|
|
12
|
+
|
|
13
|
+
:::callout{type="warning" title="Heads up"}
|
|
14
|
+
This block renders as an Alert in the preview.
|
|
15
|
+
:::
|
|
16
|
+
|
|
17
|
+
::metric{label="Scope" value="1,245" description="frozen report candidates"}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const meta = {
|
|
21
|
+
title: "Editor/MarkdownWorkspace",
|
|
22
|
+
component: MarkdownWorkspace,
|
|
23
|
+
tags: ["autodocs"],
|
|
24
|
+
parameters: {
|
|
25
|
+
layout: "fullscreen",
|
|
26
|
+
docs: {
|
|
27
|
+
description: {
|
|
28
|
+
component:
|
|
29
|
+
"The hybrid markdown surface: one value, three modes (Source = Monaco, Split = " +
|
|
30
|
+
"source + branded preview, Preview-edit = Milkdown WYSIWYG), switched via a " +
|
|
31
|
+
"@elabs-ai/components-ui ToggleGroup. The MarkdownToolbar is exercised in Source/Split modes.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
} satisfies Meta<typeof MarkdownWorkspace>;
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof meta>;
|
|
39
|
+
|
|
40
|
+
export const Split: Story = {
|
|
41
|
+
render: function SplitStory() {
|
|
42
|
+
const [value, setValue] = useState(SAMPLE);
|
|
43
|
+
return (
|
|
44
|
+
<div className="h-[520px] border-t border-border">
|
|
45
|
+
<MarkdownWorkspace value={value} onChange={setValue} defaultMode="split" />
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
play: async ({ canvasElement }) => {
|
|
50
|
+
const canvas = within(canvasElement);
|
|
51
|
+
// The mode switch (radiogroup) and formatting toolbar render immediately.
|
|
52
|
+
await expect(canvas.getByRole("radio", { name: "Split" })).toBeInTheDocument();
|
|
53
|
+
await expect(canvas.getByRole("toolbar", { name: "Markdown formatting" })).toBeInTheDocument();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const LONG = `# Focus writing
|
|
58
|
+
|
|
59
|
+
${Array.from({ length: 12 }, (_, i) => `Paragraph ${i + 1} — keep typing and the caret stays in a vertical band while the page moves underneath; every paragraph except the active one dims.`).join("\n\n")}
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Focus writing (Ulysses Phase A): typewriter scrolling + paragraph focus in
|
|
64
|
+
* the WYSIWYG mode. The "Focus" toggle in the mode row turns it on; the
|
|
65
|
+
* active paragraph stays at full opacity while the rest dim.
|
|
66
|
+
*/
|
|
67
|
+
export const FocusWriting: Story = {
|
|
68
|
+
render: function FocusWritingStory() {
|
|
69
|
+
const [value, setValue] = useState(LONG);
|
|
70
|
+
return (
|
|
71
|
+
<div className="h-[520px] border-t border-border">
|
|
72
|
+
<MarkdownWorkspace
|
|
73
|
+
value={value}
|
|
74
|
+
onChange={setValue}
|
|
75
|
+
defaultMode="wysiwyg"
|
|
76
|
+
defaultFocusWriting
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
play: async ({ canvasElement }) => {
|
|
82
|
+
const canvas = within(canvasElement);
|
|
83
|
+
const toggle = await canvas.findByRole("button", { name: "Focus writing" });
|
|
84
|
+
await expect(toggle).toHaveAttribute("aria-pressed", "true");
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `focusWriting={false}` — the host opts out of the Focus writing toggle. The
|
|
90
|
+
* toggle is completely hidden (no keyboard path), and focus-writing state is
|
|
91
|
+
* forced off. The mode switch remains and WYSIWYG editing is unaffected. (#270)
|
|
92
|
+
*/
|
|
93
|
+
export const NoFocusToggle: Story = {
|
|
94
|
+
tags: ["autodocs"],
|
|
95
|
+
render: function NoFocusToggleStory() {
|
|
96
|
+
const [value, setValue] = useState(LONG);
|
|
97
|
+
return (
|
|
98
|
+
<div className="h-[520px] border-t border-border">
|
|
99
|
+
<MarkdownWorkspace
|
|
100
|
+
value={value}
|
|
101
|
+
onChange={setValue}
|
|
102
|
+
defaultMode="wysiwyg"
|
|
103
|
+
focusWriting={false}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
play: async ({ canvasElement }) => {
|
|
109
|
+
const canvas = within(canvasElement);
|
|
110
|
+
// The Focus toggle must NOT be present.
|
|
111
|
+
await expect(canvas.queryByRole("button", { name: /focus writing/i })).toBeNull();
|
|
112
|
+
// The mode switch is still rendered.
|
|
113
|
+
await expect(canvas.getByRole("radio", { name: "Preview-edit" })).toBeInTheDocument();
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* `modeSwitch={false}` — the host supplies its own mode switch via
|
|
119
|
+
* `toolbarActions`. The built-in Source / Split / Preview-edit toggle group is
|
|
120
|
+
* hidden in both toolbar branches so the host owns the view switch. Controlled
|
|
121
|
+
* `mode`/`onModeChange` still drive the panes. (#272)
|
|
122
|
+
*/
|
|
123
|
+
export const HostOwnedModeSwitch: Story = {
|
|
124
|
+
tags: ["autodocs"],
|
|
125
|
+
render: function HostOwnedModeSwitchStory() {
|
|
126
|
+
const [value, setValue] = useState(SAMPLE);
|
|
127
|
+
const [activeMode, setActiveMode] = useState<"source" | "split">("split");
|
|
128
|
+
return (
|
|
129
|
+
<div className="h-[520px] border-t border-border">
|
|
130
|
+
<MarkdownWorkspace
|
|
131
|
+
value={value}
|
|
132
|
+
onChange={setValue}
|
|
133
|
+
mode={activeMode}
|
|
134
|
+
onModeChange={(m) => {
|
|
135
|
+
if (m === "source" || m === "split") setActiveMode(m);
|
|
136
|
+
}}
|
|
137
|
+
modeSwitch={false}
|
|
138
|
+
toolbarActions={
|
|
139
|
+
<ToggleGroup
|
|
140
|
+
type="single"
|
|
141
|
+
value={activeMode}
|
|
142
|
+
onValueChange={(m) => {
|
|
143
|
+
if (m === "source" || m === "split") setActiveMode(m);
|
|
144
|
+
}}
|
|
145
|
+
variant="segmented"
|
|
146
|
+
size="sm"
|
|
147
|
+
className="rounded-md p-0.5"
|
|
148
|
+
>
|
|
149
|
+
<ToggleGroupItem value="source" aria-label="Source" className="h-6 px-2 text-caption">
|
|
150
|
+
Source
|
|
151
|
+
</ToggleGroupItem>
|
|
152
|
+
<ToggleGroupItem value="split" aria-label="Split" className="h-6 px-2 text-caption">
|
|
153
|
+
Split
|
|
154
|
+
</ToggleGroupItem>
|
|
155
|
+
</ToggleGroup>
|
|
156
|
+
}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
play: async ({ canvasElement }) => {
|
|
162
|
+
const canvas = within(canvasElement);
|
|
163
|
+
// Built-in mode items must NOT be present.
|
|
164
|
+
await expect(canvas.queryByRole("radio", { name: "Preview-edit" })).toBeNull();
|
|
165
|
+
// The host-supplied controls render in the trailing slot.
|
|
166
|
+
await expect(canvas.getByRole("radio", { name: "Source" })).toBeInTheDocument();
|
|
167
|
+
await expect(canvas.getByRole("radio", { name: "Split" })).toBeInTheDocument();
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// A multi-screen document for exercising revealLine / scrollToHeading.
|
|
172
|
+
const MULTI_SCREEN = `# Introduction
|
|
173
|
+
|
|
174
|
+
${Array.from({ length: 8 }, (_, i) => `Paragraph ${i + 1} in the introduction section.`).join("\n\n")}
|
|
175
|
+
|
|
176
|
+
## Getting Started
|
|
177
|
+
|
|
178
|
+
${Array.from({ length: 8 }, (_, i) => `Paragraph ${i + 1} in getting started.`).join("\n\n")}
|
|
179
|
+
|
|
180
|
+
## Configuration
|
|
181
|
+
|
|
182
|
+
${Array.from({ length: 8 }, (_, i) => `Paragraph ${i + 1} in configuration.`).join("\n\n")}
|
|
183
|
+
|
|
184
|
+
## Advanced Usage
|
|
185
|
+
|
|
186
|
+
${Array.from({ length: 8 }, (_, i) => `Paragraph ${i + 1} in advanced usage.`).join("\n\n")}
|
|
187
|
+
|
|
188
|
+
## Troubleshooting
|
|
189
|
+
|
|
190
|
+
${Array.from({ length: 8 }, (_, i) => `Paragraph ${i + 1} in troubleshooting.`).join("\n\n")}
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* `MarkdownWorkspaceHandle` — imperative `revealLine` + `scrollToHeading` (#273).
|
|
195
|
+
*
|
|
196
|
+
* Use the buttons to call `ref.current.revealLine(N)` or
|
|
197
|
+
* `ref.current.scrollToHeading(slug)` programmatically. In Source/Split mode
|
|
198
|
+
* the Monaco editor scrolls to the exact line; in WYSIWYG (Preview-edit) mode
|
|
199
|
+
* the nearest heading scrolls into view (best-effort, heading-anchored).
|
|
200
|
+
*
|
|
201
|
+
* `getElement()` returns the root `<div>` (the old `HTMLDivElement` ref
|
|
202
|
+
* access); `getEditor()` returns the live Monaco instance (null in WYSIWYG).
|
|
203
|
+
*/
|
|
204
|
+
export const RevealLine: Story = {
|
|
205
|
+
tags: ["autodocs"],
|
|
206
|
+
render: function RevealLineStory() {
|
|
207
|
+
const ref = useRef<MarkdownWorkspaceHandle>(null);
|
|
208
|
+
const [value, setValue] = useState(MULTI_SCREEN);
|
|
209
|
+
const [log, setLog] = useState("");
|
|
210
|
+
|
|
211
|
+
const logAction = (msg: string) => setLog(msg);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div className="flex h-[600px] flex-col gap-2 border-t border-border p-2">
|
|
215
|
+
<div className="flex flex-wrap gap-2">
|
|
216
|
+
<Button
|
|
217
|
+
size="sm"
|
|
218
|
+
variant="outline"
|
|
219
|
+
onClick={() => {
|
|
220
|
+
ref.current?.revealLine(1);
|
|
221
|
+
logAction("revealLine(1) — Introduction heading");
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
Line 1 (Introduction)
|
|
225
|
+
</Button>
|
|
226
|
+
<Button
|
|
227
|
+
size="sm"
|
|
228
|
+
variant="outline"
|
|
229
|
+
onClick={() => {
|
|
230
|
+
// "## Getting Started" is around line 12 in the stripped body
|
|
231
|
+
ref.current?.scrollToHeading("getting-started");
|
|
232
|
+
logAction('scrollToHeading("getting-started")');
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
Heading: Getting Started
|
|
236
|
+
</Button>
|
|
237
|
+
<Button
|
|
238
|
+
size="sm"
|
|
239
|
+
variant="outline"
|
|
240
|
+
onClick={() => {
|
|
241
|
+
ref.current?.scrollToHeading("advanced-usage");
|
|
242
|
+
logAction('scrollToHeading("advanced-usage")');
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
Heading: Advanced Usage
|
|
246
|
+
</Button>
|
|
247
|
+
<Button
|
|
248
|
+
size="sm"
|
|
249
|
+
variant="outline"
|
|
250
|
+
onClick={() => {
|
|
251
|
+
ref.current?.scrollToHeading("troubleshooting");
|
|
252
|
+
logAction('scrollToHeading("troubleshooting")');
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
Heading: Troubleshooting
|
|
256
|
+
</Button>
|
|
257
|
+
<Button
|
|
258
|
+
size="sm"
|
|
259
|
+
variant="outline"
|
|
260
|
+
onClick={() => {
|
|
261
|
+
const el = ref.current?.getElement();
|
|
262
|
+
logAction(`getElement() → ${el ? el.tagName + "#" + el.dataset.testid : "null"}`);
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
getElement()
|
|
266
|
+
</Button>
|
|
267
|
+
<Button
|
|
268
|
+
size="sm"
|
|
269
|
+
variant="outline"
|
|
270
|
+
onClick={() => {
|
|
271
|
+
const ed = ref.current?.getEditor();
|
|
272
|
+
logAction(`getEditor() → ${ed ? "Monaco instance" : "null (WYSIWYG mode)"}`);
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
getEditor()
|
|
276
|
+
</Button>
|
|
277
|
+
</div>
|
|
278
|
+
{log && (
|
|
279
|
+
<p className="text-caption text-muted-foreground" data-testid="reveal-log">
|
|
280
|
+
{log}
|
|
281
|
+
</p>
|
|
282
|
+
)}
|
|
283
|
+
<div className="min-h-0 flex-1">
|
|
284
|
+
<MarkdownWorkspace ref={ref} value={value} onChange={setValue} defaultMode="source" />
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
},
|
|
289
|
+
play: async ({ canvasElement }) => {
|
|
290
|
+
const canvas = within(canvasElement);
|
|
291
|
+
// Buttons render immediately.
|
|
292
|
+
await expect(
|
|
293
|
+
canvas.getByRole("button", { name: "Heading: Getting Started" }),
|
|
294
|
+
).toBeInTheDocument();
|
|
295
|
+
// Click scrollToHeading — the log updates (Monaco scrolls in the real browser).
|
|
296
|
+
await userEvent.click(canvas.getByRole("button", { name: "Heading: Getting Started" }));
|
|
297
|
+
await expect(canvas.getByTestId("reveal-log")).toHaveTextContent("getting-started");
|
|
298
|
+
// getElement() resolves to the workspace root div.
|
|
299
|
+
await userEvent.click(canvas.getByRole("button", { name: "getElement()" }));
|
|
300
|
+
await expect(canvas.getByTestId("reveal-log")).toHaveTextContent("markdown-workspace");
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Source doc with a trailing blank line to type the slash on.
|
|
305
|
+
const SLASH_SAMPLE = `# Block menu in the source editor
|
|
306
|
+
|
|
307
|
+
Put the caret on the blank line below and type \`/\`, or use the button.
|
|
308
|
+
|
|
309
|
+
`;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* **Type \`/\` to open the block menu in the Monaco SOURCE pane** — the
|
|
313
|
+
* conflict-free trigger (no keybinding), mirroring the WYSIWYG \`/\` menu. A \`/\`
|
|
314
|
+
* at a line start (or after whitespace) opens the branded listbox at the caret;
|
|
315
|
+
* the \`/\` is replaced by the block you pick (Esc removes it, leaving the doc
|
|
316
|
+
* untouched). The story's play step inserts a \`/\` to open it on load — you can
|
|
317
|
+
* also just type \`/\` in the editor.
|
|
318
|
+
*/
|
|
319
|
+
export const SourceTypedSlash: Story = {
|
|
320
|
+
name: "Source: type / to insert a block",
|
|
321
|
+
tags: ["autodocs"],
|
|
322
|
+
render: function SourceTypedSlashStory() {
|
|
323
|
+
const ref = useRef<MarkdownWorkspaceHandle>(null);
|
|
324
|
+
const [value, setValue] = useState(SLASH_SAMPLE);
|
|
325
|
+
return (
|
|
326
|
+
<div className="flex h-[560px] flex-col gap-2 border-t border-border p-2">
|
|
327
|
+
<div className="flex items-center gap-3">
|
|
328
|
+
<Button
|
|
329
|
+
size="sm"
|
|
330
|
+
variant="outline"
|
|
331
|
+
onClick={() => {
|
|
332
|
+
const ed = ref.current?.getEditor();
|
|
333
|
+
const model = ed?.getModel();
|
|
334
|
+
if (!ed || !model) return; // no-op until Monaco has mounted
|
|
335
|
+
const line = model.getLineCount();
|
|
336
|
+
ed.focus();
|
|
337
|
+
ed.setPosition({ lineNumber: line, column: 1 });
|
|
338
|
+
ed.executeEdits("demo", [
|
|
339
|
+
{
|
|
340
|
+
range: {
|
|
341
|
+
startLineNumber: line,
|
|
342
|
+
startColumn: 1,
|
|
343
|
+
endLineNumber: line,
|
|
344
|
+
endColumn: 1,
|
|
345
|
+
},
|
|
346
|
+
text: "/",
|
|
347
|
+
},
|
|
348
|
+
]);
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
Type / (insert a slash)
|
|
352
|
+
</Button>
|
|
353
|
+
<p className="text-caption text-muted-foreground">
|
|
354
|
+
Or click into the editor and type / at the start of a line. Type to filter, ↑/↓ + Enter
|
|
355
|
+
to insert, Esc to dismiss.
|
|
356
|
+
</p>
|
|
357
|
+
</div>
|
|
358
|
+
<div className="min-h-0 flex-1">
|
|
359
|
+
<MarkdownWorkspace ref={ref} value={value} onChange={setValue} defaultMode="source" />
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
},
|
|
364
|
+
play: async ({ canvasElement }) => {
|
|
365
|
+
const canvas = within(canvasElement);
|
|
366
|
+
await waitFor(() => expect(canvasElement.querySelector(".monaco-editor")).toBeTruthy(), {
|
|
367
|
+
timeout: 8000,
|
|
368
|
+
});
|
|
369
|
+
const trigger = canvas.getByRole("button", { name: "Type / (insert a slash)" });
|
|
370
|
+
// Click until the menu opens. Before Monaco is ready the click is a no-op (no
|
|
371
|
+
// stray `/`); once open, the guard stops further clicks.
|
|
372
|
+
await waitFor(
|
|
373
|
+
async () => {
|
|
374
|
+
if (!canvas.queryByRole("listbox", { name: "Insert block" })) {
|
|
375
|
+
await userEvent.click(trigger);
|
|
376
|
+
}
|
|
377
|
+
expect(canvas.getByRole("listbox", { name: "Insert block" })).toBeInTheDocument();
|
|
378
|
+
},
|
|
379
|
+
{ timeout: 8000 },
|
|
380
|
+
);
|
|
381
|
+
await expect(
|
|
382
|
+
within(canvas.getByRole("listbox", { name: "Insert block" })).getByText("Card"),
|
|
383
|
+
).toBeInTheDocument();
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* **Hotkey** path (#271): \`slashMenu.shortcut\` opens the SAME menu. The default is
|
|
389
|
+
* **⌘⇧O** (free in Chrome/macOS) — NOT \`Mod-/\`, which Monaco binds to "Toggle Line
|
|
390
|
+
* Comment". ⚠️ ⌘⇧O / Ctrl-Shift-O is the bookmarks shortcut in Firefox and on
|
|
391
|
+
* Windows/Linux Chrome, so override \`slashMenu.shortcut\` (or rely on typing \`/\`)
|
|
392
|
+
* for cross-browser apps. The play fires the bound command to open it on load.
|
|
393
|
+
*/
|
|
394
|
+
export const SourceSlashHotkey: Story = {
|
|
395
|
+
name: "Source: ⌘⇧O hotkey",
|
|
396
|
+
tags: ["autodocs"],
|
|
397
|
+
render: function SourceSlashHotkeyStory() {
|
|
398
|
+
const ref = useRef<MarkdownWorkspaceHandle>(null);
|
|
399
|
+
const [value, setValue] = useState(SLASH_SAMPLE);
|
|
400
|
+
return (
|
|
401
|
+
<div className="flex h-[560px] flex-col gap-2 border-t border-border p-2">
|
|
402
|
+
<div className="flex items-center gap-3">
|
|
403
|
+
<Button
|
|
404
|
+
size="sm"
|
|
405
|
+
variant="outline"
|
|
406
|
+
onClick={() => {
|
|
407
|
+
const ed = ref.current?.getEditor();
|
|
408
|
+
ed?.focus();
|
|
409
|
+
ed?.getAction("brand.openSlashMenu")?.run();
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
Open block menu (⌘⇧O)
|
|
413
|
+
</Button>
|
|
414
|
+
<p className="text-caption text-muted-foreground">
|
|
415
|
+
Press ⌘⇧O with the editor focused (Chrome/macOS) or click the button. Override
|
|
416
|
+
slashMenu.shortcut for Firefox / Windows.
|
|
417
|
+
</p>
|
|
418
|
+
</div>
|
|
419
|
+
<div className="min-h-0 flex-1">
|
|
420
|
+
<MarkdownWorkspace ref={ref} value={value} onChange={setValue} defaultMode="source" />
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
},
|
|
425
|
+
play: async ({ canvasElement }) => {
|
|
426
|
+
const canvas = within(canvasElement);
|
|
427
|
+
await waitFor(() => expect(canvasElement.querySelector(".monaco-editor")).toBeTruthy(), {
|
|
428
|
+
timeout: 8000,
|
|
429
|
+
});
|
|
430
|
+
const trigger = canvas.getByRole("button", { name: "Open block menu (⌘⇧O)" });
|
|
431
|
+
await waitFor(
|
|
432
|
+
async () => {
|
|
433
|
+
if (!canvas.queryByRole("listbox", { name: "Insert block" })) {
|
|
434
|
+
await userEvent.click(trigger);
|
|
435
|
+
}
|
|
436
|
+
expect(canvas.getByRole("listbox", { name: "Insert block" })).toBeInTheDocument();
|
|
437
|
+
},
|
|
438
|
+
{ timeout: 8000 },
|
|
439
|
+
);
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
// #283 — declarative completion-provider API (`completions`)
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
const COMPLETIONS_SAMPLE = `# Completions: [[wikilink]] autocomplete (#283)
|
|
448
|
+
|
|
449
|
+
Click the button to type the [[ trigger at the end of this line, or place the
|
|
450
|
+
caret here yourself and type it — no monaco-editor import in this story.
|
|
451
|
+
|
|
452
|
+
`;
|
|
453
|
+
|
|
454
|
+
// A tiny in-memory "note index" the provider searches — entirely consumer-owned
|
|
455
|
+
// (candidates, filtering, and insertText are the app's job; the library only
|
|
456
|
+
// registers the provider and renders/inserts the result). Zero `monaco-editor`
|
|
457
|
+
// imports appear anywhere in this story.
|
|
458
|
+
const NOTE_TITLES = ["Onboarding guide", "Release notes", "Roadmap", "Testing notes"];
|
|
459
|
+
|
|
460
|
+
/** Pure matcher — a real consumer would use a shared, tested helper like this. */
|
|
461
|
+
function matchWikilinks(lineText: string, column: number): string[] {
|
|
462
|
+
const before = lineText.slice(0, column - 1);
|
|
463
|
+
const match = /\[\[([^[\]]*)$/.exec(before);
|
|
464
|
+
if (!match) return [];
|
|
465
|
+
const query = (match[1] ?? "").toLowerCase();
|
|
466
|
+
return NOTE_TITLES.filter((title) => title.toLowerCase().includes(query));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* **`completions` prop (#283)** — a declarative `[[wikilink]]` autocomplete
|
|
471
|
+
* provider, wired with ZERO `monaco-editor` imports in this story. The library
|
|
472
|
+
* owns the Monaco `registerCompletionItemProvider` registration (registered
|
|
473
|
+
* once, refcounted across mounted workspaces, disposed with the last one) and
|
|
474
|
+
* renders Monaco's OWN themed suggest widget — no custom popup on this path.
|
|
475
|
+
* Works in both Source and Split (the pane rendering Monaco either way).
|
|
476
|
+
*
|
|
477
|
+
* The play step verifies the part that is THIS library's responsibility end to
|
|
478
|
+
* end in a real browser: typing `[[` reaches the registered provider with the
|
|
479
|
+
* correct live context, which resolves the right candidates (logged below the
|
|
480
|
+
* editor). Whether Monaco's OWN native suggest popup paints itself in any given
|
|
481
|
+
* host is Monaco's own long-tested internal behavior, not new code — open this
|
|
482
|
+
* story in the Storybook UI to see it render live.
|
|
483
|
+
*/
|
|
484
|
+
export const CompletionsWikilink: Story = {
|
|
485
|
+
name: "Completions: [[wikilink]] autocomplete",
|
|
486
|
+
tags: ["autodocs"],
|
|
487
|
+
render: function CompletionsWikilinkStory() {
|
|
488
|
+
const ref = useRef<MarkdownWorkspaceHandle>(null);
|
|
489
|
+
const [value, setValue] = useState(COMPLETIONS_SAMPLE);
|
|
490
|
+
const [log, setLog] = useState("(not triggered yet)");
|
|
491
|
+
|
|
492
|
+
const provider: EditorCompletionProvider = {
|
|
493
|
+
id: "wikilink",
|
|
494
|
+
triggerCharacters: ["["],
|
|
495
|
+
provide: ({ lineText, column }) => {
|
|
496
|
+
const matches = matchWikilinks(lineText, column);
|
|
497
|
+
setLog(matches.length > 0 ? matches.join(", ") : "(no match at this caret)");
|
|
498
|
+
return matches.map((title) => ({ label: title, insertText: `${title}]]` }));
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const insertTrigger = async () => {
|
|
503
|
+
const ed = ref.current?.getEditor();
|
|
504
|
+
const model = ed?.getModel();
|
|
505
|
+
if (!ed || !model) return; // no-op until Monaco has mounted
|
|
506
|
+
const line = model.getLineCount();
|
|
507
|
+
ed.focus();
|
|
508
|
+
// Two SEPARATE single-character edits (not one 2-char insert), a tick
|
|
509
|
+
// apart — mirrors real typing: the library's force-open listener only
|
|
510
|
+
// recognizes a genuine single-char insert as "the trigger character was
|
|
511
|
+
// just typed", and a stray simultaneous pair doesn't give the FIRST
|
|
512
|
+
// (empty-result) request time to settle before the second.
|
|
513
|
+
for (const char of ["[", "["]) {
|
|
514
|
+
const column = model.getLineContent(line).length + 1;
|
|
515
|
+
ed.setPosition({ lineNumber: line, column });
|
|
516
|
+
ed.executeEdits("demo", [
|
|
517
|
+
{
|
|
518
|
+
range: {
|
|
519
|
+
startLineNumber: line,
|
|
520
|
+
startColumn: column,
|
|
521
|
+
endLineNumber: line,
|
|
522
|
+
endColumn: column,
|
|
523
|
+
},
|
|
524
|
+
text: char,
|
|
525
|
+
},
|
|
526
|
+
]);
|
|
527
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
return (
|
|
531
|
+
<div className="flex h-[560px] flex-col gap-2 border-t border-border p-2">
|
|
532
|
+
<div className="flex items-center gap-3">
|
|
533
|
+
<Button size="sm" variant="outline" onClick={insertTrigger}>
|
|
534
|
+
Type [[ (trigger suggestions)
|
|
535
|
+
</Button>
|
|
536
|
+
<p className="text-caption text-muted-foreground">
|
|
537
|
+
Or click into the editor and type [[ yourself. ↑/↓ + Enter to insert.
|
|
538
|
+
</p>
|
|
539
|
+
</div>
|
|
540
|
+
<div className="min-h-0 flex-1">
|
|
541
|
+
<MarkdownWorkspace
|
|
542
|
+
ref={ref}
|
|
543
|
+
value={value}
|
|
544
|
+
onChange={setValue}
|
|
545
|
+
defaultMode="split"
|
|
546
|
+
completions={[provider]}
|
|
547
|
+
/>
|
|
548
|
+
</div>
|
|
549
|
+
{/* Proves the end-to-end wiring (trigger detected → provider called with
|
|
550
|
+
the live caret context → candidates resolved) independent of
|
|
551
|
+
whether Monaco's own suggest popup happens to be visible when this
|
|
552
|
+
runs headless. */}
|
|
553
|
+
<p data-testid="wikilink-log" className="text-caption text-muted-foreground">
|
|
554
|
+
Candidates: {log}
|
|
555
|
+
</p>
|
|
556
|
+
</div>
|
|
557
|
+
);
|
|
558
|
+
},
|
|
559
|
+
play: async ({ canvasElement }) => {
|
|
560
|
+
const canvas = within(canvasElement);
|
|
561
|
+
await waitFor(() => expect(canvasElement.querySelector(".monaco-editor")).toBeTruthy(), {
|
|
562
|
+
timeout: 8000,
|
|
563
|
+
});
|
|
564
|
+
const trigger = canvas.getByRole("button", { name: "Type [[ (trigger suggestions)" });
|
|
565
|
+
await userEvent.click(trigger);
|
|
566
|
+
await waitFor(
|
|
567
|
+
() =>
|
|
568
|
+
expect(canvas.getByTestId("wikilink-log")).toHaveTextContent(
|
|
569
|
+
"Onboarding guide, Release notes, Roadmap, Testing notes",
|
|
570
|
+
),
|
|
571
|
+
{ timeout: 8000 },
|
|
572
|
+
);
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// ---------------------------------------------------------------------------
|
|
577
|
+
// #299 — run-only slash commands (runInSource) in the source pane
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
|
|
580
|
+
const RUN_ONLY_SAMPLE = `# Run-only slash commands (#299)
|
|
581
|
+
|
|
582
|
+
Put the caret on the blank line below and type \`/\`, then choose "Ask AI".
|
|
583
|
+
|
|
584
|
+
`;
|
|
585
|
+
|
|
586
|
+
const askAiCommand: SlashCommand = {
|
|
587
|
+
id: "ask-ai",
|
|
588
|
+
label: "Ask AI",
|
|
589
|
+
group: "Demo",
|
|
590
|
+
description: "No snippet — runs directly via runInSource",
|
|
591
|
+
keywords: ["ai", "ask"],
|
|
592
|
+
// WYSIWYG (Milkdown) — this story only exercises the source pane, but every
|
|
593
|
+
// SlashCommand still needs `run` (unchanged by #299).
|
|
594
|
+
run: () => {},
|
|
595
|
+
// Source pane (#299): strips the typed `/query`, then this fires with the
|
|
596
|
+
// live editor + the engine-agnostic content access.
|
|
597
|
+
runInSource: ({ content }) => {
|
|
598
|
+
content.insertAtCursor("_(AI answer would go here)_");
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* **`runInSource` (#299)** — a run-only command (no `snippet`) that used to be
|
|
604
|
+
* WYSIWYG-only now works in the Monaco SOURCE pane too. Selecting "Ask AI"
|
|
605
|
+
* strips the typed `/query`, then calls `runInSource` with the live editor +
|
|
606
|
+
* engine-agnostic content access — the 12 default (snippet-bearing) commands
|
|
607
|
+
* are unchanged and still insert their snippet.
|
|
608
|
+
*/
|
|
609
|
+
export const SourceRunOnlyCommand: Story = {
|
|
610
|
+
name: "Source: run-only command (runInSource)",
|
|
611
|
+
tags: ["autodocs"],
|
|
612
|
+
render: function SourceRunOnlyCommandStory() {
|
|
613
|
+
const ref = useRef<MarkdownWorkspaceHandle>(null);
|
|
614
|
+
const [value, setValue] = useState(RUN_ONLY_SAMPLE);
|
|
615
|
+
return (
|
|
616
|
+
<div className="flex h-[560px] flex-col gap-2 border-t border-border p-2">
|
|
617
|
+
<div className="flex items-center gap-3">
|
|
618
|
+
<Button
|
|
619
|
+
size="sm"
|
|
620
|
+
variant="outline"
|
|
621
|
+
onClick={() => {
|
|
622
|
+
const ed = ref.current?.getEditor();
|
|
623
|
+
const model = ed?.getModel();
|
|
624
|
+
if (!ed || !model) return; // no-op until Monaco has mounted
|
|
625
|
+
const line = model.getLineCount();
|
|
626
|
+
ed.focus();
|
|
627
|
+
ed.setPosition({ lineNumber: line, column: 1 });
|
|
628
|
+
ed.executeEdits("demo", [
|
|
629
|
+
{
|
|
630
|
+
range: {
|
|
631
|
+
startLineNumber: line,
|
|
632
|
+
startColumn: 1,
|
|
633
|
+
endLineNumber: line,
|
|
634
|
+
endColumn: 1,
|
|
635
|
+
},
|
|
636
|
+
text: "/",
|
|
637
|
+
},
|
|
638
|
+
]);
|
|
639
|
+
}}
|
|
640
|
+
>
|
|
641
|
+
Type / (insert a slash)
|
|
642
|
+
</Button>
|
|
643
|
+
<p className="text-caption text-muted-foreground">
|
|
644
|
+
"Ask AI" has no snippet — only runInSource — yet still appears + runs here.
|
|
645
|
+
</p>
|
|
646
|
+
</div>
|
|
647
|
+
<div className="min-h-0 flex-1">
|
|
648
|
+
<MarkdownWorkspace
|
|
649
|
+
ref={ref}
|
|
650
|
+
value={value}
|
|
651
|
+
onChange={setValue}
|
|
652
|
+
defaultMode="source"
|
|
653
|
+
slashMenu={{ commands: [askAiCommand, ...BRAND_SLASH_COMMANDS] }}
|
|
654
|
+
/>
|
|
655
|
+
</div>
|
|
656
|
+
{/* The document's live text, for a robust play-step assertion — scanning
|
|
657
|
+
Monaco's own rendered `.view-line` spans directly is fragile
|
|
658
|
+
(syntax-highlighting can split a run across sibling nodes). */}
|
|
659
|
+
<p data-testid="run-only-log" className="sr-only">
|
|
660
|
+
{value}
|
|
661
|
+
</p>
|
|
662
|
+
</div>
|
|
663
|
+
);
|
|
664
|
+
},
|
|
665
|
+
play: async ({ canvasElement }) => {
|
|
666
|
+
const canvas = within(canvasElement);
|
|
667
|
+
await waitFor(() => expect(canvasElement.querySelector(".monaco-editor")).toBeTruthy(), {
|
|
668
|
+
timeout: 8000,
|
|
669
|
+
});
|
|
670
|
+
const trigger = canvas.getByRole("button", { name: "Type / (insert a slash)" });
|
|
671
|
+
await waitFor(
|
|
672
|
+
async () => {
|
|
673
|
+
if (!canvas.queryByRole("listbox", { name: "Insert block" })) {
|
|
674
|
+
await userEvent.click(trigger);
|
|
675
|
+
}
|
|
676
|
+
expect(canvas.getByRole("listbox", { name: "Insert block" })).toBeInTheDocument();
|
|
677
|
+
},
|
|
678
|
+
{ timeout: 8000 },
|
|
679
|
+
);
|
|
680
|
+
const listbox = canvas.getByRole("listbox", { name: "Insert block" });
|
|
681
|
+
await expect(within(listbox).getByText("Ask AI")).toBeInTheDocument();
|
|
682
|
+
await userEvent.click(within(listbox).getByText("Ask AI"));
|
|
683
|
+
await waitFor(
|
|
684
|
+
() => expect(canvas.getByTestId("run-only-log")).toHaveTextContent("AI answer would go here"),
|
|
685
|
+
{ timeout: 8000 },
|
|
686
|
+
);
|
|
687
|
+
},
|
|
688
|
+
};
|