@elabs-ai/components-editor 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/dist/{chunk-LBC5VJBD.js → chunk-C62O7IOQ.js} +110 -21
- package/dist/chunk-C62O7IOQ.js.map +1 -0
- package/dist/index.css +4 -2
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +57 -20
- package/dist/index.js.map +1 -1
- package/dist/markdown/index.css +4 -2
- package/dist/markdown/index.css.map +1 -1
- package/dist/markdown/index.d.ts +2 -2
- package/dist/markdown/index.js +9 -9
- package/dist/markdown/index.js.map +1 -1
- package/dist/{markdown-editor-DfBZibAn.d.ts → markdown-editor-CBp_4eDv.d.ts} +1 -1
- package/package.json +6 -6
- package/src/ai-objects/entity.tsx +1 -1
- package/src/ai-objects/knowledge-card.tsx +1 -1
- package/src/calc-block/calc-editor.css +1 -1
- package/src/code-editor/code-editor.stories.tsx +76 -1
- package/src/code-editor/code-editor.tsx +1 -1
- package/src/code-workspace/code-workspace.test.tsx +83 -0
- package/src/code-workspace/code-workspace.tsx +73 -20
- package/src/diff-editor/diff-editor.stories.tsx +9 -1
- package/src/lib/monaco-deep-imports.d.ts +59 -0
- package/src/lib/monaco-theme-bridge.test.ts +307 -0
- package/src/lib/monaco-theme-bridge.ts +154 -10
- package/src/markdown-academic/citations.tsx +2 -2
- package/src/markdown-academic/footnotes.tsx +2 -2
- package/src/markdown-academic/toc.tsx +1 -1
- package/src/markdown-editor/directive-views.tsx +2 -5
- package/src/markdown-editor/markdown-editor.css +26 -3
- package/src/markdown-editor/markdown-editor.focus.test.ts +49 -0
- package/src/markdown-editor/markdown-editor.stories.tsx +128 -7
- package/src/markdown-editor/markdown-editor.tsx +8 -5
- package/src/markdown-editor/milkdown-react/use-get-editor.timer-leak.test.ts +80 -0
- package/src/markdown-editor/milkdown-react/use-get-editor.ts +37 -1
- package/src/markdown-editor/paste-embed.ts +2 -1
- package/src/markdown-editor/slash/slash-menu.stories.tsx +8 -3
- package/src/markdown-editor/slash/slash-menu.test.tsx +27 -0
- package/src/markdown-editor/slash/slash-menu.tsx +13 -1
- package/src/markdown-editor/table-view.tsx +3 -2
- package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +57 -19
- package/src/markdown-outline/document-outline.stories.tsx +1 -1
- package/src/markdown-outline/document-outline.tsx +1 -1
- package/src/markdown-preview/markdown-preview-transclusion.test.tsx +1 -1
- package/src/markdown-preview/markdown-preview.stories.tsx +24 -2
- package/src/markdown-preview/markdown-preview.test.tsx +20 -3
- package/src/markdown-toolbar/markdown-toolbar.stories.tsx +3 -0
- package/src/markdown-workspace/markdown-workspace.test.tsx +31 -1
- package/src/mermaid-diagram/mermaid-viewer.tsx +1 -1
- package/src/prose/prose.stories.tsx +3 -0
- package/src/prose/prose.test.ts +54 -0
- package/src/prose/prose.tsx +7 -0
- package/dist/chunk-LBC5VJBD.js.map +0 -1
|
@@ -55,7 +55,7 @@ export const DocumentOutline = forwardRef<HTMLElement, DocumentOutlineProps>(
|
|
|
55
55
|
className={cn(
|
|
56
56
|
"-ms-px block w-full truncate border-s-2 py-1 pe-2 text-start text-caption",
|
|
57
57
|
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
58
|
-
"focus-
|
|
58
|
+
"focus-ring-inset",
|
|
59
59
|
item.id === activeId
|
|
60
60
|
? "border-s-primary font-medium text-foreground"
|
|
61
61
|
: "border-s-transparent text-muted-foreground hover:border-s-border-strong hover:text-foreground",
|
|
@@ -11,7 +11,7 @@ afterEach(cleanup);
|
|
|
11
11
|
* nested mount is perturbed by prior Streamdown renders (an env-only gremlin — the
|
|
12
12
|
* feature renders correctly in the browser and in isolation). So transclusion gets
|
|
13
13
|
* its OWN file with the resolved (nested-render) case asserted FIRST in a clean
|
|
14
|
-
* module context.
|
|
14
|
+
* module context. Cross-theme pixel sweep still owed.
|
|
15
15
|
*/
|
|
16
16
|
describe("MarkdownPreview — transclusion (resolveTransclusion)", () => {
|
|
17
17
|
it("embeds resolved markdown in a labelled figure", async () => {
|
|
@@ -12,11 +12,33 @@ const meta = {
|
|
|
12
12
|
docs: {
|
|
13
13
|
description: {
|
|
14
14
|
component:
|
|
15
|
+
"The EDITOR preview pane, which understands the brand `:::` directives. " +
|
|
16
|
+
"Pick a markdown renderer by where the markdown is going to be READ: a " +
|
|
17
|
+
"read-only document in a chat or a side rail → `AI/MarkdownView`; the preview " +
|
|
18
|
+
"pane of the markdown editor → `Editor/MarkdownPreview`; a file the app did " +
|
|
19
|
+
"not write → the markdown adapter behind `Viewer/FileViewer`; streaming into " +
|
|
20
|
+
"a message as the model writes it → `MessageResponse` on `AI/Message`. See " +
|
|
21
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs)." +
|
|
22
|
+
"\n\n" +
|
|
15
23
|
"Renders markdown to REAL @brand components (Streamdown + a branded components " +
|
|
16
24
|
"map): `#` → Heading, link → Link, table → Table, `---` → Separator, and the brand " +
|
|
17
25
|
"directives `:::card` / `:::callout` / `::metric` / `:::timeline` → Card / Alert / " +
|
|
18
26
|
"MetricBlock / Timeline. Uses the SAME shared remark pipeline as the Milkdown editor. " +
|
|
19
|
-
"Unknown directives render an explicit error block instead of disappearing
|
|
27
|
+
"Unknown directives render an explicit error block instead of disappearing.\n\n" +
|
|
28
|
+
"Its map is NOT a superset or a subset of `MarkdownView`'s — it is a different " +
|
|
29
|
+
"job. Every standard element here is wrapped to attach `data-sourcepos` (the " +
|
|
30
|
+
"seam the editor scroll-sync, the ghost-diff wash and the search wash all read), " +
|
|
31
|
+
"and a heading additionally carries a TOC slug id, a heading-action slot and " +
|
|
32
|
+
"hover-reveal anchor chrome. It has no `baseHeadingLevel`; it does have " +
|
|
33
|
+
"wikilinks, transclusion, `resolveUrl` and mermaid fences.\n\n" +
|
|
34
|
+
"All three map their element tree onto the same `Prose*` primitives owned " +
|
|
35
|
+
"by `@elabs-ai/components-ui` (`@elabs-ai/components-editor` re-exports them " +
|
|
36
|
+
"under short names). The element MAPS stay per-surface on purpose — " +
|
|
37
|
+
"`@elabs-ai/components-ai`, `@elabs-ai/components-editor` and " +
|
|
38
|
+
"`@elabs-ai/components-viewer` are leaves that may not import one another, and " +
|
|
39
|
+
"`streamdown` deliberately never moves down into `@elabs-ai/components-ui`, or " +
|
|
40
|
+
"every consumer of every foundation component would carry it. One prose source, " +
|
|
41
|
+
"several renderers.",
|
|
20
42
|
},
|
|
21
43
|
},
|
|
22
44
|
},
|
|
@@ -88,7 +110,7 @@ export const HeadingActions: Story = {
|
|
|
88
110
|
type="button"
|
|
89
111
|
aria-label={`Pin section “${h.text}”`}
|
|
90
112
|
aria-pressed={h.text === "Timeline"}
|
|
91
|
-
className="rounded-sm p-0.5 text-muted-foreground hover:text-foreground focus-
|
|
113
|
+
className="rounded-sm p-0.5 text-muted-foreground hover:text-foreground focus-ring"
|
|
92
114
|
>
|
|
93
115
|
<Pin className="size-3.5" aria-hidden="true" />
|
|
94
116
|
</button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { MarkdownPreview } from "./markdown-preview";
|
|
5
5
|
|
|
@@ -249,16 +249,33 @@ describe("MarkdownPreview — code fences (highlight + chip + copy)", () => {
|
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
describe("MarkdownPreview — mermaid fences (#L1)", () => {
|
|
252
|
+
// #64 — under a full, parallel, uncached monorepo run this describe block's
|
|
253
|
+
// FIRST `import("mermaid")` (fired inside MermaidDiagram's effect,
|
|
254
|
+
// mermaid-diagram.tsx) is a large, cold module-graph evaluation
|
|
255
|
+
// (d3/dagre/cytoscape/khroma/katex) that can lose the race against Vitest's
|
|
256
|
+
// 5000ms default test timeout — not because the spec is wrong, but because it
|
|
257
|
+
// shares the event loop with every other worker on a CPU-starved host. Warm
|
|
258
|
+
// the module graph here, OUTSIDE any timed assertion, so the two specs below
|
|
259
|
+
// only ever pay a warm re-import. `mermaid` stays a dynamic import in
|
|
260
|
+
// mermaid-diagram.tsx (`pnpm heavy-deps:check` requires it) — this does not
|
|
261
|
+
// change that, it just pays the cold cost once, up front, off the clock.
|
|
262
|
+
beforeAll(async () => {
|
|
263
|
+
await import("mermaid");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Explicit, generous timeouts as the second, independent half of the fix —
|
|
267
|
+
// belt-and-suspenders alongside the warm-up above, in case the warm-up itself
|
|
268
|
+
// lands inside a contended window. See #64.
|
|
252
269
|
it("routes ```mermaid fences to the MermaidDiagram component", async () => {
|
|
253
270
|
render(<MarkdownPreview>{"```mermaid\ngraph TD; A-->B\n```"}</MarkdownPreview>);
|
|
254
271
|
await waitFor(() => expect(screen.getByTestId("mermaid-diagram")).toBeInTheDocument());
|
|
255
|
-
});
|
|
272
|
+
}, 15_000);
|
|
256
273
|
|
|
257
274
|
it("keeps non-mermaid fences as plain pre blocks", async () => {
|
|
258
275
|
const { container } = render(<MarkdownPreview>{"```ts\nconst x = 1;\n```"}</MarkdownPreview>);
|
|
259
276
|
await waitFor(() => expect(container.querySelector("pre")).toBeInTheDocument());
|
|
260
277
|
expect(container.querySelector('[data-testid="mermaid-diagram"]')).toBeNull();
|
|
261
|
-
});
|
|
278
|
+
}, 15_000);
|
|
262
279
|
});
|
|
263
280
|
|
|
264
281
|
describe("MarkdownPreview — colon false-positives (#workbench live finding)", () => {
|
|
@@ -18,6 +18,9 @@ const meta = {
|
|
|
18
18
|
docs: {
|
|
19
19
|
description: {
|
|
20
20
|
component:
|
|
21
|
+
"The markdown SOURCE-pane toolbar; the general-purpose roving control strip is " +
|
|
22
|
+
"`Layout/Toolbar` — see " +
|
|
23
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
21
24
|
"Formatting chrome for the markdown SOURCE pane, composed from @elabs-ai/components-ui. " +
|
|
22
25
|
"The Insert menu is driven by the slash-command registry, so source mode " +
|
|
23
26
|
"reaches the same brand blocks as the WYSIWYG `/` menu.",
|
|
@@ -3,6 +3,10 @@ import { createRef } from "react";
|
|
|
3
3
|
import { afterEach, expect, test, vi } from "vitest";
|
|
4
4
|
|
|
5
5
|
import type { SlashCommand } from "../markdown-editor/slash";
|
|
6
|
+
import {
|
|
7
|
+
hasPendingMilkdownTeardown,
|
|
8
|
+
waitForPendingMilkdownTeardown,
|
|
9
|
+
} from "../markdown-editor/milkdown-react/use-get-editor";
|
|
6
10
|
|
|
7
11
|
// ---------------------------------------------------------------------------
|
|
8
12
|
// Monaco mock — extended with revealLine / revealLineInCenter / getLineCount
|
|
@@ -83,8 +87,15 @@ vi.mock("monaco-editor", () => ({
|
|
|
83
87
|
|
|
84
88
|
import { MarkdownWorkspace, type MarkdownWorkspaceHandle } from "./markdown-workspace";
|
|
85
89
|
|
|
86
|
-
|
|
90
|
+
// #65 — `cleanup()` unmounts synchronously but Milkdown's own `editor.destroy()`
|
|
91
|
+
// is async (`@milkdown/ctx` schedules an internal cleanup timer inside it), so
|
|
92
|
+
// returning here without waiting let that timer fire after Vitest recycled this
|
|
93
|
+
// file's jsdom environment: `ReferenceError: removeEventListener is not defined`.
|
|
94
|
+
// Await every pending Milkdown teardown (tracked in `use-get-editor.ts`) before
|
|
95
|
+
// the next test's `vi.clearAllMocks()` / setup runs.
|
|
96
|
+
afterEach(async () => {
|
|
87
97
|
cleanup();
|
|
98
|
+
await waitForPendingMilkdownTeardown();
|
|
88
99
|
vi.clearAllMocks();
|
|
89
100
|
});
|
|
90
101
|
|
|
@@ -98,6 +109,25 @@ test("renders the mode switcher and the WYSIWYG editor", async () => {
|
|
|
98
109
|
await waitFor(() => expect(screen.getByText("Hello")).toBeInTheDocument());
|
|
99
110
|
});
|
|
100
111
|
|
|
112
|
+
// #65 — regression lock for the awaitable Milkdown-destroy handle itself. Proves
|
|
113
|
+
// (a) unmounting a Milkdown-backed surface registers a real pending teardown
|
|
114
|
+
// (not a no-op — a reverted fix would leave nothing tracked and fail the first
|
|
115
|
+
// assertion), and (b) awaiting the exposed handle actually drains it rather than
|
|
116
|
+
// resolving early. Five mount/unmount cycles back to back so a leak in the
|
|
117
|
+
// tracking Set itself (an entry that never gets removed) would also fail.
|
|
118
|
+
test("#65 awaits Milkdown teardown on unmount via the exposed handle", async () => {
|
|
119
|
+
for (let i = 0; i < 5; i += 1) {
|
|
120
|
+
const { unmount } = render(<MarkdownWorkspace defaultMode="wysiwyg" defaultValue="# Hello" />);
|
|
121
|
+
await waitFor(() => expect(screen.getByText("Hello")).toBeInTheDocument());
|
|
122
|
+
|
|
123
|
+
unmount();
|
|
124
|
+
expect(hasPendingMilkdownTeardown()).toBe(true);
|
|
125
|
+
|
|
126
|
+
await waitForPendingMilkdownTeardown();
|
|
127
|
+
expect(hasPendingMilkdownTeardown()).toBe(false);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
101
131
|
// #270 — focusWriting opt-out
|
|
102
132
|
test("focusWriting={false} hides the Focus toggle in wysiwyg mode", () => {
|
|
103
133
|
render(<MarkdownWorkspace defaultMode="wysiwyg" focusWriting={false} />);
|
|
@@ -278,7 +278,7 @@ export function MermaidViewer({ svg, label }: { svg: string; label: string }) {
|
|
|
278
278
|
className={cn(
|
|
279
279
|
"w-full truncate rounded-md px-2 py-1.5 text-start text-caption transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
280
280
|
"hover:bg-accent hover:text-accent-foreground",
|
|
281
|
-
"focus-
|
|
281
|
+
"focus-ring-inset",
|
|
282
282
|
activeHit === hit.id
|
|
283
283
|
? "bg-accent font-medium text-foreground"
|
|
284
284
|
: "text-muted-foreground",
|
|
@@ -9,6 +9,9 @@ const meta = {
|
|
|
9
9
|
docs: {
|
|
10
10
|
description: {
|
|
11
11
|
component:
|
|
12
|
+
"The typographic primitives every markdown renderer maps onto, not a renderer " +
|
|
13
|
+
"itself — see " +
|
|
14
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
12
15
|
"Token-driven typography primitives the markdown preview maps onto: Heading, " +
|
|
13
16
|
"Text, Link, List, Blockquote, InlineCode. Generic and theme-safe.",
|
|
14
17
|
},
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The teeth behind "one prose source, several renderers".
|
|
3
|
+
*
|
|
4
|
+
* `@elabs-ai/components-ui` owns the canonical `Prose*` set; this package
|
|
5
|
+
* re-exports it under the short names the `@elabs-ai/components-editor/markdown`
|
|
6
|
+
* surface has always used (ADR-0012 own/re-export model, #188). The whole claim
|
|
7
|
+
* that a `Prose*` change cannot drift the editor is that these are the SAME
|
|
8
|
+
* function objects — not two implementations that happen to agree today.
|
|
9
|
+
*
|
|
10
|
+
* Nothing enforced that. A future refactor could quietly replace a line here
|
|
11
|
+
* with a local component of the same name and every render test in
|
|
12
|
+
* `prose.test.tsx` would keep passing, because those assert BEHAVIOUR. These
|
|
13
|
+
* assert IDENTITY, which is the property the re-export actually promises.
|
|
14
|
+
*
|
|
15
|
+
* The import below is the package BARREL (`@elabs-ai/components-ui`), never a
|
|
16
|
+
* deep path into its source. A deep path can resolve to a second module
|
|
17
|
+
* instance whose exports are different objects with identical behaviour — the
|
|
18
|
+
* assertion would then fail for a reason that has nothing to do with the
|
|
19
|
+
* re-export, or (worse, with a different resolver) pass vacuously. The barrel is
|
|
20
|
+
* exactly what `prose.tsx` imports, so this compares like with like.
|
|
21
|
+
*/
|
|
22
|
+
import { describe, expect, it } from "vitest";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
ProseBlockquote,
|
|
26
|
+
ProseHeading,
|
|
27
|
+
ProseInlineCode,
|
|
28
|
+
ProseLink,
|
|
29
|
+
ProseList,
|
|
30
|
+
ProseListItem,
|
|
31
|
+
ProseText,
|
|
32
|
+
} from "@elabs-ai/components-ui";
|
|
33
|
+
|
|
34
|
+
import { Blockquote, Heading, InlineCode, Link, List, ListItem, Text } from "./prose";
|
|
35
|
+
|
|
36
|
+
describe("prose re-export identity", () => {
|
|
37
|
+
it.each([
|
|
38
|
+
["Heading", Heading, ProseHeading],
|
|
39
|
+
["Text", Text, ProseText],
|
|
40
|
+
["Link", Link, ProseLink],
|
|
41
|
+
["List", List, ProseList],
|
|
42
|
+
["ListItem", ListItem, ProseListItem],
|
|
43
|
+
["Blockquote", Blockquote, ProseBlockquote],
|
|
44
|
+
["InlineCode", InlineCode, ProseInlineCode],
|
|
45
|
+
])("%s is the same object as the ui primitive it re-exports", (_name, short, canonical) => {
|
|
46
|
+
expect(short).toBe(canonical);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("re-exports all seven, so the list above cannot silently shrink", () => {
|
|
50
|
+
const shortNames = [Heading, Text, Link, List, ListItem, Blockquote, InlineCode];
|
|
51
|
+
expect(shortNames).toHaveLength(7);
|
|
52
|
+
expect(shortNames.every((c) => typeof c !== "undefined")).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/prose/prose.tsx
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* `components/typography/prose.tsx`; this package derives). The
|
|
5
5
|
* `@elabs-ai/components-editor/markdown` public surface keeps the original names
|
|
6
6
|
* (Heading, Text, Link, List, ListItem, Blockquote, InlineCode).
|
|
7
|
+
*
|
|
8
|
+
* These must stay the SAME objects, not lookalikes: that identity is the whole
|
|
9
|
+
* reason a `Prose*` change cannot drift the editor away from the chat view or
|
|
10
|
+
* the file viewer. `prose.test.ts` asserts it against the package barrel — if
|
|
11
|
+
* you replace a line below with a local component, that test goes red while the
|
|
12
|
+
* behaviour tests in `prose.test.tsx` stay green, which is exactly the failure
|
|
13
|
+
* it exists to catch.
|
|
7
14
|
*/
|
|
8
15
|
export {
|
|
9
16
|
ProseHeading as Heading,
|