@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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `markdown-academic` — the opt-in academic layer for `MarkdownPreview`:
|
|
3
|
+
* footnotes, citations + bibliography, math (KaTeX), and a generated TOC. Each
|
|
4
|
+
* piece is enabled by a prop on `MarkdownPreview` (`footnotes` / `resolveCitation`
|
|
5
|
+
* / `math` / `toc`); the standalone components + types below are exported for
|
|
6
|
+
* direct composition and consumer typing.
|
|
7
|
+
*
|
|
8
|
+
* The remark transforms + tag constants are wired internally by `MarkdownPreview`
|
|
9
|
+
* and are intentionally NOT part of the public surface (drive them with the props).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Citations + bibliography.
|
|
13
|
+
export {
|
|
14
|
+
Bibliography,
|
|
15
|
+
collectCitations,
|
|
16
|
+
type BibliographyProps,
|
|
17
|
+
type CitationData,
|
|
18
|
+
type CitationStyle,
|
|
19
|
+
type CiteItem,
|
|
20
|
+
type CollectedCitations,
|
|
21
|
+
type ResolveCitation,
|
|
22
|
+
type ResolvedCitation,
|
|
23
|
+
} from "./citations";
|
|
24
|
+
|
|
25
|
+
// Math (KaTeX) — standalone renderers for direct use.
|
|
26
|
+
export { MathBlock, MathInline, type MathProps } from "./math";
|
|
27
|
+
|
|
28
|
+
// Footnotes — the branded definition section (standalone).
|
|
29
|
+
export { FootnoteList, type FootnoteListProps } from "./footnotes";
|
|
30
|
+
|
|
31
|
+
// Table of contents.
|
|
32
|
+
export { TableOfContents, type TableOfContentsProps } from "./toc";
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Math via `remark-math` + KaTeX (`$inline$`, and `$$block$$` on its own lines).
|
|
5
|
+
*
|
|
6
|
+
* `remark-math` parses `$…$` / `$$…$$` into `inlineMath` / `math` mdast nodes,
|
|
7
|
+
* which have NO mdast→hast handler (they'd otherwise degrade to literal text). So
|
|
8
|
+
* `remarkBrandMath` rewrites them into our own `brand-math` / `brand-math-inline`
|
|
9
|
+
* elements carrying the raw TeX, and the React renderers turn that into KaTeX.
|
|
10
|
+
*
|
|
11
|
+
* SECURITY: the TeX is untrusted input, so KaTeX runs with `trust: false` (blocks
|
|
12
|
+
* `\href`/`\url`/class injection), a bounded `maxExpand` (caps macro expansion —
|
|
13
|
+
* the `\def`-bomb DoS guard), and `throwOnError: false` (a bad expression renders
|
|
14
|
+
* a contained error, never crashes the page). a11y: `output: "htmlAndMathml"`
|
|
15
|
+
* emits MathML (read by assistive tech) alongside the visual HTML.
|
|
16
|
+
*/
|
|
17
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
18
|
+
import katex from "katex";
|
|
19
|
+
import { useMemo, type HTMLAttributes } from "react";
|
|
20
|
+
import { visit } from "unist-util-visit";
|
|
21
|
+
|
|
22
|
+
/** Block math element (`$$…$$`). */
|
|
23
|
+
export const MATH_BLOCK_TAG = "brand-math";
|
|
24
|
+
/** Inline math element (`$…$`). */
|
|
25
|
+
export const MATH_INLINE_TAG = "brand-math-inline";
|
|
26
|
+
/** hast property carrying the raw TeX (rendered as `data-tex`). */
|
|
27
|
+
export const MATH_PROP = "dataTex";
|
|
28
|
+
const MATH_ATTR = "data-tex";
|
|
29
|
+
|
|
30
|
+
/** Cap macro expansion — bounds `\def`-style expansion against untrusted input. */
|
|
31
|
+
const MAX_EXPAND = 1000;
|
|
32
|
+
|
|
33
|
+
interface MdNode {
|
|
34
|
+
type: string;
|
|
35
|
+
value?: string;
|
|
36
|
+
children?: MdNode[];
|
|
37
|
+
data?: { hName?: string; hProperties?: Record<string, unknown> };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Rewrite `inlineMath` / `math` (from remark-math) into branded elements carrying
|
|
42
|
+
* the raw TeX. Runs AFTER `remarkMath` in the plugin array.
|
|
43
|
+
*/
|
|
44
|
+
export function remarkBrandMath() {
|
|
45
|
+
return (tree: unknown) => {
|
|
46
|
+
visit(tree as never, (node: MdNode, index: number | undefined, parent: MdNode | undefined) => {
|
|
47
|
+
if (node.type !== "inlineMath" && node.type !== "math") return;
|
|
48
|
+
if (!parent?.children || index == null) return;
|
|
49
|
+
const display = node.type === "math";
|
|
50
|
+
const tex = typeof node.value === "string" ? node.value : "";
|
|
51
|
+
parent.children[index] = {
|
|
52
|
+
type: display ? "brandMathBlock" : "brandMathInline",
|
|
53
|
+
data: {
|
|
54
|
+
hName: display ? MATH_BLOCK_TAG : MATH_INLINE_TAG,
|
|
55
|
+
hProperties: { [MATH_PROP]: tex },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ------------------------------------------------------------------ */
|
|
63
|
+
/* React renderers */
|
|
64
|
+
/* ------------------------------------------------------------------ */
|
|
65
|
+
|
|
66
|
+
type TagProps = { node?: unknown; children?: React.ReactNode } & Record<string, unknown>;
|
|
67
|
+
|
|
68
|
+
function readTex(rest: TagProps): string {
|
|
69
|
+
return (rest[MATH_ATTR] as string | undefined) ?? (rest[MATH_PROP] as string | undefined) ?? "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Render TeX to a KaTeX HTML string (safe options); never throws. */
|
|
73
|
+
function renderKatex(tex: string, displayMode: boolean): { html: string; error: boolean } {
|
|
74
|
+
try {
|
|
75
|
+
return {
|
|
76
|
+
html: katex.renderToString(tex, {
|
|
77
|
+
displayMode,
|
|
78
|
+
throwOnError: false,
|
|
79
|
+
errorColor: "var(--destructive)",
|
|
80
|
+
trust: false,
|
|
81
|
+
maxExpand: MAX_EXPAND,
|
|
82
|
+
strict: "ignore",
|
|
83
|
+
output: "htmlAndMathml",
|
|
84
|
+
}),
|
|
85
|
+
error: false,
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return { html: "", error: true };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface MathProps extends Omit<HTMLAttributes<HTMLElement>, "children"> {
|
|
93
|
+
/** Raw TeX source. */
|
|
94
|
+
tex: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Inline math (`$…$`) → KaTeX, in the text flow. */
|
|
98
|
+
export function MathInline({ tex, className, ...props }: MathProps) {
|
|
99
|
+
const { html, error } = useMemo(() => renderKatex(tex, false), [tex]);
|
|
100
|
+
if (error) {
|
|
101
|
+
return (
|
|
102
|
+
<code
|
|
103
|
+
className={cn("text-destructive-text", className)}
|
|
104
|
+
aria-label={`Math (could not render): ${tex}`}
|
|
105
|
+
title="Could not render math"
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
{tex}
|
|
109
|
+
</code>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return (
|
|
113
|
+
<span
|
|
114
|
+
role="math"
|
|
115
|
+
// The raw TeX is a universally-readable fallback name for AT that does not
|
|
116
|
+
// process the embedded MathML; MathML-capable AT reads the MathML instead.
|
|
117
|
+
aria-label={tex}
|
|
118
|
+
className={cn("inline-block align-middle", className)}
|
|
119
|
+
// KaTeX output is generated with trust:false + bounded maxExpand (safe).
|
|
120
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Block math (`$$…$$`) → centered display KaTeX. */
|
|
127
|
+
export function MathBlock({ tex, className, ...props }: MathProps) {
|
|
128
|
+
const { html, error } = useMemo(() => renderKatex(tex, true), [tex]);
|
|
129
|
+
if (error) {
|
|
130
|
+
return (
|
|
131
|
+
<pre
|
|
132
|
+
className={cn(
|
|
133
|
+
"overflow-x-auto rounded-md bg-surface-muted p-3 text-destructive-text",
|
|
134
|
+
className,
|
|
135
|
+
)}
|
|
136
|
+
aria-label={`Math (could not render): ${tex}`}
|
|
137
|
+
title="Could not render math"
|
|
138
|
+
{...props}
|
|
139
|
+
>
|
|
140
|
+
<code>{tex}</code>
|
|
141
|
+
</pre>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
role="math"
|
|
147
|
+
aria-label={tex}
|
|
148
|
+
className={cn("my-3 overflow-x-auto text-center", className)}
|
|
149
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
150
|
+
{...props}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Components-map renderer for the inline math element. */
|
|
156
|
+
export function MathInlineTag({ node: _n, children: _c, ...rest }: TagProps) {
|
|
157
|
+
return <MathInline tex={readTex(rest)} />;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Components-map renderer for the block math element. */
|
|
161
|
+
export function MathBlockTag({ node: _n, children: _c, ...rest }: TagProps) {
|
|
162
|
+
return <MathBlock tex={readTex(rest)} />;
|
|
163
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generated table of contents — an in-flow `::toc` block.
|
|
5
|
+
*
|
|
6
|
+
* Reuses `parseMarkdownOutline` (the same heading extractor `DocumentOutline`
|
|
7
|
+
* uses — no second parser) for slugs + levels, and renders a quiet nav list of
|
|
8
|
+
* same-page anchor links. `MarkdownPreview` provides the outline through
|
|
9
|
+
* `TocProvider`; it also stamps the matching `id` on each rendered heading
|
|
10
|
+
* (`useHeadingId`) so the links resolve.
|
|
11
|
+
*/
|
|
12
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
13
|
+
import { createContext, forwardRef, useContext, type HTMLAttributes } from "react";
|
|
14
|
+
|
|
15
|
+
import type { MarkdownOutlineItem } from "../markdown-outline";
|
|
16
|
+
|
|
17
|
+
// Per-depth indent on the standard spacing scale (statically scannable so Tailwind
|
|
18
|
+
// keeps the classes). Index = heading depth relative to the shallowest in view.
|
|
19
|
+
const INDENT = ["ps-0", "ps-3", "ps-6", "ps-9", "ps-12", "ps-12"] as const;
|
|
20
|
+
|
|
21
|
+
interface TocState {
|
|
22
|
+
items: MarkdownOutlineItem[];
|
|
23
|
+
/** Heading slug keyed by 1-based start line (frontmatter-stripped coords). */
|
|
24
|
+
idByLine: Map<number, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const TocContext = createContext<TocState | null>(null);
|
|
28
|
+
|
|
29
|
+
export interface TocProviderProps {
|
|
30
|
+
items: MarkdownOutlineItem[];
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function TocProvider({ items, children }: TocProviderProps) {
|
|
35
|
+
const idByLine = new Map<number, string>();
|
|
36
|
+
for (const it of items) idByLine.set(it.line, it.id);
|
|
37
|
+
return <TocContext.Provider value={{ items, idByLine }}>{children}</TocContext.Provider>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The heading `id` for a 1-based source line, or `undefined`. */
|
|
41
|
+
export function useHeadingId(line: number | undefined): string | undefined {
|
|
42
|
+
const ctx = useContext(TocContext);
|
|
43
|
+
if (line == null || !ctx) return undefined;
|
|
44
|
+
return ctx.idByLine.get(line);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TableOfContentsProps extends Omit<HTMLAttributes<HTMLElement>, "children"> {
|
|
48
|
+
/** Heading outline; defaults to the outline collected by `MarkdownPreview`. */
|
|
49
|
+
items?: MarkdownOutlineItem[];
|
|
50
|
+
/** Section heading label. Default `"Contents"`. */
|
|
51
|
+
title?: string;
|
|
52
|
+
/** Deepest heading level to include (1–6). Default 3. */
|
|
53
|
+
maxLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Quiet generated TOC. Standalone with an explicit `items` array, or fed from the
|
|
58
|
+
* preview context inside a `::toc` block. Indentation tracks heading depth; no
|
|
59
|
+
* fill or border — the indent + links are the only gestures.
|
|
60
|
+
*/
|
|
61
|
+
export const TableOfContents = forwardRef<HTMLElement, TableOfContentsProps>(
|
|
62
|
+
function TableOfContents({ items, title = "Contents", maxLevel = 3, className, ...props }, ref) {
|
|
63
|
+
const ctx = useContext(TocContext);
|
|
64
|
+
const source = items ?? ctx?.items ?? [];
|
|
65
|
+
const list = source.filter((it) => it.level <= maxLevel);
|
|
66
|
+
if (list.length === 0) return null;
|
|
67
|
+
|
|
68
|
+
const minLevel = Math.min(...list.map((it) => it.level));
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<nav ref={ref} aria-label={title} className={cn("my-4 text-meta", className)} {...props}>
|
|
72
|
+
<p className="mb-2 font-medium text-muted-foreground">{title}</p>
|
|
73
|
+
<ol className="space-y-1">
|
|
74
|
+
{list.map((it) => (
|
|
75
|
+
<li key={it.id} className={INDENT[Math.min(it.level - minLevel, INDENT.length - 1)]}>
|
|
76
|
+
<a
|
|
77
|
+
href={`#${it.id}`}
|
|
78
|
+
className="text-muted-foreground underline hover:text-foreground hover:underline focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
79
|
+
>
|
|
80
|
+
{it.text}
|
|
81
|
+
</a>
|
|
82
|
+
</li>
|
|
83
|
+
))}
|
|
84
|
+
</ol>
|
|
85
|
+
</nav>
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CompletionMenu — the token-styled listbox the WYSIWYG completions widget
|
|
5
|
+
* renders at the caret (`completions-widget.tsx`). Deliberately simpler than
|
|
6
|
+
* `../slash/slash-menu.tsx`'s `SlashMenu`: `EditorCompletionItem` carries only
|
|
7
|
+
* `label`/`detail`/`insertText` (no groups/icons), so there is nothing to group.
|
|
8
|
+
* Same visual grammar (bg-popover, accent selection) for consistency with the
|
|
9
|
+
* slash popup and Monaco's own themed suggest widget.
|
|
10
|
+
*/
|
|
11
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
12
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
13
|
+
|
|
14
|
+
import type { EditorCompletionItem } from "../../lib/editor-completions";
|
|
15
|
+
|
|
16
|
+
export interface CompletionMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
17
|
+
/** The candidates to show (already resolved by the provider(s)). */
|
|
18
|
+
items: EditorCompletionItem[];
|
|
19
|
+
/** Index of the highlighted candidate. */
|
|
20
|
+
activeIndex: number;
|
|
21
|
+
/** Called when a candidate is chosen (click or the plugin's Enter/Tab). */
|
|
22
|
+
onSelect: (index: number) => void;
|
|
23
|
+
/** DOM id prefix so each option id is stable + unique. */
|
|
24
|
+
idPrefix?: string;
|
|
25
|
+
/** Shown while no candidates have resolved yet (or none matched). */
|
|
26
|
+
emptyLabel?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Build the stable DOM id for an option element. */
|
|
30
|
+
export function completionOptionId(idPrefix: string, index: number): string {
|
|
31
|
+
return `${idPrefix}-${index}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const CompletionMenu = forwardRef<HTMLDivElement, CompletionMenuProps>(
|
|
35
|
+
function CompletionMenu(
|
|
36
|
+
{
|
|
37
|
+
items,
|
|
38
|
+
activeIndex,
|
|
39
|
+
onSelect,
|
|
40
|
+
idPrefix = "brand-completions",
|
|
41
|
+
emptyLabel = "No suggestions",
|
|
42
|
+
className,
|
|
43
|
+
...props
|
|
44
|
+
},
|
|
45
|
+
ref,
|
|
46
|
+
) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
ref={ref}
|
|
50
|
+
role="listbox"
|
|
51
|
+
aria-label="Suggestions"
|
|
52
|
+
className={cn(
|
|
53
|
+
"max-h-[min(280px,50vh)] w-64 overflow-y-auto overflow-x-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{items.length === 0 ? (
|
|
59
|
+
<div className="px-2 py-3 text-center text-caption text-muted-foreground">
|
|
60
|
+
{emptyLabel}
|
|
61
|
+
</div>
|
|
62
|
+
) : (
|
|
63
|
+
items.map((item, index) => {
|
|
64
|
+
const selected = index === activeIndex;
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
key={`${item.label}-${String(index)}`}
|
|
68
|
+
id={completionOptionId(idPrefix, index)}
|
|
69
|
+
role="option"
|
|
70
|
+
aria-selected={selected}
|
|
71
|
+
data-selected={selected ? "true" : undefined}
|
|
72
|
+
// The editor keeps focus — select on mousedown (before the editor
|
|
73
|
+
// would steal focus back), mirroring `slash-menu.tsx`.
|
|
74
|
+
onMouseDown={(e) => {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
onSelect(index);
|
|
77
|
+
}}
|
|
78
|
+
className={cn(
|
|
79
|
+
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast",
|
|
80
|
+
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
<span className="flex min-w-0 flex-col">
|
|
84
|
+
<span className="truncate">{item.label}</span>
|
|
85
|
+
{item.detail ? (
|
|
86
|
+
<span className="truncate text-meta text-muted-foreground">{item.detail}</span>
|
|
87
|
+
) : null}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
})
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
);
|