@thebes/cadmea 1.1.1 → 1.3.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.
@@ -0,0 +1,190 @@
1
+ import { createComponent, escape, ssr, ssrAttribute, ssrStyleProperty } from "solid-js/web";
2
+ import { For, Show, createSignal, onCleanup, onMount } from "solid-js";
3
+ import "@tiptap/core";
4
+ import "@tiptap/extension-image";
5
+ import "@tiptap/starter-kit";
6
+ //#region src/RichTextEditor.tsx
7
+ var _tmpl$ = "<span class=\"font-bold\">B</span>", _tmpl$2 = "<span class=\"italic\">I</span>", _tmpl$3 = "<span class=\"underline\">U</span>", _tmpl$4 = "<span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\"></span>", _tmpl$5 = [
8
+ "<div role=\"menu\" aria-label=\"Insert block\" class=\"bg-base-100 border-base-300 rounded-box absolute z-10 flex min-w-44 flex-col border p-1 shadow\" style=\"",
9
+ "\">",
10
+ "</div>"
11
+ ], _tmpl$6 = "<input type=\"file\" accept=\"image/*\" class=\"hidden\">", _tmpl$7 = [
12
+ "<div class=\"border-base-300 rounded-box border\"><div class=\"border-base-300 flex flex-wrap items-center gap-0.5 border-b p-1\">",
13
+ "",
14
+ "",
15
+ "",
16
+ "<span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\"></span>",
17
+ "",
18
+ "",
19
+ "",
20
+ "",
21
+ "",
22
+ "",
23
+ "</div><div class=\"relative\"><div",
24
+ " class=\"min-h-32 p-3\"></div>",
25
+ "</div>",
26
+ "</div>"
27
+ ], _tmpl$8 = [
28
+ "<button type=\"button\" role=\"menuitem\" class=\"",
29
+ "\">",
30
+ "</button>"
31
+ ], _tmpl$9 = [
32
+ "<button type=\"button\"",
33
+ " class=\"",
34
+ "\">",
35
+ "</button>"
36
+ ];
37
+ function filterSlashItems(items, query) {
38
+ const q = query.toLowerCase();
39
+ if (!q) return items;
40
+ return items.filter((it) => it.label.toLowerCase().includes(q) || it.keywords.includes(q));
41
+ }
42
+ function RichTextEditor(props) {
43
+ const [tick, setTick] = createSignal(0);
44
+ const [slash, setSlash] = createSignal(null);
45
+ const [activeIdx, setActiveIdx] = createSignal(0);
46
+ const slashItems = () => {
47
+ const items = [
48
+ {
49
+ label: "Heading 2",
50
+ keywords: "h2 title heading",
51
+ run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run()
52
+ },
53
+ {
54
+ label: "Heading 3",
55
+ keywords: "h3 subtitle heading",
56
+ run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run()
57
+ },
58
+ {
59
+ label: "Bullet list",
60
+ keywords: "ul unordered bullets",
61
+ run: (e) => e.chain().focus().toggleBulletList().run()
62
+ },
63
+ {
64
+ label: "Numbered list",
65
+ keywords: "ol ordered numbers",
66
+ run: (e) => e.chain().focus().toggleOrderedList().run()
67
+ },
68
+ {
69
+ label: "Quote",
70
+ keywords: "blockquote citation",
71
+ run: (e) => e.chain().focus().toggleBlockquote().run()
72
+ },
73
+ {
74
+ label: "Divider",
75
+ keywords: "hr rule separator line",
76
+ run: (e) => e.chain().focus().setHorizontalRule().run()
77
+ }
78
+ ];
79
+ if (props.onUploadFile) items.push({
80
+ label: "Image",
81
+ keywords: "img photo picture upload",
82
+ run: () => void 0
83
+ });
84
+ return items;
85
+ };
86
+ const filteredSlash = () => {
87
+ const s = slash();
88
+ return s ? filterSlashItems(slashItems(), s.query) : [];
89
+ };
90
+ onMount(() => {});
91
+ onCleanup(() => void 0);
92
+ const isActive = (name, attrs) => {
93
+ tick();
94
+ return Boolean(void 0);
95
+ };
96
+ function setLink() {}
97
+ return ssr(_tmpl$7, escape(createComponent(ToolbarButton, {
98
+ label: "Bold",
99
+ onClick: () => void 0,
100
+ active: () => isActive("bold"),
101
+ get children() {
102
+ return ssr(_tmpl$);
103
+ }
104
+ })), escape(createComponent(ToolbarButton, {
105
+ label: "Italic",
106
+ onClick: () => void 0,
107
+ active: () => isActive("italic"),
108
+ get children() {
109
+ return ssr(_tmpl$2);
110
+ }
111
+ })), escape(createComponent(ToolbarButton, {
112
+ label: "Underline",
113
+ onClick: () => void 0,
114
+ active: () => isActive("underline"),
115
+ get children() {
116
+ return ssr(_tmpl$3);
117
+ }
118
+ })), escape(createComponent(ToolbarButton, {
119
+ label: "Link",
120
+ onClick: setLink,
121
+ active: () => isActive("link"),
122
+ children: "Link"
123
+ })), escape(createComponent(ToolbarButton, {
124
+ label: "Heading 2",
125
+ onClick: () => void 0,
126
+ active: () => isActive("heading", { level: 2 }),
127
+ children: "H2"
128
+ })), escape(createComponent(ToolbarButton, {
129
+ label: "Heading 3",
130
+ onClick: () => void 0,
131
+ active: () => isActive("heading", { level: 3 }),
132
+ children: "H3"
133
+ })), escape(createComponent(ToolbarButton, {
134
+ label: "Bullet list",
135
+ onClick: () => void 0,
136
+ active: () => isActive("bulletList"),
137
+ children: "•"
138
+ })), escape(createComponent(ToolbarButton, {
139
+ label: "Numbered list",
140
+ onClick: () => void 0,
141
+ active: () => isActive("orderedList"),
142
+ children: "1."
143
+ })), escape(createComponent(ToolbarButton, {
144
+ label: "Quote",
145
+ onClick: () => void 0,
146
+ active: () => isActive("blockquote"),
147
+ children: "❝"
148
+ })), escape(createComponent(ToolbarButton, {
149
+ label: "Divider",
150
+ onClick: () => void 0,
151
+ children: "—"
152
+ })), escape(createComponent(Show, {
153
+ get when() {
154
+ return props.onUploadFile;
155
+ },
156
+ get children() {
157
+ return [ssr(_tmpl$4), createComponent(ToolbarButton, {
158
+ label: "Image",
159
+ onClick: () => void 0,
160
+ children: "Image"
161
+ })];
162
+ }
163
+ })), ssrAttribute("id", escape(props.id, true), false), escape(createComponent(Show, {
164
+ get when() {
165
+ return slash() && filteredSlash().length > 0;
166
+ },
167
+ get children() {
168
+ return ssr(_tmpl$5, ssrStyleProperty("left:", `${escape(slash()?.left ?? 0, true)}px`) + ssrStyleProperty(";top:", `${escape(slash()?.top ?? 0, true) + 4}px`), escape(createComponent(For, {
169
+ get each() {
170
+ return filteredSlash();
171
+ },
172
+ children: (item, i) => ssr(_tmpl$8, `rounded px-3 py-1.5 text-left text-sm ${i() === activeIdx() ? "bg-base-200" : ""}`, escape(item.label))
173
+ })));
174
+ }
175
+ })), escape(createComponent(Show, {
176
+ get when() {
177
+ return props.onUploadFile;
178
+ },
179
+ get children() {
180
+ return ssr(_tmpl$6);
181
+ }
182
+ })));
183
+ }
184
+ function ToolbarButton(props) {
185
+ return ssr(_tmpl$9, ssrAttribute("aria-label", escape(props.label, true), false) + ssrAttribute("aria-pressed", escape(props.active?.() ?? false, true), false) + ssrAttribute("title", escape(props.label, true), false), `btn btn-ghost btn-xs ${props.active?.() ?? false ? "btn-active" : ""}`, escape(props.children));
186
+ }
187
+ //#endregion
188
+ export { RichTextEditor };
189
+
190
+ //# sourceMappingURL=RichTextEditor-CPQTvhQD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RichTextEditor-CPQTvhQD.js","names":["Editor","Image","StarterKit","createSignal","For","JSX","onCleanup","onMount","Show","RichTextEditorProps","id","content","onChange","doc","onUploadFile","file","File","Promise","url","SlashItem","label","keywords","run","editor","matchSlashQuery","textBeforeCursor","m","match","filterSlashItems","items","query","q","toLowerCase","filter","it","includes","SlashState","from","to","left","top","RichTextEditor","props","container","HTMLDivElement","fileInput","HTMLInputElement","tick","setTick","bump","t","slash","setSlash","activeIdx","setActiveIdx","slashItems","e","chain","focus","toggleHeading","level","toggleBulletList","toggleOrderedList","toggleBlockquote","setHorizontalRule","push","click","filteredSlash","s","detectSlash","state","sel","selection","empty","$from","textBefore","parent","textBetween","parentOffset","undefined","start","coords","view","coordsAtPos","rect","getBoundingClientRect","bottom","runSlashItem","item","deleteRange","element","extensions","configure","link","openOnClick","editorProps","attributes","class","handleKeyDown","_view","event","key","i","Math","min","length","max","onUpdate","current","getJSON","onSelectionUpdate","destroy","isActive","name","attrs","Record","Boolean","setLink","prev","getAttributes","href","window","prompt","extendMarkRange","unsetLink","handleImageFile","Event","currentTarget","files","setImage","src","value","_$ssr","_tmpl$7","_$escape","_$createComponent","ToolbarButton","onClick","toggleBold","active","children","_tmpl$","toggleItalic","_tmpl$2","toggleUnderline","_tmpl$3","when","_tmpl$4","_$ssrAttribute","_tmpl$5","_$ssrStyleProperty","each","_tmpl$8","_tmpl$6","Element","_tmpl$9"],"sources":["../src/RichTextEditor.tsx"],"sourcesContent":["import { Editor } from \"@tiptap/core\";\nimport Image from \"@tiptap/extension-image\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport {\n createSignal,\n For,\n type JSX,\n onCleanup,\n onMount,\n Show,\n} from \"solid-js\";\n\nexport interface RichTextEditorProps {\n id?: string;\n /** TipTap's native JSON document shape — stored as-is, no transform layer. */\n content?: object;\n onChange: (doc: object) => void;\n /**\n * Resolves a picked image file to a stored URL (same contract as the form's\n * upload fields). When provided, the toolbar and slash menu expose an\n * \"Image\" insert; omitted, image insertion is hidden.\n */\n onUploadFile?: (file: File) => Promise<{ url: string }>;\n}\n\ninterface SlashItem {\n label: string;\n /** Extra search terms so e.g. \"ul\" finds \"Bullet list\". */\n keywords: string;\n run: (editor: Editor) => void;\n}\n\n// Pure: a slash menu opens only when the current block's text up to the\n// cursor is a bare `/` followed by an optional word (Notion/Ghost-style, at\n// the start of an empty-ish block). Returns the query (may be \"\") or null.\n// Exported for unit testing without a live ProseMirror view.\nexport function matchSlashQuery(textBeforeCursor: string): string | null {\n const m = textBeforeCursor.match(/^\\/(\\w*)$/);\n return m ? m[1] : null;\n}\n\n// Pure: filter slash items by label or keywords against a (lowercased) query.\nexport function filterSlashItems(\n items: SlashItem[],\n query: string,\n): SlashItem[] {\n const q = query.toLowerCase();\n if (!q) return items;\n return items.filter(\n (it) => it.label.toLowerCase().includes(q) || it.keywords.includes(q),\n );\n}\n\ninterface SlashState {\n from: number;\n to: number;\n query: string;\n left: number;\n top: number;\n}\n\n// No official Solid binding for TipTap exists, so this wraps @tiptap/core's\n// vanilla `Editor` class directly in Solid's onMount/onCleanup lifecycle —\n// per CLAUDE.md's preference for the framework-agnostic core API over an\n// unofficial community port. A persistent formatting toolbar (discoverable\n// for non-technical clients) plus a `/` slash menu for inserting blocks make\n// this a Ghost-like writing surface rather than a bare textarea. `content`\n// is only read once at mount, matching how the form's other fields init from\n// `initialValues` rather than reacting to later prop changes.\nexport function RichTextEditor(props: RichTextEditorProps) {\n let container: HTMLDivElement | undefined;\n let fileInput: HTMLInputElement | undefined;\n let editor: Editor | undefined;\n\n // Tiptap's Editor isn't reactive; bump a signal on every transaction so the\n // toolbar's active states (bold on/off, current heading…) re-render.\n const [tick, setTick] = createSignal(0);\n const bump = () => setTick((t) => t + 1);\n const [slash, setSlash] = createSignal<SlashState | null>(null);\n const [activeIdx, setActiveIdx] = createSignal(0);\n\n const slashItems = (): SlashItem[] => {\n const items: SlashItem[] = [\n {\n label: \"Heading 2\",\n keywords: \"h2 title heading\",\n run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(),\n },\n {\n label: \"Heading 3\",\n keywords: \"h3 subtitle heading\",\n run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(),\n },\n {\n label: \"Bullet list\",\n keywords: \"ul unordered bullets\",\n run: (e) => e.chain().focus().toggleBulletList().run(),\n },\n {\n label: \"Numbered list\",\n keywords: \"ol ordered numbers\",\n run: (e) => e.chain().focus().toggleOrderedList().run(),\n },\n {\n label: \"Quote\",\n keywords: \"blockquote citation\",\n run: (e) => e.chain().focus().toggleBlockquote().run(),\n },\n {\n label: \"Divider\",\n keywords: \"hr rule separator line\",\n run: (e) => e.chain().focus().setHorizontalRule().run(),\n },\n ];\n if (props.onUploadFile) {\n items.push({\n label: \"Image\",\n keywords: \"img photo picture upload\",\n run: () => fileInput?.click(),\n });\n }\n return items;\n };\n\n const filteredSlash = () => {\n const s = slash();\n return s ? filterSlashItems(slashItems(), s.query) : [];\n };\n\n // Re-evaluate whether a slash menu should be open after every selection or\n // doc change.\n function detectSlash() {\n if (!editor) return;\n const { state } = editor;\n const sel = state.selection;\n if (!sel.empty) {\n setSlash(null);\n return;\n }\n const $from = sel.$from;\n const textBefore = $from.parent.textBetween(\n 0,\n $from.parentOffset,\n undefined,\n \"\",\n );\n const query = matchSlashQuery(textBefore);\n if (query === null) {\n setSlash(null);\n return;\n }\n const to = sel.from;\n const from = $from.start();\n let left = 0;\n let top = 0;\n try {\n const coords = editor.view.coordsAtPos(to);\n const rect = container?.getBoundingClientRect();\n left = coords.left - (rect?.left ?? 0);\n top = coords.bottom - (rect?.top ?? 0);\n } catch {\n // coordsAtPos throws if layout isn't ready (e.g. jsdom) — fall back to\n // the top-left of the editor; the menu is still usable.\n }\n setSlash({ from, to, query, left, top });\n setActiveIdx(0);\n }\n\n function runSlashItem(item: SlashItem) {\n const s = slash();\n if (!s || !editor) return;\n // Drop the typed \"/query\" first, then run the block command at that spot.\n editor.chain().focus().deleteRange({ from: s.from, to: s.to }).run();\n item.run(editor);\n setSlash(null);\n }\n\n onMount(() => {\n if (!container) return;\n editor = new Editor({\n element: container,\n extensions: [\n StarterKit.configure({ link: { openOnClick: false } }),\n Image,\n ],\n content: props.content ?? \"\",\n editorProps: {\n attributes: { class: \"prose-site max-w-none focus:outline-none\" },\n // Drive slash-menu keyboard nav while it's open; let TipTap handle\n // everything else.\n handleKeyDown: (_view, event) => {\n if (!slash()) return false;\n const items = filteredSlash();\n if (event.key === \"ArrowDown\") {\n setActiveIdx((i) => Math.min(i + 1, items.length - 1));\n return true;\n }\n if (event.key === \"ArrowUp\") {\n setActiveIdx((i) => Math.max(i - 1, 0));\n return true;\n }\n if (event.key === \"Enter\") {\n const item = items[activeIdx()];\n if (item) {\n runSlashItem(item);\n return true;\n }\n }\n if (event.key === \"Escape\") {\n setSlash(null);\n return true;\n }\n return false;\n },\n },\n onUpdate: ({ editor: current }) => {\n props.onChange(current.getJSON());\n bump();\n detectSlash();\n },\n onSelectionUpdate: () => {\n bump();\n detectSlash();\n },\n });\n });\n\n onCleanup(() => editor?.destroy());\n\n // Reading `tick()` here subscribes the caller (each toolbar button's\n // `active` accessor) to editor transactions, so active states re-render.\n const isActive = (name: string, attrs?: Record<string, unknown>): boolean => {\n tick();\n return Boolean(editor?.isActive(name, attrs));\n };\n\n function setLink() {\n if (!editor) return;\n const prev = editor.getAttributes(\"link\").href as string | undefined;\n const url = window.prompt(\"Link URL\", prev ?? \"https://\");\n if (url === null) return;\n if (url === \"\") {\n editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n return;\n }\n editor.chain().focus().extendMarkRange(\"link\").setLink({ href: url }).run();\n }\n\n async function handleImageFile(\n e: Event & { currentTarget: HTMLInputElement },\n ) {\n const file = e.currentTarget.files?.[0];\n if (!file || !props.onUploadFile || !editor) return;\n try {\n const { url } = await props.onUploadFile(file);\n editor.chain().focus().setImage({ src: url }).run();\n } finally {\n e.currentTarget.value = \"\";\n }\n }\n\n return (\n <div class=\"border-base-300 rounded-box border\">\n <div class=\"border-base-300 flex flex-wrap items-center gap-0.5 border-b p-1\">\n <ToolbarButton\n label=\"Bold\"\n onClick={() => editor?.chain().focus().toggleBold().run()}\n active={() => isActive(\"bold\")}\n >\n <span class=\"font-bold\">B</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Italic\"\n onClick={() => editor?.chain().focus().toggleItalic().run()}\n active={() => isActive(\"italic\")}\n >\n <span class=\"italic\">I</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Underline\"\n onClick={() => editor?.chain().focus().toggleUnderline().run()}\n active={() => isActive(\"underline\")}\n >\n <span class=\"underline\">U</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Link\"\n onClick={setLink}\n active={() => isActive(\"link\")}\n >\n Link\n </ToolbarButton>\n <span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\" />\n <ToolbarButton\n label=\"Heading 2\"\n onClick={() =>\n editor?.chain().focus().toggleHeading({ level: 2 }).run()\n }\n active={() => isActive(\"heading\", { level: 2 })}\n >\n H2\n </ToolbarButton>\n <ToolbarButton\n label=\"Heading 3\"\n onClick={() =>\n editor?.chain().focus().toggleHeading({ level: 3 }).run()\n }\n active={() => isActive(\"heading\", { level: 3 })}\n >\n H3\n </ToolbarButton>\n <ToolbarButton\n label=\"Bullet list\"\n onClick={() => editor?.chain().focus().toggleBulletList().run()}\n active={() => isActive(\"bulletList\")}\n >\n •\n </ToolbarButton>\n <ToolbarButton\n label=\"Numbered list\"\n onClick={() => editor?.chain().focus().toggleOrderedList().run()}\n active={() => isActive(\"orderedList\")}\n >\n 1.\n </ToolbarButton>\n <ToolbarButton\n label=\"Quote\"\n onClick={() => editor?.chain().focus().toggleBlockquote().run()}\n active={() => isActive(\"blockquote\")}\n >\n ❝\n </ToolbarButton>\n <ToolbarButton\n label=\"Divider\"\n onClick={() => editor?.chain().focus().setHorizontalRule().run()}\n >\n —\n </ToolbarButton>\n <Show when={props.onUploadFile}>\n <span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\" />\n <ToolbarButton label=\"Image\" onClick={() => fileInput?.click()}>\n Image\n </ToolbarButton>\n </Show>\n </div>\n\n <div class=\"relative\">\n <div\n id={props.id}\n class=\"min-h-32 p-3\"\n ref={(el) => (container = el)}\n />\n <Show when={slash() && filteredSlash().length > 0}>\n <div\n role=\"menu\"\n aria-label=\"Insert block\"\n class=\"bg-base-100 border-base-300 rounded-box absolute z-10 flex min-w-44 flex-col border p-1 shadow\"\n style={{\n left: `${slash()?.left ?? 0}px`,\n top: `${(slash()?.top ?? 0) + 4}px`,\n }}\n >\n <For each={filteredSlash()}>\n {(item, i) => (\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"rounded px-3 py-1.5 text-left text-sm\"\n classList={{ \"bg-base-200\": i() === activeIdx() }}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => runSlashItem(item)}\n >\n {item.label}\n </button>\n )}\n </For>\n </div>\n </Show>\n </div>\n\n <Show when={props.onUploadFile}>\n <input\n ref={(el) => (fileInput = el)}\n type=\"file\"\n accept=\"image/*\"\n class=\"hidden\"\n onChange={handleImageFile}\n />\n </Show>\n </div>\n );\n}\n\n// A toolbar button. `active` is an accessor so Solid re-tracks it on every\n// editor transaction (via the `tick` signal isActive reads).\nfunction ToolbarButton(props: {\n label: string;\n active?: () => boolean;\n onClick: () => void;\n children: JSX.Element;\n}): JSX.Element {\n return (\n <button\n type=\"button\"\n aria-label={props.label}\n aria-pressed={props.active?.() ?? false}\n title={props.label}\n class=\"btn btn-ghost btn-xs\"\n classList={{ \"btn-active\": props.active?.() ?? false }}\n // Keep the editor selection while clicking the toolbar.\n onMouseDown={(e) => e.preventDefault()}\n onClick={props.onClick}\n >\n {props.children}\n </button>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,334 @@
1
+ import { addEventListener, createComponent, delegateEvents, effect, insert, memo, setAttribute, setStyleProperty, template, use } from "solid-js/web";
2
+ import { For, Show, createSignal, onCleanup, onMount } from "solid-js";
3
+ import { Editor } from "@tiptap/core";
4
+ import Image from "@tiptap/extension-image";
5
+ import StarterKit from "@tiptap/starter-kit";
6
+ //#region src/RichTextEditor.tsx
7
+ var _tmpl$ = /*#__PURE__*/ template(`<span class=font-bold>B`), _tmpl$2 = /*#__PURE__*/ template(`<span class=italic>I`), _tmpl$3 = /*#__PURE__*/ template(`<span class=underline>U`), _tmpl$4 = /*#__PURE__*/ template(`<span class="bg-base-300 mx-1 h-4 w-px"aria-hidden=true>`), _tmpl$5 = /*#__PURE__*/ template(`<div role=menu aria-label="Insert block"class="bg-base-100 border-base-300 rounded-box absolute z-10 flex min-w-44 flex-col border p-1 shadow">`), _tmpl$6 = /*#__PURE__*/ template(`<input type=file accept=image/* class=hidden>`), _tmpl$7 = /*#__PURE__*/ template(`<div class="border-base-300 rounded-box border"><div class="border-base-300 flex flex-wrap items-center gap-0.5 border-b p-1"><span class="bg-base-300 mx-1 h-4 w-px"aria-hidden=true></span></div><div class=relative><div class="min-h-32 p-3">`), _tmpl$8 = /*#__PURE__*/ template(`<button type=button role=menuitem class="rounded px-3 py-1.5 text-left text-sm">`), _tmpl$9 = /*#__PURE__*/ template(`<button type=button class="btn btn-ghost btn-xs">`);
8
+ function matchSlashQuery(textBeforeCursor) {
9
+ const m = textBeforeCursor.match(/^\/(\w*)$/);
10
+ return m ? m[1] : null;
11
+ }
12
+ function filterSlashItems(items, query) {
13
+ const q = query.toLowerCase();
14
+ if (!q) return items;
15
+ return items.filter((it) => it.label.toLowerCase().includes(q) || it.keywords.includes(q));
16
+ }
17
+ function RichTextEditor(props) {
18
+ let container;
19
+ let fileInput;
20
+ let editor;
21
+ const [tick, setTick] = createSignal(0);
22
+ const bump = () => setTick((t) => t + 1);
23
+ const [slash, setSlash] = createSignal(null);
24
+ const [activeIdx, setActiveIdx] = createSignal(0);
25
+ const slashItems = () => {
26
+ const items = [
27
+ {
28
+ label: "Heading 2",
29
+ keywords: "h2 title heading",
30
+ run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run()
31
+ },
32
+ {
33
+ label: "Heading 3",
34
+ keywords: "h3 subtitle heading",
35
+ run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run()
36
+ },
37
+ {
38
+ label: "Bullet list",
39
+ keywords: "ul unordered bullets",
40
+ run: (e) => e.chain().focus().toggleBulletList().run()
41
+ },
42
+ {
43
+ label: "Numbered list",
44
+ keywords: "ol ordered numbers",
45
+ run: (e) => e.chain().focus().toggleOrderedList().run()
46
+ },
47
+ {
48
+ label: "Quote",
49
+ keywords: "blockquote citation",
50
+ run: (e) => e.chain().focus().toggleBlockquote().run()
51
+ },
52
+ {
53
+ label: "Divider",
54
+ keywords: "hr rule separator line",
55
+ run: (e) => e.chain().focus().setHorizontalRule().run()
56
+ }
57
+ ];
58
+ if (props.onUploadFile) items.push({
59
+ label: "Image",
60
+ keywords: "img photo picture upload",
61
+ run: () => fileInput?.click()
62
+ });
63
+ return items;
64
+ };
65
+ const filteredSlash = () => {
66
+ const s = slash();
67
+ return s ? filterSlashItems(slashItems(), s.query) : [];
68
+ };
69
+ function detectSlash() {
70
+ if (!editor) return;
71
+ const { state } = editor;
72
+ const sel = state.selection;
73
+ if (!sel.empty) {
74
+ setSlash(null);
75
+ return;
76
+ }
77
+ const $from = sel.$from;
78
+ const query = matchSlashQuery($from.parent.textBetween(0, $from.parentOffset, void 0, ""));
79
+ if (query === null) {
80
+ setSlash(null);
81
+ return;
82
+ }
83
+ const to = sel.from;
84
+ const from = $from.start();
85
+ let left = 0;
86
+ let top = 0;
87
+ try {
88
+ const coords = editor.view.coordsAtPos(to);
89
+ const rect = container?.getBoundingClientRect();
90
+ left = coords.left - (rect?.left ?? 0);
91
+ top = coords.bottom - (rect?.top ?? 0);
92
+ } catch {}
93
+ setSlash({
94
+ from,
95
+ to,
96
+ query,
97
+ left,
98
+ top
99
+ });
100
+ setActiveIdx(0);
101
+ }
102
+ function runSlashItem(item) {
103
+ const s = slash();
104
+ if (!s || !editor) return;
105
+ editor.chain().focus().deleteRange({
106
+ from: s.from,
107
+ to: s.to
108
+ }).run();
109
+ item.run(editor);
110
+ setSlash(null);
111
+ }
112
+ onMount(() => {
113
+ if (!container) return;
114
+ editor = new Editor({
115
+ element: container,
116
+ extensions: [StarterKit.configure({ link: { openOnClick: false } }), Image],
117
+ content: props.content ?? "",
118
+ editorProps: {
119
+ attributes: { class: "prose-site max-w-none focus:outline-none" },
120
+ handleKeyDown: (_view, event) => {
121
+ if (!slash()) return false;
122
+ const items = filteredSlash();
123
+ if (event.key === "ArrowDown") {
124
+ setActiveIdx((i) => Math.min(i + 1, items.length - 1));
125
+ return true;
126
+ }
127
+ if (event.key === "ArrowUp") {
128
+ setActiveIdx((i) => Math.max(i - 1, 0));
129
+ return true;
130
+ }
131
+ if (event.key === "Enter") {
132
+ const item = items[activeIdx()];
133
+ if (item) {
134
+ runSlashItem(item);
135
+ return true;
136
+ }
137
+ }
138
+ if (event.key === "Escape") {
139
+ setSlash(null);
140
+ return true;
141
+ }
142
+ return false;
143
+ }
144
+ },
145
+ onUpdate: ({ editor: current }) => {
146
+ props.onChange(current.getJSON());
147
+ bump();
148
+ detectSlash();
149
+ },
150
+ onSelectionUpdate: () => {
151
+ bump();
152
+ detectSlash();
153
+ }
154
+ });
155
+ });
156
+ onCleanup(() => editor?.destroy());
157
+ const isActive = (name, attrs) => {
158
+ tick();
159
+ return Boolean(editor?.isActive(name, attrs));
160
+ };
161
+ function setLink() {
162
+ if (!editor) return;
163
+ const prev = editor.getAttributes("link").href;
164
+ const url = window.prompt("Link URL", prev ?? "https://");
165
+ if (url === null) return;
166
+ if (url === "") {
167
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
168
+ return;
169
+ }
170
+ editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
171
+ }
172
+ async function handleImageFile(e) {
173
+ const file = e.currentTarget.files?.[0];
174
+ if (!file || !props.onUploadFile || !editor) return;
175
+ try {
176
+ const { url } = await props.onUploadFile(file);
177
+ editor.chain().focus().setImage({ src: url }).run();
178
+ } finally {
179
+ e.currentTarget.value = "";
180
+ }
181
+ }
182
+ return (() => {
183
+ var _el$ = _tmpl$7(), _el$2 = _el$.firstChild, _el$6 = _el$2.firstChild, _el$8 = _el$2.nextSibling, _el$9 = _el$8.firstChild;
184
+ insert(_el$2, createComponent(ToolbarButton, {
185
+ label: "Bold",
186
+ onClick: () => editor?.chain().focus().toggleBold().run(),
187
+ active: () => isActive("bold"),
188
+ get children() {
189
+ return _tmpl$();
190
+ }
191
+ }), _el$6);
192
+ insert(_el$2, createComponent(ToolbarButton, {
193
+ label: "Italic",
194
+ onClick: () => editor?.chain().focus().toggleItalic().run(),
195
+ active: () => isActive("italic"),
196
+ get children() {
197
+ return _tmpl$2();
198
+ }
199
+ }), _el$6);
200
+ insert(_el$2, createComponent(ToolbarButton, {
201
+ label: "Underline",
202
+ onClick: () => editor?.chain().focus().toggleUnderline().run(),
203
+ active: () => isActive("underline"),
204
+ get children() {
205
+ return _tmpl$3();
206
+ }
207
+ }), _el$6);
208
+ insert(_el$2, createComponent(ToolbarButton, {
209
+ label: "Link",
210
+ onClick: setLink,
211
+ active: () => isActive("link"),
212
+ children: "Link"
213
+ }), _el$6);
214
+ insert(_el$2, createComponent(ToolbarButton, {
215
+ label: "Heading 2",
216
+ onClick: () => editor?.chain().focus().toggleHeading({ level: 2 }).run(),
217
+ active: () => isActive("heading", { level: 2 }),
218
+ children: "H2"
219
+ }), null);
220
+ insert(_el$2, createComponent(ToolbarButton, {
221
+ label: "Heading 3",
222
+ onClick: () => editor?.chain().focus().toggleHeading({ level: 3 }).run(),
223
+ active: () => isActive("heading", { level: 3 }),
224
+ children: "H3"
225
+ }), null);
226
+ insert(_el$2, createComponent(ToolbarButton, {
227
+ label: "Bullet list",
228
+ onClick: () => editor?.chain().focus().toggleBulletList().run(),
229
+ active: () => isActive("bulletList"),
230
+ children: "•"
231
+ }), null);
232
+ insert(_el$2, createComponent(ToolbarButton, {
233
+ label: "Numbered list",
234
+ onClick: () => editor?.chain().focus().toggleOrderedList().run(),
235
+ active: () => isActive("orderedList"),
236
+ children: "1."
237
+ }), null);
238
+ insert(_el$2, createComponent(ToolbarButton, {
239
+ label: "Quote",
240
+ onClick: () => editor?.chain().focus().toggleBlockquote().run(),
241
+ active: () => isActive("blockquote"),
242
+ children: "❝"
243
+ }), null);
244
+ insert(_el$2, createComponent(ToolbarButton, {
245
+ label: "Divider",
246
+ onClick: () => editor?.chain().focus().setHorizontalRule().run(),
247
+ children: "—"
248
+ }), null);
249
+ insert(_el$2, createComponent(Show, {
250
+ get when() {
251
+ return props.onUploadFile;
252
+ },
253
+ get children() {
254
+ return [_tmpl$4(), createComponent(ToolbarButton, {
255
+ label: "Image",
256
+ onClick: () => fileInput?.click(),
257
+ children: "Image"
258
+ })];
259
+ }
260
+ }), null);
261
+ use((el) => container = el, _el$9);
262
+ insert(_el$8, createComponent(Show, {
263
+ get when() {
264
+ return memo(() => !!slash())() && filteredSlash().length > 0;
265
+ },
266
+ get children() {
267
+ var _el$0 = _tmpl$5();
268
+ insert(_el$0, createComponent(For, {
269
+ get each() {
270
+ return filteredSlash();
271
+ },
272
+ children: (item, i) => (() => {
273
+ var _el$10 = _tmpl$8();
274
+ _el$10.$$click = () => runSlashItem(item);
275
+ _el$10.$$mousedown = (e) => e.preventDefault();
276
+ insert(_el$10, () => item.label);
277
+ effect(() => _el$10.classList.toggle("bg-base-200", !!(i() === activeIdx())));
278
+ return _el$10;
279
+ })()
280
+ }));
281
+ effect((_p$) => {
282
+ var _v$ = `${slash()?.left ?? 0}px`, _v$2 = `${(slash()?.top ?? 0) + 4}px`;
283
+ _v$ !== _p$.e && setStyleProperty(_el$0, "left", _p$.e = _v$);
284
+ _v$2 !== _p$.t && setStyleProperty(_el$0, "top", _p$.t = _v$2);
285
+ return _p$;
286
+ }, {
287
+ e: void 0,
288
+ t: void 0
289
+ });
290
+ return _el$0;
291
+ }
292
+ }), null);
293
+ insert(_el$, createComponent(Show, {
294
+ get when() {
295
+ return props.onUploadFile;
296
+ },
297
+ get children() {
298
+ var _el$1 = _tmpl$6();
299
+ _el$1.addEventListener("change", handleImageFile);
300
+ use((el) => fileInput = el, _el$1);
301
+ return _el$1;
302
+ }
303
+ }), null);
304
+ effect(() => setAttribute(_el$9, "id", props.id));
305
+ return _el$;
306
+ })();
307
+ }
308
+ function ToolbarButton(props) {
309
+ return (() => {
310
+ var _el$11 = _tmpl$9();
311
+ addEventListener(_el$11, "click", props.onClick, true);
312
+ _el$11.$$mousedown = (e) => e.preventDefault();
313
+ insert(_el$11, () => props.children);
314
+ effect((_p$) => {
315
+ var _v$3 = props.label, _v$4 = props.active?.() ?? false, _v$5 = props.label, _v$6 = !!(props.active?.() ?? false);
316
+ _v$3 !== _p$.e && setAttribute(_el$11, "aria-label", _p$.e = _v$3);
317
+ _v$4 !== _p$.t && setAttribute(_el$11, "aria-pressed", _p$.t = _v$4);
318
+ _v$5 !== _p$.a && setAttribute(_el$11, "title", _p$.a = _v$5);
319
+ _v$6 !== _p$.o && _el$11.classList.toggle("btn-active", _p$.o = _v$6);
320
+ return _p$;
321
+ }, {
322
+ e: void 0,
323
+ t: void 0,
324
+ a: void 0,
325
+ o: void 0
326
+ });
327
+ return _el$11;
328
+ })();
329
+ }
330
+ delegateEvents(["mousedown", "click"]);
331
+ //#endregion
332
+ export { RichTextEditor };
333
+
334
+ //# sourceMappingURL=RichTextEditor-ComcBFfl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RichTextEditor-ComcBFfl.js","names":["Editor","Image","StarterKit","createSignal","For","JSX","onCleanup","onMount","Show","RichTextEditorProps","id","content","onChange","doc","onUploadFile","file","File","Promise","url","SlashItem","label","keywords","run","editor","matchSlashQuery","textBeforeCursor","m","match","filterSlashItems","items","query","q","toLowerCase","filter","it","includes","SlashState","from","to","left","top","RichTextEditor","props","container","HTMLDivElement","fileInput","HTMLInputElement","tick","setTick","bump","t","slash","setSlash","activeIdx","setActiveIdx","slashItems","e","chain","focus","toggleHeading","level","toggleBulletList","toggleOrderedList","toggleBlockquote","setHorizontalRule","push","click","filteredSlash","s","detectSlash","state","sel","selection","empty","$from","textBefore","parent","textBetween","parentOffset","undefined","start","coords","view","coordsAtPos","rect","getBoundingClientRect","bottom","runSlashItem","item","deleteRange","element","extensions","configure","link","openOnClick","editorProps","attributes","class","handleKeyDown","_view","event","key","i","Math","min","length","max","onUpdate","current","getJSON","onSelectionUpdate","destroy","isActive","name","attrs","Record","Boolean","setLink","prev","getAttributes","href","window","prompt","extendMarkRange","unsetLink","handleImageFile","Event","currentTarget","files","setImage","src","value","_el$","_tmpl$7","_el$2","firstChild","_el$6","_el$8","nextSibling","_el$9","_$insert","_$createComponent","ToolbarButton","onClick","toggleBold","active","children","_tmpl$","toggleItalic","_tmpl$2","toggleUnderline","_tmpl$3","when","_tmpl$4","_$use","el","_$memo","_el$0","_tmpl$5","each","_el$10","_tmpl$8","$$click","$$mousedown","preventDefault","_$effect","classList","toggle","_p$","_v$","_v$2","_$setStyleProperty","_el$1","_tmpl$6","addEventListener","_$setAttribute","Element","_el$11","_tmpl$9","_$addEventListener","_v$3","_v$4","_v$5","_v$6","a","o","_$delegateEvents"],"sources":["../src/RichTextEditor.tsx"],"sourcesContent":["import { Editor } from \"@tiptap/core\";\nimport Image from \"@tiptap/extension-image\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport {\n createSignal,\n For,\n type JSX,\n onCleanup,\n onMount,\n Show,\n} from \"solid-js\";\n\nexport interface RichTextEditorProps {\n id?: string;\n /** TipTap's native JSON document shape — stored as-is, no transform layer. */\n content?: object;\n onChange: (doc: object) => void;\n /**\n * Resolves a picked image file to a stored URL (same contract as the form's\n * upload fields). When provided, the toolbar and slash menu expose an\n * \"Image\" insert; omitted, image insertion is hidden.\n */\n onUploadFile?: (file: File) => Promise<{ url: string }>;\n}\n\ninterface SlashItem {\n label: string;\n /** Extra search terms so e.g. \"ul\" finds \"Bullet list\". */\n keywords: string;\n run: (editor: Editor) => void;\n}\n\n// Pure: a slash menu opens only when the current block's text up to the\n// cursor is a bare `/` followed by an optional word (Notion/Ghost-style, at\n// the start of an empty-ish block). Returns the query (may be \"\") or null.\n// Exported for unit testing without a live ProseMirror view.\nexport function matchSlashQuery(textBeforeCursor: string): string | null {\n const m = textBeforeCursor.match(/^\\/(\\w*)$/);\n return m ? m[1] : null;\n}\n\n// Pure: filter slash items by label or keywords against a (lowercased) query.\nexport function filterSlashItems(\n items: SlashItem[],\n query: string,\n): SlashItem[] {\n const q = query.toLowerCase();\n if (!q) return items;\n return items.filter(\n (it) => it.label.toLowerCase().includes(q) || it.keywords.includes(q),\n );\n}\n\ninterface SlashState {\n from: number;\n to: number;\n query: string;\n left: number;\n top: number;\n}\n\n// No official Solid binding for TipTap exists, so this wraps @tiptap/core's\n// vanilla `Editor` class directly in Solid's onMount/onCleanup lifecycle —\n// per CLAUDE.md's preference for the framework-agnostic core API over an\n// unofficial community port. A persistent formatting toolbar (discoverable\n// for non-technical clients) plus a `/` slash menu for inserting blocks make\n// this a Ghost-like writing surface rather than a bare textarea. `content`\n// is only read once at mount, matching how the form's other fields init from\n// `initialValues` rather than reacting to later prop changes.\nexport function RichTextEditor(props: RichTextEditorProps) {\n let container: HTMLDivElement | undefined;\n let fileInput: HTMLInputElement | undefined;\n let editor: Editor | undefined;\n\n // Tiptap's Editor isn't reactive; bump a signal on every transaction so the\n // toolbar's active states (bold on/off, current heading…) re-render.\n const [tick, setTick] = createSignal(0);\n const bump = () => setTick((t) => t + 1);\n const [slash, setSlash] = createSignal<SlashState | null>(null);\n const [activeIdx, setActiveIdx] = createSignal(0);\n\n const slashItems = (): SlashItem[] => {\n const items: SlashItem[] = [\n {\n label: \"Heading 2\",\n keywords: \"h2 title heading\",\n run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(),\n },\n {\n label: \"Heading 3\",\n keywords: \"h3 subtitle heading\",\n run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(),\n },\n {\n label: \"Bullet list\",\n keywords: \"ul unordered bullets\",\n run: (e) => e.chain().focus().toggleBulletList().run(),\n },\n {\n label: \"Numbered list\",\n keywords: \"ol ordered numbers\",\n run: (e) => e.chain().focus().toggleOrderedList().run(),\n },\n {\n label: \"Quote\",\n keywords: \"blockquote citation\",\n run: (e) => e.chain().focus().toggleBlockquote().run(),\n },\n {\n label: \"Divider\",\n keywords: \"hr rule separator line\",\n run: (e) => e.chain().focus().setHorizontalRule().run(),\n },\n ];\n if (props.onUploadFile) {\n items.push({\n label: \"Image\",\n keywords: \"img photo picture upload\",\n run: () => fileInput?.click(),\n });\n }\n return items;\n };\n\n const filteredSlash = () => {\n const s = slash();\n return s ? filterSlashItems(slashItems(), s.query) : [];\n };\n\n // Re-evaluate whether a slash menu should be open after every selection or\n // doc change.\n function detectSlash() {\n if (!editor) return;\n const { state } = editor;\n const sel = state.selection;\n if (!sel.empty) {\n setSlash(null);\n return;\n }\n const $from = sel.$from;\n const textBefore = $from.parent.textBetween(\n 0,\n $from.parentOffset,\n undefined,\n \"\",\n );\n const query = matchSlashQuery(textBefore);\n if (query === null) {\n setSlash(null);\n return;\n }\n const to = sel.from;\n const from = $from.start();\n let left = 0;\n let top = 0;\n try {\n const coords = editor.view.coordsAtPos(to);\n const rect = container?.getBoundingClientRect();\n left = coords.left - (rect?.left ?? 0);\n top = coords.bottom - (rect?.top ?? 0);\n } catch {\n // coordsAtPos throws if layout isn't ready (e.g. jsdom) — fall back to\n // the top-left of the editor; the menu is still usable.\n }\n setSlash({ from, to, query, left, top });\n setActiveIdx(0);\n }\n\n function runSlashItem(item: SlashItem) {\n const s = slash();\n if (!s || !editor) return;\n // Drop the typed \"/query\" first, then run the block command at that spot.\n editor.chain().focus().deleteRange({ from: s.from, to: s.to }).run();\n item.run(editor);\n setSlash(null);\n }\n\n onMount(() => {\n if (!container) return;\n editor = new Editor({\n element: container,\n extensions: [\n StarterKit.configure({ link: { openOnClick: false } }),\n Image,\n ],\n content: props.content ?? \"\",\n editorProps: {\n attributes: { class: \"prose-site max-w-none focus:outline-none\" },\n // Drive slash-menu keyboard nav while it's open; let TipTap handle\n // everything else.\n handleKeyDown: (_view, event) => {\n if (!slash()) return false;\n const items = filteredSlash();\n if (event.key === \"ArrowDown\") {\n setActiveIdx((i) => Math.min(i + 1, items.length - 1));\n return true;\n }\n if (event.key === \"ArrowUp\") {\n setActiveIdx((i) => Math.max(i - 1, 0));\n return true;\n }\n if (event.key === \"Enter\") {\n const item = items[activeIdx()];\n if (item) {\n runSlashItem(item);\n return true;\n }\n }\n if (event.key === \"Escape\") {\n setSlash(null);\n return true;\n }\n return false;\n },\n },\n onUpdate: ({ editor: current }) => {\n props.onChange(current.getJSON());\n bump();\n detectSlash();\n },\n onSelectionUpdate: () => {\n bump();\n detectSlash();\n },\n });\n });\n\n onCleanup(() => editor?.destroy());\n\n // Reading `tick()` here subscribes the caller (each toolbar button's\n // `active` accessor) to editor transactions, so active states re-render.\n const isActive = (name: string, attrs?: Record<string, unknown>): boolean => {\n tick();\n return Boolean(editor?.isActive(name, attrs));\n };\n\n function setLink() {\n if (!editor) return;\n const prev = editor.getAttributes(\"link\").href as string | undefined;\n const url = window.prompt(\"Link URL\", prev ?? \"https://\");\n if (url === null) return;\n if (url === \"\") {\n editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n return;\n }\n editor.chain().focus().extendMarkRange(\"link\").setLink({ href: url }).run();\n }\n\n async function handleImageFile(\n e: Event & { currentTarget: HTMLInputElement },\n ) {\n const file = e.currentTarget.files?.[0];\n if (!file || !props.onUploadFile || !editor) return;\n try {\n const { url } = await props.onUploadFile(file);\n editor.chain().focus().setImage({ src: url }).run();\n } finally {\n e.currentTarget.value = \"\";\n }\n }\n\n return (\n <div class=\"border-base-300 rounded-box border\">\n <div class=\"border-base-300 flex flex-wrap items-center gap-0.5 border-b p-1\">\n <ToolbarButton\n label=\"Bold\"\n onClick={() => editor?.chain().focus().toggleBold().run()}\n active={() => isActive(\"bold\")}\n >\n <span class=\"font-bold\">B</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Italic\"\n onClick={() => editor?.chain().focus().toggleItalic().run()}\n active={() => isActive(\"italic\")}\n >\n <span class=\"italic\">I</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Underline\"\n onClick={() => editor?.chain().focus().toggleUnderline().run()}\n active={() => isActive(\"underline\")}\n >\n <span class=\"underline\">U</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Link\"\n onClick={setLink}\n active={() => isActive(\"link\")}\n >\n Link\n </ToolbarButton>\n <span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\" />\n <ToolbarButton\n label=\"Heading 2\"\n onClick={() =>\n editor?.chain().focus().toggleHeading({ level: 2 }).run()\n }\n active={() => isActive(\"heading\", { level: 2 })}\n >\n H2\n </ToolbarButton>\n <ToolbarButton\n label=\"Heading 3\"\n onClick={() =>\n editor?.chain().focus().toggleHeading({ level: 3 }).run()\n }\n active={() => isActive(\"heading\", { level: 3 })}\n >\n H3\n </ToolbarButton>\n <ToolbarButton\n label=\"Bullet list\"\n onClick={() => editor?.chain().focus().toggleBulletList().run()}\n active={() => isActive(\"bulletList\")}\n >\n •\n </ToolbarButton>\n <ToolbarButton\n label=\"Numbered list\"\n onClick={() => editor?.chain().focus().toggleOrderedList().run()}\n active={() => isActive(\"orderedList\")}\n >\n 1.\n </ToolbarButton>\n <ToolbarButton\n label=\"Quote\"\n onClick={() => editor?.chain().focus().toggleBlockquote().run()}\n active={() => isActive(\"blockquote\")}\n >\n ❝\n </ToolbarButton>\n <ToolbarButton\n label=\"Divider\"\n onClick={() => editor?.chain().focus().setHorizontalRule().run()}\n >\n —\n </ToolbarButton>\n <Show when={props.onUploadFile}>\n <span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\" />\n <ToolbarButton label=\"Image\" onClick={() => fileInput?.click()}>\n Image\n </ToolbarButton>\n </Show>\n </div>\n\n <div class=\"relative\">\n <div\n id={props.id}\n class=\"min-h-32 p-3\"\n ref={(el) => (container = el)}\n />\n <Show when={slash() && filteredSlash().length > 0}>\n <div\n role=\"menu\"\n aria-label=\"Insert block\"\n class=\"bg-base-100 border-base-300 rounded-box absolute z-10 flex min-w-44 flex-col border p-1 shadow\"\n style={{\n left: `${slash()?.left ?? 0}px`,\n top: `${(slash()?.top ?? 0) + 4}px`,\n }}\n >\n <For each={filteredSlash()}>\n {(item, i) => (\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"rounded px-3 py-1.5 text-left text-sm\"\n classList={{ \"bg-base-200\": i() === activeIdx() }}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => runSlashItem(item)}\n >\n {item.label}\n </button>\n )}\n </For>\n </div>\n </Show>\n </div>\n\n <Show when={props.onUploadFile}>\n <input\n ref={(el) => (fileInput = el)}\n type=\"file\"\n accept=\"image/*\"\n class=\"hidden\"\n onChange={handleImageFile}\n />\n </Show>\n </div>\n );\n}\n\n// A toolbar button. `active` is an accessor so Solid re-tracks it on every\n// editor transaction (via the `tick` signal isActive reads).\nfunction ToolbarButton(props: {\n label: string;\n active?: () => boolean;\n onClick: () => void;\n children: JSX.Element;\n}): JSX.Element {\n return (\n <button\n type=\"button\"\n aria-label={props.label}\n aria-pressed={props.active?.() ?? false}\n title={props.label}\n class=\"btn btn-ghost btn-xs\"\n classList={{ \"btn-active\": props.active?.() ?? false }}\n // Keep the editor selection while clicking the toolbar.\n onMouseDown={(e) => e.preventDefault()}\n onClick={props.onClick}\n >\n {props.children}\n </button>\n );\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { JSX } from "solid-js";
2
1
  import { CollectionConfig, EditRef } from "@thebes/cadmus/cms";
2
+ import { JSX } from "solid-js";
3
3
  import { ImageCrop, ImageHotspot } from "@thebes/cadmus/storage";
4
4
 
5
5
  //#region src/capabilities.d.ts
@@ -107,11 +107,10 @@ interface CollectionEditProps {
107
107
  url: string;
108
108
  }>;
109
109
  /**
110
- * Options for `relationship` fields (hasMany:false only — see
111
- * RelationshipFieldConfig's `hasMany` caveat), keyed by the field's
112
- * `relationTo` collection slug. `CollectionEdit` can't query another
113
- * collection itself, so the consuming route fetches the related rows
114
- * and passes them in.
110
+ * Options for `relationship` fields (both single and `hasMany`), keyed by
111
+ * the field's `relationTo` collection slug. `CollectionEdit` can't query
112
+ * another collection itself, so the consuming route fetches the related
113
+ * rows and passes them in; the field renders them as a searchable combobox.
115
114
  */
116
115
  relationshipOptions?: Partial<Record<string, RelationshipOption[]>>;
117
116
  /**
@@ -140,10 +139,11 @@ interface CollectionEditProps {
140
139
  declare function CollectionEdit(props: CollectionEditProps): JSX.Element;
141
140
  //#endregion
142
141
  //#region src/CollectionList.d.ts
142
+ type Row = Record<string, unknown>;
143
143
  interface CollectionListProps {
144
144
  config: CollectionConfig;
145
- rows: Record<string, unknown>[];
146
- onRowClick?: (row: Record<string, unknown>) => void;
145
+ rows: Row[];
146
+ onRowClick?: (row: Row) => void;
147
147
  /**
148
148
  * 1-based current page. Omit (along with `pageSize`) to render without
149
149
  * the pagination bar entirely — list views with no `find()` paging