vestrycms 0.0.0-stage → 0.2.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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -2
  3. package/dist/config/fields.d.ts +30 -0
  4. package/dist/config/fields.js +75 -0
  5. package/dist/config/index.d.ts +9 -0
  6. package/dist/config/index.js +18 -0
  7. package/dist/config/types.d.ts +412 -0
  8. package/dist/config/types.js +10 -0
  9. package/dist/core/api.d.ts +67 -0
  10. package/dist/core/api.js +543 -0
  11. package/dist/core/auth.d.ts +1597 -0
  12. package/dist/core/auth.js +264 -0
  13. package/dist/core/cache.d.ts +27 -0
  14. package/dist/core/cache.js +52 -0
  15. package/dist/core/entries.d.ts +19 -0
  16. package/dist/core/entries.js +127 -0
  17. package/dist/core/index-types.d.ts +20 -0
  18. package/dist/core/index-types.js +1 -0
  19. package/dist/core/index.d.ts +13 -0
  20. package/dist/core/index.js +13 -0
  21. package/dist/core/mail.d.ts +28 -0
  22. package/dist/core/mail.js +62 -0
  23. package/dist/core/paths.d.ts +35 -0
  24. package/dist/core/paths.js +148 -0
  25. package/dist/core/roles.d.ts +24 -0
  26. package/dist/core/roles.js +15 -0
  27. package/dist/core/service.d.ts +102 -0
  28. package/dist/core/service.js +478 -0
  29. package/dist/core/source.d.ts +82 -0
  30. package/dist/core/source.js +75 -0
  31. package/dist/core/sources/github-app.d.ts +26 -0
  32. package/dist/core/sources/github-app.js +161 -0
  33. package/dist/core/sources/github.d.ts +40 -0
  34. package/dist/core/sources/github.js +278 -0
  35. package/dist/core/sources/local.d.ts +16 -0
  36. package/dist/core/sources/local.js +98 -0
  37. package/dist/core/values.d.ts +22 -0
  38. package/dist/core/values.js +123 -0
  39. package/dist/env.d.ts +19 -0
  40. package/dist/index.d.ts +18 -0
  41. package/dist/index.js +167 -0
  42. package/dist/integration/build-index.d.ts +8 -0
  43. package/dist/integration/build-index.js +67 -0
  44. package/dist/integration/client-deps.d.ts +10 -0
  45. package/dist/integration/client-deps.js +43 -0
  46. package/dist/integration/dev-middleware.d.ts +6 -0
  47. package/dist/integration/dev-middleware.js +48 -0
  48. package/dist/integration/formatter.d.ts +7 -0
  49. package/dist/integration/formatter.js +56 -0
  50. package/dist/mdx/expressions.d.ts +14 -0
  51. package/dist/mdx/expressions.js +141 -0
  52. package/dist/mdx/index.d.ts +4 -0
  53. package/dist/mdx/index.js +4 -0
  54. package/dist/mdx/parse.d.ts +6 -0
  55. package/dist/mdx/parse.js +401 -0
  56. package/dist/mdx/serialize.d.ts +9 -0
  57. package/dist/mdx/serialize.js +485 -0
  58. package/dist/mdx/types.d.ts +42 -0
  59. package/dist/mdx/types.js +6 -0
  60. package/dist/pages/admin.astro +27 -0
  61. package/dist/pages/api.d.ts +3 -0
  62. package/dist/pages/api.js +31 -0
  63. package/dist/ui/App.d.ts +4 -0
  64. package/dist/ui/App.js +70 -0
  65. package/dist/ui/api.d.ts +83 -0
  66. package/dist/ui/api.js +97 -0
  67. package/dist/ui/auth-client.d.ts +27 -0
  68. package/dist/ui/auth-client.js +29 -0
  69. package/dist/ui/components/AssetPicker.d.ts +27 -0
  70. package/dist/ui/components/AssetPicker.js +97 -0
  71. package/dist/ui/components/Icon.d.ts +11 -0
  72. package/dist/ui/components/Icon.js +46 -0
  73. package/dist/ui/components/Layout.d.ts +15 -0
  74. package/dist/ui/components/Layout.js +87 -0
  75. package/dist/ui/components/editor/EditorContext.d.ts +15 -0
  76. package/dist/ui/components/editor/EditorContext.js +9 -0
  77. package/dist/ui/components/editor/NodeViews.d.ts +4 -0
  78. package/dist/ui/components/editor/NodeViews.js +127 -0
  79. package/dist/ui/components/editor/RichTextEditor.d.ts +24 -0
  80. package/dist/ui/components/editor/RichTextEditor.js +230 -0
  81. package/dist/ui/components/editor/extensions.d.ts +30 -0
  82. package/dist/ui/components/editor/extensions.js +248 -0
  83. package/dist/ui/components/editor/schema.d.ts +12 -0
  84. package/dist/ui/components/editor/schema.js +37 -0
  85. package/dist/ui/components/fields/CodeInput.d.ts +10 -0
  86. package/dist/ui/components/fields/CodeInput.js +8 -0
  87. package/dist/ui/components/fields/CodeMirrorInput.d.ts +2 -0
  88. package/dist/ui/components/fields/CodeMirrorInput.js +44 -0
  89. package/dist/ui/components/fields/FieldRenderer.d.ts +41 -0
  90. package/dist/ui/components/fields/FieldRenderer.js +299 -0
  91. package/dist/ui/components/fields/ImageTools.d.ts +31 -0
  92. package/dist/ui/components/fields/ImageTools.js +296 -0
  93. package/dist/ui/components/fields/MarkdownField.d.ts +7 -0
  94. package/dist/ui/components/fields/MarkdownField.js +16 -0
  95. package/dist/ui/components/fields/TreeInput.d.ts +13 -0
  96. package/dist/ui/components/fields/TreeInput.js +221 -0
  97. package/dist/ui/components/ui.d.ts +63 -0
  98. package/dist/ui/components/ui.js +113 -0
  99. package/dist/ui/context.d.ts +17 -0
  100. package/dist/ui/context.js +13 -0
  101. package/dist/ui/lib/format.d.ts +5 -0
  102. package/dist/ui/lib/format.js +11 -0
  103. package/dist/ui/lib/utils.d.ts +24 -0
  104. package/dist/ui/lib/utils.js +255 -0
  105. package/dist/ui/main.d.ts +1 -0
  106. package/dist/ui/main.js +11 -0
  107. package/dist/ui/pages/Assets.d.ts +1 -0
  108. package/dist/ui/pages/Assets.js +69 -0
  109. package/dist/ui/pages/CollectionList.d.ts +6 -0
  110. package/dist/ui/pages/CollectionList.js +115 -0
  111. package/dist/ui/pages/Dashboard.d.ts +1 -0
  112. package/dist/ui/pages/Dashboard.js +57 -0
  113. package/dist/ui/pages/EntryEditor.d.ts +3 -0
  114. package/dist/ui/pages/EntryEditor.js +313 -0
  115. package/dist/ui/pages/Login.d.ts +6 -0
  116. package/dist/ui/pages/Login.js +88 -0
  117. package/dist/ui/pages/Users.d.ts +1 -0
  118. package/dist/ui/pages/Users.js +102 -0
  119. package/dist/ui/styles.css +2554 -0
  120. package/package.json +123 -3
@@ -0,0 +1,127 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { NodeViewContent, NodeViewWrapper, } from "@tiptap/react";
3
+ import { ChevronDown, ChevronRight, Code2, Settings2, Trash2, } from "lucide-react";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { isRawExpression } from "../../../mdx/types.js";
6
+ import { cx, fieldLabel, summarize } from "../../lib/utils.js";
7
+ import { Icon } from "../Icon.js";
8
+ import { Button, Modal } from "../ui.js";
9
+ import { CodeInput } from "../fields/CodeInput.js";
10
+ import { FieldRenderer } from "../fields/FieldRenderer.js";
11
+ import { useEditorEnv } from "./EditorContext.js";
12
+ import { assetToRepoPath } from "../../../core/values.js";
13
+ /** Repo path of an image prop value (values are already repo paths in the editor; tolerate stored forms too). */
14
+ function assetPath(field, value) {
15
+ return assetToRepoPath(field, value, "");
16
+ }
17
+ /**
18
+ * Keeps a value in local state while also pushing it to the document.
19
+ *
20
+ * Node-view edits reach the document through a ProseMirror transaction, and
21
+ * the re-render with the new attrs arrives a tick later. A plain controlled
22
+ * input therefore sees its old value restored and then replaced, which
23
+ * throws the caret to the end on every keystroke. Local state re-renders
24
+ * synchronously; external changes (undo, a reload) still flow in because
25
+ * the document value is re-adopted whenever it differs from what we sent.
26
+ */
27
+ function useDocumentValue(value, commit) {
28
+ const [local, setLocal] = useState(value);
29
+ const sent = useRef(value);
30
+ useEffect(() => {
31
+ if (value !== sent.current && !deepEqual(value, sent.current)) {
32
+ sent.current = value;
33
+ setLocal(value);
34
+ }
35
+ }, [value]);
36
+ const update = useCallback((next) => {
37
+ sent.current = next;
38
+ setLocal(next);
39
+ commit(next);
40
+ }, [commit]);
41
+ return [local, update];
42
+ }
43
+ function deepEqual(a, b) {
44
+ if (a === b)
45
+ return true;
46
+ try {
47
+ return JSON.stringify(a) === JSON.stringify(b);
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ function PropsForm({ schema, props: docProps, onChange, }) {
54
+ const [props, setProps] = useDocumentValue(docProps, onChange);
55
+ const keys = new Set([...Object.keys(schema), ...Object.keys(props)]);
56
+ return (_jsx("div", { className: "v-fields", children: [...keys].map((key) => {
57
+ const field = schema[key];
58
+ const value = props[key];
59
+ const raw = isRawExpression(value);
60
+ if (!field || raw) {
61
+ // Unknown prop or non-literal expression: edit as code.
62
+ return (_jsxs("div", { className: "v-field", children: [_jsxs("label", { className: "v-field__label", children: [field ? fieldLabel(key, field) : key, _jsx("span", { className: "v-field__key", children: raw ? "expression" : "unknown prop" })] }), _jsx(CodeInput, { value: raw ? value.$raw : JSON.stringify(value, null, 2), language: "javascript", rows: 3, onChange: (v) => setProps({ ...props, [key]: { $raw: v } }) })] }, key));
63
+ }
64
+ if (field.hidden)
65
+ return null;
66
+ return (_jsx("div", { className: cx("v-field", field.width === "half" && "v-field--half", field.width === "third" && "v-field--third"), children: _jsx(FieldRenderer, { fieldKey: key, path: key, field: field, value: value, onChange: (v) => setProps({ ...props, [key]: v }) }) }, key));
67
+ }) }));
68
+ }
69
+ function summaryOf(props) {
70
+ const parts = [];
71
+ for (const [k, v] of Object.entries(props)) {
72
+ if (v === undefined || v === null || v === "")
73
+ continue;
74
+ if (isRawExpression(v))
75
+ parts.push(`${k}={…}`);
76
+ else if (typeof v === "string")
77
+ parts.push(`${k}="${v.length > 40 ? `${v.slice(0, 40)}…` : v}"`);
78
+ else if (Array.isArray(v))
79
+ parts.push(`${k}: ${v.length} item${v.length === 1 ? "" : "s"}`);
80
+ else if (typeof v === "object")
81
+ parts.push(`${k}: ${summarize(v) || "{…}"}`);
82
+ else
83
+ parts.push(`${k}=${String(v)}`);
84
+ }
85
+ return parts.join(" · ");
86
+ }
87
+ export function ComponentNodeView({ node, updateAttributes, deleteNode, selected, }) {
88
+ const env = useEditorEnv();
89
+ const commitProps = useCallback((p) => updateAttributes({ props: p }), [updateAttributes]);
90
+ const name = String(node.attrs.name);
91
+ const config = env.components[name];
92
+ const props = (node.attrs.props ?? {});
93
+ const schema = config?.props ?? {};
94
+ const [open, setOpen] = useState(() => Object.keys(schema).length > 0 &&
95
+ Object.values(schema).every((f) => f.type !== "array" && f.type !== "object"));
96
+ const thumbnail = Object.entries(schema).find(([k, f]) => f.type === "image" && typeof props[k] === "string" && props[k]);
97
+ const acceptsChildren = config
98
+ ? Boolean(config.children)
99
+ : node.childCount > 0 || Boolean(node.attrs.hasChildren);
100
+ const hasProps = Object.keys(schema).length > 0 || Object.keys(props).length > 0;
101
+ return (_jsxs(NodeViewWrapper, { className: cx("v-block", selected && "is-selected", !open && !acceptsChildren && "is-collapsed"), children: [_jsxs("div", { className: "v-block__header", contentEditable: false, "data-drag-handle": "", children: [_jsx("span", { className: "v-block__icon", children: _jsx(Icon, { name: config?.icon ?? "box" }) }), _jsx("span", { className: "v-block__name", children: config?.label ?? name }), thumbnail && (_jsx("img", { className: "v-thumb", style: { width: 36, height: 26 }, src: env.resolveImage?.(env.toDocPath(assetPath(schema[thumbnail[0]], String(props[thumbnail[0]])))) ?? "", alt: "" })), _jsx("span", { className: "v-block__summary", children: !open ? summaryOf(props) : config?.description }), _jsxs("div", { className: "v-block__actions", children: [hasProps && (_jsx(Button, { variant: "ghost", size: "xs", icon: true, title: open ? "Hide settings" : "Edit settings", onClick: () => setOpen((o) => !o), children: open ? _jsx(ChevronDown, {}) : _jsx(Settings2, {}) })), _jsx(Button, { variant: "ghost", size: "xs", icon: true, title: "Remove block", onClick: deleteNode, children: _jsx(Trash2, {}) })] })] }), open && hasProps && (_jsx("div", { className: "v-block__props", contentEditable: false, children: _jsx(PropsForm, { schema: schema, props: props, onChange: commitProps }) })), acceptsChildren && (_jsx("div", { className: "v-block__children", children: _jsx(NodeViewContent, { className: "v-prose v-prose-nested" }) })), !config && !acceptsChildren && !hasProps && (_jsx("div", { className: "v-block__empty", children: "Unknown component with no props." }))] }));
102
+ }
103
+ export function InlineComponentNodeView({ node, updateAttributes, deleteNode, }) {
104
+ const env = useEditorEnv();
105
+ const commitProps = useCallback((p) => updateAttributes({ props: p }), [updateAttributes]);
106
+ const name = String(node.attrs.name);
107
+ const config = env.components[name];
108
+ const [editing, setEditing] = useState(false);
109
+ const props = (node.attrs.props ?? {});
110
+ return (_jsxs(NodeViewWrapper, { as: "span", className: "v-inline-component", "data-name": name, onDoubleClick: () => setEditing(true), children: [_jsx(NodeViewContent, { as: "span" }), editing && (_jsx(Modal, { title: `${config?.label ?? name} settings`, onClose: () => setEditing(false), footer: _jsxs(_Fragment, { children: [_jsxs(Button, { variant: "danger", onClick: deleteNode, children: [_jsx(Trash2, {}), " Remove"] }), _jsx(Button, { variant: "primary", onClick: () => setEditing(false), children: "Done" })] }), children: _jsx(PropsForm, { schema: config?.props ?? {}, props: props, onChange: commitProps }) }))] }));
111
+ }
112
+ const RAW_LABELS = {
113
+ expression: {
114
+ label: "Expression",
115
+ hint: "JavaScript expression rendered by MDX",
116
+ },
117
+ esm: { label: "Module code", hint: "Imports and exports kept verbatim" },
118
+ html: { label: "Raw HTML", hint: "Passed through as-is" },
119
+ };
120
+ export function RawBlockNodeView({ node, updateAttributes, deleteNode, selected, }) {
121
+ const kind = String(node.attrs.kind ?? "expression");
122
+ const [open, setOpen] = useState(false);
123
+ const meta = RAW_LABELS[kind] ?? RAW_LABELS.expression;
124
+ const commitValue = useCallback((v) => updateAttributes({ value: v }), [updateAttributes]);
125
+ const [value, setValue] = useDocumentValue(String(node.attrs.value ?? ""), commitValue);
126
+ return (_jsxs(NodeViewWrapper, { className: cx("v-block v-block--raw", selected && "is-selected"), children: [_jsxs("div", { className: "v-block__header", contentEditable: false, "data-drag-handle": "", children: [_jsx("span", { className: "v-block__icon", children: _jsx(Code2, {}) }), _jsx("span", { className: "v-block__name", children: meta.label }), _jsx("span", { className: "v-block__summary", children: meta.hint }), _jsxs("div", { className: "v-block__actions", children: [_jsx(Button, { variant: "ghost", size: "xs", icon: true, title: open ? "Collapse" : "Edit", onClick: () => setOpen((o) => !o), children: open ? _jsx(ChevronDown, {}) : _jsx(ChevronRight, {}) }), _jsx(Button, { variant: "ghost", size: "xs", icon: true, title: "Remove", onClick: deleteNode, children: _jsx(Trash2, {}) })] })] }), _jsx("div", { contentEditable: false, children: open ? (_jsx("div", { className: "v-block__code", children: _jsx("textarea", { value: value, onChange: (e) => setValue(e.target.value), spellCheck: false }) })) : (_jsx("pre", { className: "v-block__code", children: kind === "expression" ? `{${value}}` : value })) })] }));
127
+ }
@@ -0,0 +1,24 @@
1
+ import { type EditorEnv } from "./EditorContext.js";
2
+ export interface RichTextEditorProps {
3
+ value: string;
4
+ onChange: (mdx: string) => void;
5
+ /** Component names allowed in this document. */
6
+ components?: string[];
7
+ env: EditorEnv;
8
+ format?: "md" | "mdx";
9
+ placeholder?: string;
10
+ compact?: boolean;
11
+ /** Render without its own border, to sit flush inside a card. */
12
+ flush?: boolean;
13
+ onUpload?: (upload: {
14
+ path: string;
15
+ base64: string;
16
+ previewUrl: string;
17
+ }) => void;
18
+ }
19
+ /**
20
+ * Tiptap editor that reads and writes MDX. The document is parsed once on
21
+ * load; edits are serialized back (debounced) and reported through `onChange`.
22
+ * A source mode exposes the raw MDX in CodeMirror; switching back re-parses.
23
+ */
24
+ export declare function RichTextEditor(props: RichTextEditorProps): import("react").JSX.Element;
@@ -0,0 +1,230 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { EditorContent, useEditor, useEditorState, } from "@tiptap/react";
3
+ import { Bold, Code, Code2, Eye, Heading1, Heading2, Heading3, Image as ImageIcon, Italic, Link as LinkIcon, List, ListOrdered, Minus, Plus, Quote, Redo2, Strikethrough, Subscript as SubIcon, Superscript as SupIcon, Table as TableIcon, Undo2, Unlink, } from "lucide-react";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { parseMdx, serializeMdx } from "../../../mdx/index.js";
6
+ import { cx, defaultValues } from "../../lib/utils.js";
7
+ import { AssetPicker } from "../AssetPicker.js";
8
+ import { Icon } from "../Icon.js";
9
+ import { CodeInput } from "../fields/CodeInput.js";
10
+ import { Button, Menu, Modal } from "../ui.js";
11
+ import { EditorEnvContext, useEditorEnv, } from "./EditorContext.js";
12
+ import { editorExtensions } from "./schema.js";
13
+ /**
14
+ * Tiptap editor that reads and writes MDX. The document is parsed once on
15
+ * load; edits are serialized back (debounced) and reported through `onChange`.
16
+ * A source mode exposes the raw MDX in CodeMirror; switching back re-parses.
17
+ */
18
+ export function RichTextEditor(props) {
19
+ return (_jsx(EditorEnvContext.Provider, { value: props.env, children: _jsx(EditorInner, { ...props }) }));
20
+ }
21
+ function EditorInner({ value, onChange, components, env, format = "mdx", placeholder, compact, flush, onUpload, }) {
22
+ const [mode, setMode] = useState("visual");
23
+ const [parseError, setParseError] = useState(null);
24
+ const importsRef = useRef({});
25
+ const lastEmitted = useRef(value);
26
+ const registry = useMemo(() => {
27
+ const out = {};
28
+ for (const [name, c] of Object.entries(env.components)) {
29
+ out[name] = {
30
+ import: c.import,
31
+ namedImport: c.namedImport,
32
+ inline: c.inline,
33
+ children: c.children,
34
+ };
35
+ }
36
+ return out;
37
+ }, [env.components]);
38
+ const parse = useCallback((mdx) => {
39
+ try {
40
+ const parsed = parseMdx(mdx, { format });
41
+ importsRef.current = parsed.imports;
42
+ setParseError(null);
43
+ return parsed.doc;
44
+ }
45
+ catch (e) {
46
+ setParseError(e.message);
47
+ return null;
48
+ }
49
+ }, [format]);
50
+ const serialize = useCallback((doc) => serializeMdx(doc, {
51
+ format,
52
+ components: registry,
53
+ existingImports: importsRef.current,
54
+ }), [format, registry]);
55
+ const initialDoc = useMemo(() => parse(value), []); // eslint-disable-line react-hooks/exhaustive-deps
56
+ const extensions = useMemo(() => editorExtensions({
57
+ resolveSrc: (src) => env.resolveImage?.(src) ?? src,
58
+ placeholder,
59
+ }), [env, placeholder]);
60
+ const timer = useRef(null);
61
+ const editor = useEditor({
62
+ extensions,
63
+ content: initialDoc ?? { type: "doc", content: [{ type: "paragraph" }] },
64
+ editorProps: { attributes: { class: "v-prose", spellcheck: "true" } },
65
+ onUpdate: ({ editor }) => {
66
+ if (timer.current)
67
+ clearTimeout(timer.current);
68
+ timer.current = setTimeout(() => {
69
+ const mdx = serialize(editor.getJSON());
70
+ lastEmitted.current = mdx;
71
+ onChange(mdx);
72
+ }, 250);
73
+ },
74
+ });
75
+ // External value changes (e.g. after a reload) → re-parse.
76
+ useEffect(() => {
77
+ if (!editor || mode !== "visual")
78
+ return;
79
+ if (value === lastEmitted.current)
80
+ return;
81
+ const doc = parse(value);
82
+ if (doc) {
83
+ lastEmitted.current = value;
84
+ editor.commands.setContent(doc, { emitUpdate: false });
85
+ }
86
+ }, [value, editor, mode, parse]);
87
+ useEffect(() => () => {
88
+ if (timer.current)
89
+ clearTimeout(timer.current);
90
+ }, []);
91
+ const switchMode = (next) => {
92
+ if (next === mode || !editor)
93
+ return;
94
+ if (next === "source") {
95
+ if (timer.current) {
96
+ clearTimeout(timer.current);
97
+ timer.current = null;
98
+ const mdx = serialize(editor.getJSON());
99
+ lastEmitted.current = mdx;
100
+ onChange(mdx);
101
+ }
102
+ setMode("source");
103
+ }
104
+ else {
105
+ const doc = parse(value);
106
+ if (!doc)
107
+ return;
108
+ lastEmitted.current = value;
109
+ editor.commands.setContent(doc, { emitUpdate: false });
110
+ setMode("visual");
111
+ }
112
+ };
113
+ return (_jsxs("div", { className: cx("v-rte", compact && "v-rte--compact", flush && "v-rte--flush"), children: [editor && (_jsx(Toolbar, { editor: editor, mode: mode, onMode: switchMode, components: components ?? Object.keys(env.components), onUpload: onUpload, format: format })), mode === "visual" ? (_jsx(EditorContent, { editor: editor })) : (_jsxs("div", { className: "v-source-editor", children: [parseError && (_jsxs("div", { className: "v-alert v-alert--error", style: { margin: 10 }, children: ["Could not parse MDX: ", parseError] })), _jsx(CodeInput, { value: value, language: "markdown", minHeight: "320px", onChange: (v) => {
114
+ lastEmitted.current = v;
115
+ onChange(v);
116
+ } })] }))] }));
117
+ }
118
+ function Toolbar({ editor, mode, onMode, components, onUpload, format, }) {
119
+ const env = useEditorEnv();
120
+ const [linkOpen, setLinkOpen] = useState(false);
121
+ const [imageOpen, setImageOpen] = useState(false);
122
+ const state = useEditorState({
123
+ editor,
124
+ selector: ({ editor }) => ({
125
+ bold: editor.isActive("bold"),
126
+ italic: editor.isActive("italic"),
127
+ strike: editor.isActive("strike"),
128
+ code: editor.isActive("code"),
129
+ sup: editor.isActive("superscript"),
130
+ sub: editor.isActive("subscript"),
131
+ link: editor.isActive("link"),
132
+ h1: editor.isActive("heading", { level: 1 }),
133
+ h2: editor.isActive("heading", { level: 2 }),
134
+ h3: editor.isActive("heading", { level: 3 }),
135
+ bullet: editor.isActive("bulletList"),
136
+ ordered: editor.isActive("orderedList"),
137
+ quote: editor.isActive("blockquote"),
138
+ codeBlock: editor.isActive("codeBlock"),
139
+ table: editor.isActive("table"),
140
+ canUndo: editor.can().undo(),
141
+ canRedo: editor.can().redo(),
142
+ }),
143
+ });
144
+ const disabled = mode === "source";
145
+ const tool = (label, active, icon, run, isDisabled = false) => (_jsx("button", { type: "button", className: cx("v-rte__tool", active && "is-active"), title: label, "aria-label": label, disabled: disabled || isDisabled, onMouseDown: (e) => e.preventDefault(), onClick: run, children: icon }));
146
+ const insertComponent = (name) => {
147
+ const cfg = env.components[name];
148
+ const props = defaultValues(cfg?.props ?? {});
149
+ if (cfg?.inline) {
150
+ editor
151
+ .chain()
152
+ .focus()
153
+ .insertContent({
154
+ type: "mdxInlineComponent",
155
+ attrs: { name, props, hasChildren: Boolean(cfg.children) },
156
+ content: cfg.children ? [{ type: "text", text: "text" }] : [],
157
+ })
158
+ .run();
159
+ return;
160
+ }
161
+ const node = {
162
+ type: "mdxComponent",
163
+ attrs: { name, props, hasChildren: Boolean(cfg?.children) },
164
+ content: cfg?.children ? [{ type: "paragraph" }] : [],
165
+ };
166
+ editor.chain().focus().insertContent(node).run();
167
+ };
168
+ return (_jsxs("div", { className: "v-rte__toolbar", children: [tool("Undo", false, _jsx(Undo2, {}), () => editor.chain().focus().undo().run(), !state.canUndo), tool("Redo", false, _jsx(Redo2, {}), () => editor.chain().focus().redo().run(), !state.canRedo), _jsx("span", { className: "v-rte__sep" }), tool("Heading 1", state.h1, _jsx(Heading1, {}), () => editor.chain().focus().toggleHeading({ level: 1 }).run()), tool("Heading 2", state.h2, _jsx(Heading2, {}), () => editor.chain().focus().toggleHeading({ level: 2 }).run()), tool("Heading 3", state.h3, _jsx(Heading3, {}), () => editor.chain().focus().toggleHeading({ level: 3 }).run()), _jsx("span", { className: "v-rte__sep" }), tool("Bold", state.bold, _jsx(Bold, {}), () => editor.chain().focus().toggleBold().run()), tool("Italic", state.italic, _jsx(Italic, {}), () => editor.chain().focus().toggleItalic().run()), tool("Strikethrough", state.strike, _jsx(Strikethrough, {}), () => editor.chain().focus().toggleStrike().run()), tool("Inline code", state.code, _jsx(Code, {}), () => editor.chain().focus().toggleCode().run()), format === "mdx" &&
169
+ tool("Superscript", state.sup, _jsx(SupIcon, {}), () => editor.chain().focus().toggleSuperscript().run()), format === "mdx" &&
170
+ tool("Subscript", state.sub, _jsx(SubIcon, {}), () => editor.chain().focus().toggleSubscript().run()), state.link
171
+ ? tool("Remove link", true, _jsx(Unlink, {}), () => editor.chain().focus().unsetLink().run())
172
+ : tool("Link", false, _jsx(LinkIcon, {}), () => setLinkOpen(true)), _jsx("span", { className: "v-rte__sep" }), tool("Bullet list", state.bullet, _jsx(List, {}), () => editor.chain().focus().toggleBulletList().run()), tool("Numbered list", state.ordered, _jsx(ListOrdered, {}), () => editor.chain().focus().toggleOrderedList().run()), tool("Quote", state.quote, _jsx(Quote, {}), () => editor.chain().focus().toggleBlockquote().run()), tool("Code block", state.codeBlock, _jsx(Code2, {}), () => editor.chain().focus().toggleCodeBlock().run()), tool("Divider", false, _jsx(Minus, {}), () => editor.chain().focus().setHorizontalRule().run()), tool("Image", false, _jsx(ImageIcon, {}), () => setImageOpen(true), !env.imageDirectory), _jsx(Menu, { trigger: ({ toggle }) => (_jsx("button", { type: "button", className: cx("v-rte__tool", state.table && "is-active"), title: "Table", disabled: disabled, onMouseDown: (e) => e.preventDefault(), onClick: toggle, children: _jsx(TableIcon, {}) })), children: (close) => (_jsxs(_Fragment, { children: [_jsx("button", { className: "v-menu__item", onClick: () => {
173
+ editor
174
+ .chain()
175
+ .focus()
176
+ .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
177
+ .run();
178
+ close();
179
+ }, children: "Insert table" }), state.table && (_jsxs(_Fragment, { children: [_jsx("button", { className: "v-menu__item", onClick: () => {
180
+ editor.chain().focus().addRowAfter().run();
181
+ close();
182
+ }, children: "Add row below" }), _jsx("button", { className: "v-menu__item", onClick: () => {
183
+ editor.chain().focus().addColumnAfter().run();
184
+ close();
185
+ }, children: "Add column after" }), _jsx("button", { className: "v-menu__item", onClick: () => {
186
+ editor.chain().focus().deleteRow().run();
187
+ close();
188
+ }, children: "Delete row" }), _jsx("button", { className: "v-menu__item", onClick: () => {
189
+ editor.chain().focus().deleteColumn().run();
190
+ close();
191
+ }, children: "Delete column" }), _jsx("button", { className: "v-menu__item is-danger", onClick: () => {
192
+ editor.chain().focus().deleteTable().run();
193
+ close();
194
+ }, children: "Delete table" })] }))] })) }), format === "mdx" && components.length > 0 && (_jsxs(_Fragment, { children: [_jsx("span", { className: "v-rte__sep" }), _jsx(Menu, { trigger: ({ toggle }) => (_jsxs("button", { type: "button", className: "v-rte__insert", disabled: disabled, onMouseDown: (e) => e.preventDefault(), onClick: toggle, children: [_jsx(Plus, {}), " Insert"] })), children: (close) => (_jsxs(_Fragment, { children: [_jsx("div", { className: "v-menu__heading", children: "Components" }), components.map((name) => {
195
+ const c = env.components[name];
196
+ return (_jsxs("button", { className: "v-menu__item", onClick: () => {
197
+ insertComponent(name);
198
+ close();
199
+ }, children: [_jsx(Icon, { name: c?.icon ?? "box" }), _jsxs("span", { children: [c?.label ?? name, c?.description && _jsx("small", { children: c.description })] })] }, name));
200
+ })] })) })] })), _jsx("span", { className: "v-rte__spacer" }), _jsxs("div", { className: "v-segment", style: { height: 30 }, children: [_jsxs("button", { type: "button", className: cx(mode === "visual" && "is-active"), style: { height: 28 }, onClick: () => onMode("visual"), title: "Visual editor", children: [_jsx(Eye, { size: 14, style: { marginRight: 4, verticalAlign: -2 } }), " Visual"] }), _jsxs("button", { type: "button", className: cx(mode === "source" && "is-active"), style: { height: 28 }, onClick: () => onMode("source"), title: "Edit MDX source", children: [_jsx(Code2, { size: 14, style: { marginRight: 4, verticalAlign: -2 } }), " ", "Source"] })] }), linkOpen && (_jsx(LinkDialog, { editor: editor, onClose: () => setLinkOpen(false) })), imageOpen && env.imageDirectory && (_jsx(AssetPicker, { directory: env.imageDirectory, accept: ["jpg", "jpeg", "png", "gif", "webp", "avif", "svg"], title: "Insert image", deferUploads: Boolean(onUpload), onClose: () => setImageOpen(false), onPick: (path, upload) => {
201
+ if (upload)
202
+ onUpload?.(upload);
203
+ editor
204
+ .chain()
205
+ .focus()
206
+ .setImage({ src: env.toDocPath(path), alt: "" })
207
+ .run();
208
+ setImageOpen(false);
209
+ } }))] }));
210
+ }
211
+ function LinkDialog({ editor, onClose, }) {
212
+ const [href, setHref] = useState(() => editor.getAttributes("link").href ?? "");
213
+ const apply = () => {
214
+ const url = href.trim();
215
+ if (!url)
216
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
217
+ else
218
+ editor
219
+ .chain()
220
+ .focus()
221
+ .extendMarkRange("link")
222
+ .setLink({ href: url })
223
+ .run();
224
+ onClose();
225
+ };
226
+ return (_jsxs(Modal, { title: "Link", onClose: onClose, footer: _jsxs(_Fragment, { children: [_jsx(Button, { onClick: onClose, children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: apply, children: "Apply" })] }), children: [_jsx("label", { className: "v-field__label", htmlFor: "v-link-url", children: "URL" }), _jsx("input", { id: "v-link-url", className: "v-input", autoFocus: true, value: href, placeholder: "https://example.com or /visit", onChange: (e) => setHref(e.target.value), onKeyDown: (e) => {
227
+ if (e.key === "Enter")
228
+ apply();
229
+ } })] }));
230
+ }
@@ -0,0 +1,30 @@
1
+ import { Extension, Node } from "@tiptap/core";
2
+ /** A block-level MDX component, e.g. `<Highlights items={[...]} />`. */
3
+ export declare const MdxComponent: Node<any, any>;
4
+ /** An inline MDX component, e.g. `<Abbr title="...">WELS</Abbr>`. */
5
+ export declare const MdxInlineComponent: Node<any, any>;
6
+ /** `{expression}` at block level. */
7
+ export declare const MdxExpression: Node<any, any>;
8
+ /** `export const x = ...` and other ESM that isn't a known import. */
9
+ export declare const MdxEsm: Node<any, any>;
10
+ /** Raw HTML blocks (plain Markdown files). */
11
+ export declare const RawHtml: Node<any, any>;
12
+ export declare const MdxInlineExpression: Node<any, any>;
13
+ export declare const RawInlineHtml: Node<any, any>;
14
+ /**
15
+ * Markdown images are inline. `resolveSrc` maps the stored path (relative to
16
+ * the entry file, or a public URL) to something the browser can load.
17
+ */
18
+ export declare const MdxImage: Node<{
19
+ resolveSrc: (src: string) => string;
20
+ } & Record<string, unknown>, any>;
21
+ /**
22
+ * List attributes from Markdown that StarterKit's lists lack: whether a list
23
+ * is loose (blank lines between items; tightening it changes the rendered
24
+ * HTML) and a task item's checkbox (`- [ ]` / `- [x]`).
25
+ */
26
+ export declare const ListAttributes: Extension<any, any>;
27
+ /** A GFM footnote definition, `[^label]: ...`. */
28
+ export declare const FootnoteDefinition: Node<any, any>;
29
+ /** A GFM footnote reference, `[^label]`. */
30
+ export declare const FootnoteReference: Node<any, any>;
@@ -0,0 +1,248 @@
1
+ import { Extension, Node, mergeAttributes } from "@tiptap/core";
2
+ import Image from "@tiptap/extension-image";
3
+ import { ReactNodeViewRenderer } from "@tiptap/react";
4
+ import { ComponentNodeView, InlineComponentNodeView, RawBlockNodeView, } from "./NodeViews.js";
5
+ /** A block-level MDX component, e.g. `<Highlights items={[...]} />`. */
6
+ export const MdxComponent = Node.create({
7
+ name: "mdxComponent",
8
+ group: "block",
9
+ content: "block*",
10
+ defining: true,
11
+ isolating: true,
12
+ draggable: true,
13
+ selectable: true,
14
+ addAttributes() {
15
+ return {
16
+ name: { default: "" },
17
+ props: { default: {} },
18
+ spreads: { default: [] },
19
+ hasChildren: { default: false },
20
+ };
21
+ },
22
+ parseHTML() {
23
+ return [{ tag: "div[data-mdx-component]" }];
24
+ },
25
+ renderHTML({ HTMLAttributes, node }) {
26
+ return [
27
+ "div",
28
+ mergeAttributes(HTMLAttributes, {
29
+ "data-mdx-component": node.attrs.name,
30
+ }),
31
+ 0,
32
+ ];
33
+ },
34
+ addNodeView() {
35
+ return ReactNodeViewRenderer(ComponentNodeView, {
36
+ contentDOMElementTag: "div",
37
+ });
38
+ },
39
+ });
40
+ /** An inline MDX component, e.g. `<Abbr title="...">WELS</Abbr>`. */
41
+ export const MdxInlineComponent = Node.create({
42
+ name: "mdxInlineComponent",
43
+ group: "inline",
44
+ inline: true,
45
+ content: "inline*",
46
+ selectable: true,
47
+ addAttributes() {
48
+ return {
49
+ name: { default: "" },
50
+ props: { default: {} },
51
+ spreads: { default: [] },
52
+ hasChildren: { default: false },
53
+ };
54
+ },
55
+ parseHTML() {
56
+ return [{ tag: "span[data-mdx-inline-component]" }];
57
+ },
58
+ renderHTML({ HTMLAttributes, node }) {
59
+ return [
60
+ "span",
61
+ mergeAttributes(HTMLAttributes, {
62
+ "data-mdx-inline-component": node.attrs.name,
63
+ class: "v-inline-component",
64
+ "data-name": node.attrs.name,
65
+ }),
66
+ 0,
67
+ ];
68
+ },
69
+ addNodeView() {
70
+ return ReactNodeViewRenderer(InlineComponentNodeView, {
71
+ contentDOMElementTag: "span",
72
+ });
73
+ },
74
+ });
75
+ function rawBlock(name, kind) {
76
+ return Node.create({
77
+ name,
78
+ group: "block",
79
+ atom: true,
80
+ draggable: true,
81
+ selectable: true,
82
+ addAttributes() {
83
+ return { value: { default: "" }, kind: { default: kind } };
84
+ },
85
+ parseHTML() {
86
+ return [{ tag: `div[data-${name}]` }];
87
+ },
88
+ renderHTML({ HTMLAttributes, node }) {
89
+ return [
90
+ "div",
91
+ mergeAttributes(HTMLAttributes, { [`data-${name}`]: "" }),
92
+ node.attrs.value,
93
+ ];
94
+ },
95
+ addNodeView() {
96
+ return ReactNodeViewRenderer(RawBlockNodeView);
97
+ },
98
+ });
99
+ }
100
+ /** `{expression}` at block level. */
101
+ export const MdxExpression = rawBlock("mdxExpression", "expression");
102
+ /** `export const x = ...` and other ESM that isn't a known import. */
103
+ export const MdxEsm = rawBlock("mdxEsm", "esm");
104
+ /** Raw HTML blocks (plain Markdown files). */
105
+ export const RawHtml = rawBlock("rawHtml", "html");
106
+ function rawInline(name) {
107
+ return Node.create({
108
+ name,
109
+ group: "inline",
110
+ inline: true,
111
+ atom: true,
112
+ selectable: true,
113
+ addAttributes() {
114
+ return { value: { default: "" } };
115
+ },
116
+ parseHTML() {
117
+ return [{ tag: `span[data-${name}]` }];
118
+ },
119
+ renderHTML({ HTMLAttributes, node }) {
120
+ return [
121
+ "span",
122
+ mergeAttributes(HTMLAttributes, {
123
+ [`data-${name}`]: "",
124
+ class: "v-inline-expression",
125
+ title: "Click to edit",
126
+ }),
127
+ name === "mdxInlineExpression"
128
+ ? `{${node.attrs.value}}`
129
+ : node.attrs.value,
130
+ ];
131
+ },
132
+ });
133
+ }
134
+ export const MdxInlineExpression = rawInline("mdxInlineExpression");
135
+ export const RawInlineHtml = rawInline("rawInlineHtml");
136
+ /**
137
+ * Markdown images are inline. `resolveSrc` maps the stored path (relative to
138
+ * the entry file, or a public URL) to something the browser can load.
139
+ */
140
+ export const MdxImage = Image.extend({
141
+ addOptions() {
142
+ return {
143
+ ...this.parent?.(),
144
+ inline: true,
145
+ allowBase64: false,
146
+ resolveSrc: (src) => src,
147
+ };
148
+ },
149
+ renderHTML({ HTMLAttributes }) {
150
+ const src = String(HTMLAttributes.src ?? "");
151
+ return [
152
+ "img",
153
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { src: this.options.resolveSrc(src), "data-src": src }),
154
+ ];
155
+ },
156
+ });
157
+ /**
158
+ * List attributes from Markdown that StarterKit's lists lack: whether a list
159
+ * is loose (blank lines between items; tightening it changes the rendered
160
+ * HTML) and a task item's checkbox (`- [ ]` / `- [x]`).
161
+ */
162
+ export const ListAttributes = Extension.create({
163
+ name: "listAttributes",
164
+ addGlobalAttributes() {
165
+ return [
166
+ {
167
+ types: ["bulletList", "orderedList"],
168
+ attributes: {
169
+ spread: {
170
+ default: false,
171
+ parseHTML: (el) => el.hasAttribute("data-spread"),
172
+ renderHTML: (attrs) => (attrs.spread ? { "data-spread": "" } : {}),
173
+ },
174
+ },
175
+ },
176
+ {
177
+ types: ["listItem"],
178
+ attributes: {
179
+ checked: {
180
+ default: null,
181
+ parseHTML: (el) => {
182
+ const v = el.getAttribute("data-checked");
183
+ return v === null ? null : v === "true";
184
+ },
185
+ renderHTML: (attrs) => typeof attrs.checked === "boolean"
186
+ ? { "data-checked": String(attrs.checked) }
187
+ : {},
188
+ },
189
+ },
190
+ },
191
+ ];
192
+ },
193
+ });
194
+ const footnoteAttributes = {
195
+ identifier: { default: "" },
196
+ label: { default: "" },
197
+ };
198
+ /** A GFM footnote definition, `[^label]: ...`. */
199
+ export const FootnoteDefinition = Node.create({
200
+ name: "footnoteDefinition",
201
+ group: "block",
202
+ content: "block+",
203
+ defining: true,
204
+ isolating: true,
205
+ addAttributes() {
206
+ return footnoteAttributes;
207
+ },
208
+ parseHTML() {
209
+ return [{ tag: "div[data-footnote-definition]" }];
210
+ },
211
+ renderHTML({ HTMLAttributes, node }) {
212
+ return [
213
+ "div",
214
+ mergeAttributes(HTMLAttributes, {
215
+ "data-footnote-definition": "",
216
+ "data-label": `[^${node.attrs.label}]`,
217
+ class: "v-footnote-definition",
218
+ }),
219
+ 0,
220
+ ];
221
+ },
222
+ });
223
+ /** A GFM footnote reference, `[^label]`. */
224
+ export const FootnoteReference = Node.create({
225
+ name: "footnoteReference",
226
+ group: "inline",
227
+ inline: true,
228
+ atom: true,
229
+ selectable: true,
230
+ addAttributes() {
231
+ return footnoteAttributes;
232
+ },
233
+ parseHTML() {
234
+ // Ahead of Superscript's `sup` rule, or a pasted reference turns into
235
+ // superscript text.
236
+ return [{ tag: "sup[data-footnote-reference]", priority: 60 }];
237
+ },
238
+ renderHTML({ HTMLAttributes, node }) {
239
+ return [
240
+ "sup",
241
+ mergeAttributes(HTMLAttributes, {
242
+ "data-footnote-reference": "",
243
+ class: "v-footnote-reference",
244
+ }),
245
+ `[^${node.attrs.label}]`,
246
+ ];
247
+ },
248
+ });