@pilotiq/tiptap 3.2.1 → 3.3.1

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,30 @@
1
+ import React from 'react';
2
+ import { type MarkdownEditorProps } from '@pilotiq/pilotiq/react';
3
+ /**
4
+ * Plug-in WYSIWYG markdown editor registered with pilotiq core's
5
+ * `registerMarkdownEditor()`. Replaces the legacy textarea + manual-toolbar
6
+ * UI with a real rich editor; serializes to markdown on every change via
7
+ * `tiptap-markdown` so the wire format stays a plain markdown string under
8
+ * the field name.
9
+ *
10
+ * Collab-aware: when a `<RecordCollabRoom>` is mounted up-tree AND
11
+ * `registerCollabExtensions()` ran (both shipped by `@pilotiq-pro/collab`),
12
+ * the editor binds to the room's shared `Y.XmlFragment` via Tiptap's
13
+ * `Collaboration` extension. Every peer mounts the same editor against the
14
+ * same fragment; markdown serialization runs locally per peer so only the
15
+ * ProseMirror tree crosses the wire.
16
+ *
17
+ * Tabs (top-right):
18
+ * - **Editor** (default) — WYSIWYG.
19
+ * - **Source** — raw markdown textarea; on switch back to Editor the editor
20
+ * parses the textarea contents (round-trips through tiptap-markdown).
21
+ * - **Preview** — read-only render of the current markdown via the editor's
22
+ * own HTML output. Same view a user would see on the public site if the
23
+ * resource ships a read-side renderer.
24
+ *
25
+ * Single-source-of-truth posture: the editor's `onUpdate` is the canonical
26
+ * write path. Source-tab edits flow back through the editor on tab-switch
27
+ * (no dual state, no drift between source and editor doc).
28
+ */
29
+ export declare function MarkdownEditor({ name, defaultValue, placeholder, disabled, onChange, onBlur, toolbarButtons, minHeight, maxHeight, fileAttachmentsDirectory, fileAttachmentsVisibility, uploadUrl, }: MarkdownEditorProps): React.ReactElement | null;
30
+ //# sourceMappingURL=MarkdownEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownEditor.d.ts","sourceRoot":"","sources":["../../src/react/MarkdownEditor.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAA;AAQnE,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,wBAAwB,CAAA;AAuG/B;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,QAAgB,EAChB,QAAQ,EACR,MAAM,EACN,cAAc,EACd,SAAS,EACT,SAAS,EACT,wBAAwB,EACxB,yBAAyB,EACzB,SAAS,GACV,EAAE,mBAAmB,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAiVjD"}
@@ -0,0 +1,361 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useEditor, EditorContent } from '@tiptap/react';
4
+ import StarterKit from '@tiptap/starter-kit';
5
+ import Placeholder from '@tiptap/extension-placeholder';
6
+ import Image from '@tiptap/extension-image';
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ import { Markdown } from 'tiptap-markdown';
9
+ import { useCollabRoom, getCollabExtensions, } from '@pilotiq/pilotiq/react';
10
+ // Inline lucide.dev SVGs — same posture as `toolbarButtons.tsx` so this
11
+ // package doesn't pull `lucide-react` as a peer dep. Keep stroke / size
12
+ // consistent with the rich-text toolbar.
13
+ const ICON_PROPS = {
14
+ width: 14, height: 14, viewBox: '0 0 24 24',
15
+ fill: 'none', stroke: 'currentColor',
16
+ strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
17
+ 'aria-hidden': 'true',
18
+ };
19
+ const Spinner = (_jsx("svg", { ...ICON_PROPS, className: "animate-spin", children: _jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }));
20
+ const SvgIcons = {
21
+ bold: (_jsxs("svg", { ...ICON_PROPS, strokeWidth: 2.25, children: [_jsx("path", { d: "M6 12h9a4 4 0 0 1 0 8H6Z" }), _jsx("path", { d: "M6 4h7a4 4 0 0 1 0 8H6Z" })] })),
22
+ italic: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("line", { x1: "19", y1: "4", x2: "10", y2: "4" }), _jsx("line", { x1: "14", y1: "20", x2: "5", y2: "20" }), _jsx("line", { x1: "15", y1: "4", x2: "9", y2: "20" })] })),
23
+ strike: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("path", { d: "M16 4H9a3 3 0 0 0-2.83 4" }), _jsx("path", { d: "M14 12a4 4 0 0 1 0 8H6" }), _jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" })] })),
24
+ link: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }), _jsx("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.71" })] })),
25
+ heading: _jsx("span", { className: "text-xs font-semibold leading-none", children: "H2" }),
26
+ bulletList: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("line", { x1: "8", y1: "6", x2: "21", y2: "6" }), _jsx("line", { x1: "8", y1: "12", x2: "21", y2: "12" }), _jsx("line", { x1: "8", y1: "18", x2: "21", y2: "18" }), _jsx("circle", { cx: "4", cy: "6", r: "1" }), _jsx("circle", { cx: "4", cy: "12", r: "1" }), _jsx("circle", { cx: "4", cy: "18", r: "1" })] })),
27
+ orderedList: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("line", { x1: "10", y1: "6", x2: "21", y2: "6" }), _jsx("line", { x1: "10", y1: "12", x2: "21", y2: "12" }), _jsx("line", { x1: "10", y1: "18", x2: "21", y2: "18" }), _jsx("path", { d: "M4 6h1v4" }), _jsx("path", { d: "M4 10h2" }), _jsx("path", { d: "M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" })] })),
28
+ blockquote: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("path", { d: "M3 21c3 0 7-1 7-8V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h3" }), _jsx("path", { d: "M15 21c3 0 7-1 7-8V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h3" })] })),
29
+ codeBlock: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("polyline", { points: "16 18 22 12 16 6" }), _jsx("polyline", { points: "8 6 2 12 8 18" })] })),
30
+ attachFiles: (_jsx("svg", { ...ICON_PROPS, children: _jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) })),
31
+ pencil: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("path", { d: "M12 20h9" }), _jsx("path", { d: "M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z" })] })),
32
+ source: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), _jsx("polyline", { points: "14 2 14 8 20 8" }), _jsx("line", { x1: "9", y1: "13", x2: "15", y2: "13" }), _jsx("line", { x1: "9", y1: "17", x2: "15", y2: "17" })] })),
33
+ eye: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8Z" }), _jsx("circle", { cx: "12", cy: "12", r: "3" })] })),
34
+ };
35
+ /**
36
+ * Plug-in WYSIWYG markdown editor registered with pilotiq core's
37
+ * `registerMarkdownEditor()`. Replaces the legacy textarea + manual-toolbar
38
+ * UI with a real rich editor; serializes to markdown on every change via
39
+ * `tiptap-markdown` so the wire format stays a plain markdown string under
40
+ * the field name.
41
+ *
42
+ * Collab-aware: when a `<RecordCollabRoom>` is mounted up-tree AND
43
+ * `registerCollabExtensions()` ran (both shipped by `@pilotiq-pro/collab`),
44
+ * the editor binds to the room's shared `Y.XmlFragment` via Tiptap's
45
+ * `Collaboration` extension. Every peer mounts the same editor against the
46
+ * same fragment; markdown serialization runs locally per peer so only the
47
+ * ProseMirror tree crosses the wire.
48
+ *
49
+ * Tabs (top-right):
50
+ * - **Editor** (default) — WYSIWYG.
51
+ * - **Source** — raw markdown textarea; on switch back to Editor the editor
52
+ * parses the textarea contents (round-trips through tiptap-markdown).
53
+ * - **Preview** — read-only render of the current markdown via the editor's
54
+ * own HTML output. Same view a user would see on the public site if the
55
+ * resource ships a read-side renderer.
56
+ *
57
+ * Single-source-of-truth posture: the editor's `onUpdate` is the canonical
58
+ * write path. Source-tab edits flow back through the editor on tab-switch
59
+ * (no dual state, no drift between source and editor doc).
60
+ */
61
+ export function MarkdownEditor({ name, defaultValue, placeholder, disabled = false, onChange, onBlur, toolbarButtons, minHeight, maxHeight, fileAttachmentsDirectory, fileAttachmentsVisibility, uploadUrl, }) {
62
+ const room = useCollabRoom();
63
+ const factory = getCollabExtensions();
64
+ const collabActive = !!(room && factory);
65
+ const [tab, setTab] = useState('editor');
66
+ const [sourceDraft, setSourceDraft] = useState(defaultValue);
67
+ const [uploading, setUploading] = useState(false);
68
+ const fileInputRef = useRef(null);
69
+ // Collab extension factory output. Built once per editor mount (the
70
+ // factory closes over the room's ydoc + provider + field name); keyed
71
+ // remount below ensures we never swap it underneath the running editor.
72
+ const collabExtensions = useMemo(() => {
73
+ if (!collabActive || !room || !factory)
74
+ return [];
75
+ return factory({
76
+ ydoc: room.ydoc,
77
+ provider: room.provider,
78
+ fieldName: name,
79
+ ...(room.user ? { user: room.user } : {}),
80
+ });
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ }, [collabActive]);
83
+ const editor = useEditor({
84
+ // Tiptap v3 SSR guard. With `immediatelyRender: true` (default)
85
+ // `useEditor` touches the DOM during construction; under Vike's
86
+ // `onRenderHtml` that throws "SSR has been detected, please set
87
+ // `immediatelyRender` explicitly to `false` to avoid hydration
88
+ // mismatches." Deferring until the first React effect lets SSR
89
+ // produce an empty shell + hydration mount the live editor.
90
+ immediatelyRender: false,
91
+ editable: !disabled,
92
+ extensions: [
93
+ StarterKit.configure({
94
+ link: { openOnClick: false, autolink: true },
95
+ // Collaboration brings its own Yjs-backed history — disable
96
+ // StarterKit's local undoRedo when collab is active (else Tiptap
97
+ // logs a "not compatible with @tiptap/extension-undo-redo" warning).
98
+ ...(collabActive ? { undoRedo: false } : {}),
99
+ }),
100
+ // Markdown round-trip — parses `content` (when non-collab) and
101
+ // exposes `editor.storage.markdown.getMarkdown()`. We pass `html:
102
+ // false` because the wire format is markdown only.
103
+ Markdown.configure({
104
+ html: false,
105
+ tightLists: true,
106
+ breaks: false,
107
+ linkify: true,
108
+ transformPastedText: true,
109
+ transformCopiedText: true,
110
+ }),
111
+ Image.configure({ inline: false, allowBase64: false }),
112
+ Placeholder.configure({ placeholder: placeholder ?? 'Write in markdown…' }),
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ ...collabExtensions,
115
+ ],
116
+ // Collab takes ownership of the document — passing `content` would
117
+ // race the Y.XmlFragment sync. Seed after first connect (effect below).
118
+ content: collabActive ? '' : defaultValue,
119
+ onUpdate({ editor }) {
120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
121
+ const storage = editor.storage.markdown;
122
+ const md = typeof storage?.getMarkdown === 'function' ? storage.getMarkdown() : '';
123
+ onChange(md);
124
+ },
125
+ onBlur() { onBlur?.(); },
126
+ },
127
+ // Re-mount when collab toggles. Other props (name, placeholder) are
128
+ // stable per mount — the field renderer doesn't swap them at runtime.
129
+ [collabActive]);
130
+ useEffect(() => {
131
+ if (!editor)
132
+ return;
133
+ editor.setEditable(!disabled && tab === 'editor');
134
+ }, [editor, disabled, tab]);
135
+ // First-load seed for collab. Collaboration starts the editor empty
136
+ // regardless of `content`; once the provider syncs from the server we
137
+ // check whether the field's `Y.XmlFragment` was ever written. Empty +
138
+ // we have an initial value = first session for this record. Mirrors
139
+ // the rich-text TiptapEditor seed path and the CollabTextRenderer seed.
140
+ const [hasSeeded, setHasSeeded] = useState(false);
141
+ useEffect(() => {
142
+ if (!editor || !collabActive || !room || hasSeeded)
143
+ return;
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ const ydoc = room.ydoc;
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
+ const provider = room.provider;
148
+ if (!ydoc || !provider)
149
+ return;
150
+ const trySeed = () => {
151
+ try {
152
+ const fragment = ydoc.getXmlFragment(name);
153
+ if (fragment && fragment.length === 0 && defaultValue) {
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ const cmd = editor.commands.setContent;
156
+ if (cmd)
157
+ cmd(defaultValue);
158
+ }
159
+ setHasSeeded(true);
160
+ }
161
+ catch {
162
+ setHasSeeded(true);
163
+ }
164
+ };
165
+ if (provider.synced) {
166
+ trySeed();
167
+ return;
168
+ }
169
+ provider.once('synced', trySeed);
170
+ return () => {
171
+ try {
172
+ provider.off?.('synced', trySeed);
173
+ }
174
+ catch { /* ignore */ }
175
+ };
176
+ // eslint-disable-next-line react-hooks/exhaustive-deps
177
+ }, [editor, collabActive, room]);
178
+ // Source-tab → Editor: parse the textarea back into the editor (this also
179
+ // emits onChange via the editor's onUpdate). One-way during the same flip.
180
+ const enterEditorTab = () => {
181
+ if (tab === 'source' && editor) {
182
+ try {
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
+ ;
185
+ editor.commands.setContent(sourceDraft);
186
+ }
187
+ catch { /* ignore parse errors */ }
188
+ }
189
+ setTab('editor');
190
+ };
191
+ const enterSourceTab = () => {
192
+ if (editor) {
193
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
+ const storage = editor.storage.markdown;
195
+ const md = typeof storage?.getMarkdown === 'function' ? storage.getMarkdown() : '';
196
+ setSourceDraft(md);
197
+ }
198
+ setTab('source');
199
+ };
200
+ const enterPreviewTab = () => {
201
+ setTab('preview');
202
+ };
203
+ // Live preview HTML is the editor's own HTML output — same renderer that
204
+ // produced what the user sees in the Editor tab. Read-only.
205
+ const previewHtml = useMemo(() => {
206
+ if (tab !== 'preview' || !editor)
207
+ return '';
208
+ try {
209
+ return editor.getHTML();
210
+ }
211
+ catch {
212
+ return '';
213
+ }
214
+ }, [tab, editor]);
215
+ const uploadAndInsert = async (file) => {
216
+ if (!uploadUrl || !editor)
217
+ return;
218
+ setUploading(true);
219
+ try {
220
+ const fd = new FormData();
221
+ fd.append('file', file);
222
+ if (fileAttachmentsDirectory)
223
+ fd.append('directory', fileAttachmentsDirectory);
224
+ if (fileAttachmentsVisibility)
225
+ fd.append('visibility', fileAttachmentsVisibility);
226
+ fd.append('fieldName', name);
227
+ const res = await fetch(uploadUrl, { method: 'POST', body: fd, headers: { Accept: 'application/json' } });
228
+ const data = await res.json().catch(() => ({}));
229
+ if (!res.ok || !data.ok || !data.url)
230
+ return;
231
+ const isImage = file.type.startsWith('image/');
232
+ if (isImage) {
233
+ editor.chain().focus().setImage({ src: data.url, alt: file.name }).run();
234
+ }
235
+ else {
236
+ editor.chain().focus().insertContent(`[${file.name}](${data.url})`).run();
237
+ }
238
+ }
239
+ finally {
240
+ setUploading(false);
241
+ }
242
+ };
243
+ const onAttachClick = () => {
244
+ const el = fileInputRef.current;
245
+ if (el)
246
+ el.click();
247
+ };
248
+ const onFilePicked = (e) => {
249
+ const file = e.target.files?.[0];
250
+ if (file)
251
+ void uploadAndInsert(file);
252
+ e.target.value = '';
253
+ };
254
+ // Toolbar item resolution. The pilotiq-side ids map onto Tiptap commands.
255
+ // attachFiles is gated on a configured uploadUrl (server strips it server-
256
+ // side when no adapter is registered, but defensive double-gate here too).
257
+ const allow = useMemo(() => new Set(toolbarButtons), [toolbarButtons]);
258
+ const canAttach = allow.has('attachFiles') && !!uploadUrl;
259
+ const exec = (id) => {
260
+ if (!editor)
261
+ return;
262
+ const c = editor.chain().focus();
263
+ switch (id) {
264
+ case 'bold':
265
+ c.toggleBold().run();
266
+ break;
267
+ case 'italic':
268
+ c.toggleItalic().run();
269
+ break;
270
+ case 'strike':
271
+ c.toggleStrike().run();
272
+ break;
273
+ case 'link': {
274
+ const prev = editor.getAttributes('link').href;
275
+ const url = window.prompt('URL', prev ?? '') ?? '';
276
+ if (url === '')
277
+ c.unsetLink().run();
278
+ else
279
+ c.extendMarkRange('link').setLink({ href: url }).run();
280
+ break;
281
+ }
282
+ case 'heading':
283
+ c.toggleHeading({ level: 2 }).run();
284
+ break;
285
+ case 'bulletList':
286
+ c.toggleBulletList().run();
287
+ break;
288
+ case 'orderedList':
289
+ c.toggleOrderedList().run();
290
+ break;
291
+ case 'blockquote':
292
+ c.toggleBlockquote().run();
293
+ break;
294
+ case 'codeBlock':
295
+ c.toggleCodeBlock().run();
296
+ break;
297
+ case 'attachFiles':
298
+ onAttachClick();
299
+ break;
300
+ default: /* unknown id — skip */ break;
301
+ }
302
+ };
303
+ const isActive = (id) => {
304
+ if (!editor)
305
+ return false;
306
+ switch (id) {
307
+ case 'bold': return editor.isActive('bold');
308
+ case 'italic': return editor.isActive('italic');
309
+ case 'strike': return editor.isActive('strike');
310
+ case 'link': return editor.isActive('link');
311
+ case 'heading': return editor.isActive('heading', { level: 2 });
312
+ case 'bulletList': return editor.isActive('bulletList');
313
+ case 'orderedList': return editor.isActive('orderedList');
314
+ case 'blockquote': return editor.isActive('blockquote');
315
+ case 'codeBlock': return editor.isActive('codeBlock');
316
+ default: return false;
317
+ }
318
+ };
319
+ const labels = {
320
+ bold: 'Bold (⌘B)',
321
+ italic: 'Italic (⌘I)',
322
+ strike: 'Strikethrough',
323
+ link: 'Link (⌘K)',
324
+ heading: 'Heading',
325
+ bulletList: 'Bulleted list',
326
+ orderedList: 'Numbered list',
327
+ blockquote: 'Quote',
328
+ codeBlock: 'Code block',
329
+ attachFiles: 'Attach file',
330
+ };
331
+ const wrapperStyle = {};
332
+ if (minHeight)
333
+ wrapperStyle.minHeight = minHeight;
334
+ if (maxHeight)
335
+ wrapperStyle.maxHeight = maxHeight;
336
+ return (_jsxs("div", { className: "flex flex-col rounded-md border bg-background", children: [canAttach && (_jsx("input", { ref: fileInputRef, type: "file", className: "hidden", onChange: onFilePicked })), _jsxs("div", { className: "flex items-center justify-between border-b px-2 py-1 gap-2", children: [_jsxs("div", { className: "flex items-center gap-0.5", children: [_jsxs(TabButton, { active: tab === 'editor', onClick: enterEditorTab, children: [SvgIcons['pencil'], " Editor"] }), _jsxs(TabButton, { active: tab === 'source', onClick: enterSourceTab, children: [SvgIcons['source'], " Source"] }), _jsxs(TabButton, { active: tab === 'preview', onClick: enterPreviewTab, children: [SvgIcons['eye'], " Preview"] })] }), tab === 'editor' && toolbarButtons.length > 0 && (_jsx("div", { className: "flex items-center gap-0.5", children: toolbarButtons.map((b) => {
337
+ if (b === 'attachFiles' && !canAttach)
338
+ return null;
339
+ const icon = SvgIcons[b];
340
+ if (!icon)
341
+ return null;
342
+ const isAttach = b === 'attachFiles';
343
+ const active = isActive(b);
344
+ return (_jsx("button", { type: "button", className: [
345
+ 'inline-flex size-7 items-center justify-center rounded text-foreground transition-colors',
346
+ active
347
+ ? 'bg-accent text-accent-foreground'
348
+ : 'hover:bg-accent hover:text-accent-foreground',
349
+ 'disabled:opacity-50',
350
+ ].join(' '), onClick: () => exec(b), disabled: disabled || (isAttach && uploading), title: labels[b] ?? b, "aria-label": labels[b] ?? b, "aria-pressed": active, children: isAttach && uploading ? Spinner : icon }, b));
351
+ }) }))] }), tab === 'editor' && (_jsx("div", { className: "prose prose-sm dark:prose-invert max-w-none px-3 py-2 [&_.ProseMirror]:outline-none [&_.ProseMirror]:min-h-[6rem]", style: wrapperStyle, children: _jsx(EditorContent, { editor: editor }) })), tab === 'source' && (_jsx("textarea", { className: "w-full resize-y bg-transparent px-3 py-2 text-sm font-mono leading-relaxed outline-none disabled:opacity-50", style: wrapperStyle, value: sourceDraft, onChange: (e) => setSourceDraft(e.target.value), ...(placeholder !== undefined ? { placeholder } : {}), disabled: disabled, "aria-label": `${name} (markdown source)` })), tab === 'preview' && (_jsx("div", { className: "prose prose-sm dark:prose-invert max-w-none px-3 py-2", style: wrapperStyle, dangerouslySetInnerHTML: { __html: previewHtml || '<p class="text-muted-foreground italic">Nothing to preview</p>' } }))] }));
352
+ }
353
+ function TabButton({ active, onClick, children }) {
354
+ return (_jsx("button", { type: "button", className: [
355
+ 'inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium transition-colors',
356
+ active
357
+ ? 'bg-accent text-accent-foreground'
358
+ : 'text-muted-foreground hover:text-foreground',
359
+ ].join(' '), onClick: onClick, children: children }));
360
+ }
361
+ //# sourceMappingURL=MarkdownEditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownEditor.js","sourceRoot":"","sources":["../../src/react/MarkdownEditor.tsx"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,IAAI,IAAI,CASrC"}
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAgBrC"}
package/dist/register.js CHANGED
@@ -1,7 +1,8 @@
1
- import { registerFieldRenderer, registerCollabTextRenderer } from '@pilotiq/pilotiq/react';
1
+ import { registerFieldRenderer, registerCollabTextRenderer, registerMarkdownEditor } from '@pilotiq/pilotiq/react';
2
2
  import { registerRichTextRenderer } from '@pilotiq/pilotiq/richtext';
3
3
  import { TiptapEditor } from './react/TiptapEditor.js';
4
4
  import { CollabTextRenderer } from './react/CollabTextRenderer.js';
5
+ import { MarkdownEditor } from './react/MarkdownEditor.js';
5
6
  import { renderRichTextToHtml, isRichTextValue } from './render.js';
6
7
  /**
7
8
  * Register the Tiptap editor as the pilotiq renderer for `fieldType: 'richtext'`.
@@ -30,5 +31,12 @@ export function registerTiptap() {
30
31
  // out via `.collab(false)`, the renderer mounts `CollabTextRenderer`
31
32
  // instead of the legacy `Y.Text` + `computeDelta` + `preserveCursor` path.
32
33
  registerCollabTextRenderer(CollabTextRenderer);
34
+ // WYSIWYG markdown editor — replaces `MarkdownField`'s legacy textarea +
35
+ // manual-toolbar path with a real rich editor that serializes to markdown
36
+ // via `tiptap-markdown` on every change. Collab-aware on the same
37
+ // `useCollabRoom()` + `getCollabExtensions()` plumbing as the rich-text
38
+ // editor. Without `@pilotiq/tiptap` installed, `MarkdownInput` falls back
39
+ // to the textarea path so panels that skip the adapter still work.
40
+ registerMarkdownEditor(MarkdownEditor);
33
41
  }
34
42
  //# sourceMappingURL=register.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAA;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEnE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc;IAC5B,qBAAqB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;IAC/C,wBAAwB,CAAC,oBAAoB,EAAE,eAAe,CAAC,CAAA;IAC/D,uEAAuE;IACvE,2EAA2E;IAC3E,6EAA6E;IAC7E,qEAAqE;IACrE,2EAA2E;IAC3E,0BAA0B,CAAC,kBAAkB,CAAC,CAAA;AAChD,CAAC"}
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAClH,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEnE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc;IAC5B,qBAAqB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;IAC/C,wBAAwB,CAAC,oBAAoB,EAAE,eAAe,CAAC,CAAA;IAC/D,uEAAuE;IACvE,2EAA2E;IAC3E,6EAA6E;IAC7E,qEAAqE;IACrE,2EAA2E;IAC3E,0BAA0B,CAAC,kBAAkB,CAAC,CAAA;IAC9C,yEAAyE;IACzE,0EAA0E;IAC1E,kEAAkE;IAClE,wEAAwE;IACxE,0EAA0E;IAC1E,mEAAmE;IACnE,sBAAsB,CAAC,cAAc,CAAC,CAAA;AACxC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilotiq/tiptap",
3
- "version": "3.2.1",
3
+ "version": "3.3.1",
4
4
  "description": "Tiptap rich-text editor adapter for @pilotiq/pilotiq — slash menu, draggable blocks, custom-block API",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -51,6 +51,7 @@
51
51
  "@tiptap/extension-table": "3.22.4",
52
52
  "@tiptap/extension-details": "3.22.4",
53
53
  "@tiptap/suggestion": "^3",
54
+ "tiptap-markdown": "^0.9",
54
55
  "react": "^18 || ^19",
55
56
  "react-dom": "^18 || ^19"
56
57
  },
@@ -73,13 +74,14 @@
73
74
  "@tiptap/extension-table": "3.22.4",
74
75
  "@tiptap/extension-details": "3.22.4",
75
76
  "@tiptap/suggestion": "^3",
77
+ "tiptap-markdown": "^0.9",
76
78
  "@types/node": "^20",
77
79
  "@types/react": "^19",
78
80
  "@types/react-dom": "^19",
79
81
  "react": "^19",
80
82
  "react-dom": "^19",
81
83
  "typescript": "^5",
82
- "@pilotiq/pilotiq": "^0.12.0"
84
+ "@pilotiq/pilotiq": "^0.15.0"
83
85
  },
84
86
  "author": "Suleiman Shahbari",
85
87
  "scripts": {
@@ -0,0 +1,513 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
+ import { useEditor, EditorContent } from '@tiptap/react'
3
+ import type { AnyExtension } from '@tiptap/core'
4
+ import StarterKit from '@tiptap/starter-kit'
5
+ import Placeholder from '@tiptap/extension-placeholder'
6
+ import Image from '@tiptap/extension-image'
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ import { Markdown } from 'tiptap-markdown'
9
+ import {
10
+ useCollabRoom,
11
+ getCollabExtensions,
12
+ type MarkdownEditorProps,
13
+ } from '@pilotiq/pilotiq/react'
14
+
15
+ // Inline lucide.dev SVGs — same posture as `toolbarButtons.tsx` so this
16
+ // package doesn't pull `lucide-react` as a peer dep. Keep stroke / size
17
+ // consistent with the rich-text toolbar.
18
+ const ICON_PROPS = {
19
+ width: 14, height: 14, viewBox: '0 0 24 24',
20
+ fill: 'none', stroke: 'currentColor',
21
+ strokeWidth: 2, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const,
22
+ 'aria-hidden': 'true' as const,
23
+ }
24
+ const Spinner = (
25
+ <svg {...ICON_PROPS} className="animate-spin">
26
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
27
+ </svg>
28
+ )
29
+ const SvgIcons: Record<string, React.ReactElement> = {
30
+ bold: (
31
+ <svg {...ICON_PROPS} strokeWidth={2.25}>
32
+ <path d="M6 12h9a4 4 0 0 1 0 8H6Z" />
33
+ <path d="M6 4h7a4 4 0 0 1 0 8H6Z" />
34
+ </svg>
35
+ ),
36
+ italic: (
37
+ <svg {...ICON_PROPS}>
38
+ <line x1="19" y1="4" x2="10" y2="4" />
39
+ <line x1="14" y1="20" x2="5" y2="20" />
40
+ <line x1="15" y1="4" x2="9" y2="20" />
41
+ </svg>
42
+ ),
43
+ strike: (
44
+ <svg {...ICON_PROPS}>
45
+ <path d="M16 4H9a3 3 0 0 0-2.83 4" />
46
+ <path d="M14 12a4 4 0 0 1 0 8H6" />
47
+ <line x1="4" y1="12" x2="20" y2="12" />
48
+ </svg>
49
+ ),
50
+ link: (
51
+ <svg {...ICON_PROPS}>
52
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
53
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.71" />
54
+ </svg>
55
+ ),
56
+ heading: <span className="text-xs font-semibold leading-none">H2</span>,
57
+ bulletList: (
58
+ <svg {...ICON_PROPS}>
59
+ <line x1="8" y1="6" x2="21" y2="6" />
60
+ <line x1="8" y1="12" x2="21" y2="12" />
61
+ <line x1="8" y1="18" x2="21" y2="18" />
62
+ <circle cx="4" cy="6" r="1" />
63
+ <circle cx="4" cy="12" r="1" />
64
+ <circle cx="4" cy="18" r="1" />
65
+ </svg>
66
+ ),
67
+ orderedList: (
68
+ <svg {...ICON_PROPS}>
69
+ <line x1="10" y1="6" x2="21" y2="6" />
70
+ <line x1="10" y1="12" x2="21" y2="12" />
71
+ <line x1="10" y1="18" x2="21" y2="18" />
72
+ <path d="M4 6h1v4" />
73
+ <path d="M4 10h2" />
74
+ <path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" />
75
+ </svg>
76
+ ),
77
+ blockquote: (
78
+ <svg {...ICON_PROPS}>
79
+ <path d="M3 21c3 0 7-1 7-8V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h3" />
80
+ <path d="M15 21c3 0 7-1 7-8V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h3" />
81
+ </svg>
82
+ ),
83
+ codeBlock: (
84
+ <svg {...ICON_PROPS}>
85
+ <polyline points="16 18 22 12 16 6" />
86
+ <polyline points="8 6 2 12 8 18" />
87
+ </svg>
88
+ ),
89
+ attachFiles: (
90
+ <svg {...ICON_PROPS}>
91
+ <path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
92
+ </svg>
93
+ ),
94
+ pencil: (
95
+ <svg {...ICON_PROPS}>
96
+ <path d="M12 20h9" />
97
+ <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z" />
98
+ </svg>
99
+ ),
100
+ source: (
101
+ <svg {...ICON_PROPS}>
102
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
103
+ <polyline points="14 2 14 8 20 8" />
104
+ <line x1="9" y1="13" x2="15" y2="13" />
105
+ <line x1="9" y1="17" x2="15" y2="17" />
106
+ </svg>
107
+ ),
108
+ eye: (
109
+ <svg {...ICON_PROPS}>
110
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8Z" />
111
+ <circle cx="12" cy="12" r="3" />
112
+ </svg>
113
+ ),
114
+ }
115
+
116
+ /**
117
+ * Plug-in WYSIWYG markdown editor registered with pilotiq core's
118
+ * `registerMarkdownEditor()`. Replaces the legacy textarea + manual-toolbar
119
+ * UI with a real rich editor; serializes to markdown on every change via
120
+ * `tiptap-markdown` so the wire format stays a plain markdown string under
121
+ * the field name.
122
+ *
123
+ * Collab-aware: when a `<RecordCollabRoom>` is mounted up-tree AND
124
+ * `registerCollabExtensions()` ran (both shipped by `@pilotiq-pro/collab`),
125
+ * the editor binds to the room's shared `Y.XmlFragment` via Tiptap's
126
+ * `Collaboration` extension. Every peer mounts the same editor against the
127
+ * same fragment; markdown serialization runs locally per peer so only the
128
+ * ProseMirror tree crosses the wire.
129
+ *
130
+ * Tabs (top-right):
131
+ * - **Editor** (default) — WYSIWYG.
132
+ * - **Source** — raw markdown textarea; on switch back to Editor the editor
133
+ * parses the textarea contents (round-trips through tiptap-markdown).
134
+ * - **Preview** — read-only render of the current markdown via the editor's
135
+ * own HTML output. Same view a user would see on the public site if the
136
+ * resource ships a read-side renderer.
137
+ *
138
+ * Single-source-of-truth posture: the editor's `onUpdate` is the canonical
139
+ * write path. Source-tab edits flow back through the editor on tab-switch
140
+ * (no dual state, no drift between source and editor doc).
141
+ */
142
+ export function MarkdownEditor({
143
+ name,
144
+ defaultValue,
145
+ placeholder,
146
+ disabled = false,
147
+ onChange,
148
+ onBlur,
149
+ toolbarButtons,
150
+ minHeight,
151
+ maxHeight,
152
+ fileAttachmentsDirectory,
153
+ fileAttachmentsVisibility,
154
+ uploadUrl,
155
+ }: MarkdownEditorProps): React.ReactElement | null {
156
+ const room = useCollabRoom()
157
+ const factory = getCollabExtensions()
158
+ const collabActive = !!(room && factory)
159
+
160
+ const [tab, setTab] = useState<'editor' | 'source' | 'preview'>('editor')
161
+ const [sourceDraft, setSourceDraft] = useState<string>(defaultValue)
162
+ const [uploading, setUploading] = useState(false)
163
+ const fileInputRef = useRef<HTMLInputElement | null>(null)
164
+
165
+ // Collab extension factory output. Built once per editor mount (the
166
+ // factory closes over the room's ydoc + provider + field name); keyed
167
+ // remount below ensures we never swap it underneath the running editor.
168
+ const collabExtensions = useMemo<AnyExtension[]>(() => {
169
+ if (!collabActive || !room || !factory) return []
170
+ return factory({
171
+ ydoc: room.ydoc,
172
+ provider: room.provider,
173
+ fieldName: name,
174
+ ...(room.user ? { user: room.user } : {}),
175
+ }) as AnyExtension[]
176
+ // eslint-disable-next-line react-hooks/exhaustive-deps
177
+ }, [collabActive])
178
+
179
+ const editor = useEditor(
180
+ {
181
+ // Tiptap v3 SSR guard. With `immediatelyRender: true` (default)
182
+ // `useEditor` touches the DOM during construction; under Vike's
183
+ // `onRenderHtml` that throws "SSR has been detected, please set
184
+ // `immediatelyRender` explicitly to `false` to avoid hydration
185
+ // mismatches." Deferring until the first React effect lets SSR
186
+ // produce an empty shell + hydration mount the live editor.
187
+ immediatelyRender: false,
188
+ editable: !disabled,
189
+ extensions: [
190
+ StarterKit.configure({
191
+ link: { openOnClick: false, autolink: true },
192
+ // Collaboration brings its own Yjs-backed history — disable
193
+ // StarterKit's local undoRedo when collab is active (else Tiptap
194
+ // logs a "not compatible with @tiptap/extension-undo-redo" warning).
195
+ ...(collabActive ? { undoRedo: false } : {}),
196
+ }),
197
+ // Markdown round-trip — parses `content` (when non-collab) and
198
+ // exposes `editor.storage.markdown.getMarkdown()`. We pass `html:
199
+ // false` because the wire format is markdown only.
200
+ Markdown.configure({
201
+ html: false,
202
+ tightLists: true,
203
+ breaks: false,
204
+ linkify: true,
205
+ transformPastedText: true,
206
+ transformCopiedText: true,
207
+ }),
208
+ Image.configure({ inline: false, allowBase64: false }),
209
+ Placeholder.configure({ placeholder: placeholder ?? 'Write in markdown…' }),
210
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
211
+ ...(collabExtensions as any[]),
212
+ ],
213
+ // Collab takes ownership of the document — passing `content` would
214
+ // race the Y.XmlFragment sync. Seed after first connect (effect below).
215
+ content: collabActive ? '' : defaultValue,
216
+ onUpdate({ editor }) {
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
+ const storage = (editor.storage as any).markdown
219
+ const md = typeof storage?.getMarkdown === 'function' ? storage.getMarkdown() : ''
220
+ onChange(md)
221
+ },
222
+ onBlur() { onBlur?.() },
223
+ },
224
+ // Re-mount when collab toggles. Other props (name, placeholder) are
225
+ // stable per mount — the field renderer doesn't swap them at runtime.
226
+ [collabActive],
227
+ )
228
+
229
+ useEffect(() => {
230
+ if (!editor) return
231
+ editor.setEditable(!disabled && tab === 'editor')
232
+ }, [editor, disabled, tab])
233
+
234
+ // First-load seed for collab. Collaboration starts the editor empty
235
+ // regardless of `content`; once the provider syncs from the server we
236
+ // check whether the field's `Y.XmlFragment` was ever written. Empty +
237
+ // we have an initial value = first session for this record. Mirrors
238
+ // the rich-text TiptapEditor seed path and the CollabTextRenderer seed.
239
+ const [hasSeeded, setHasSeeded] = useState(false)
240
+ useEffect(() => {
241
+ if (!editor || !collabActive || !room || hasSeeded) return
242
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
243
+ const ydoc = room.ydoc as any
244
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
245
+ const provider = room.provider as any
246
+ if (!ydoc || !provider) return
247
+
248
+ const trySeed = (): void => {
249
+ try {
250
+ const fragment = ydoc.getXmlFragment(name)
251
+ if (fragment && fragment.length === 0 && defaultValue) {
252
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
253
+ const cmd = (editor.commands as any).setContent
254
+ if (cmd) cmd(defaultValue)
255
+ }
256
+ setHasSeeded(true)
257
+ } catch {
258
+ setHasSeeded(true)
259
+ }
260
+ }
261
+
262
+ if (provider.synced) {
263
+ trySeed()
264
+ return
265
+ }
266
+ provider.once('synced', trySeed)
267
+ return () => {
268
+ try { provider.off?.('synced', trySeed) } catch { /* ignore */ }
269
+ }
270
+ // eslint-disable-next-line react-hooks/exhaustive-deps
271
+ }, [editor, collabActive, room])
272
+
273
+ // Source-tab → Editor: parse the textarea back into the editor (this also
274
+ // emits onChange via the editor's onUpdate). One-way during the same flip.
275
+ const enterEditorTab = (): void => {
276
+ if (tab === 'source' && editor) {
277
+ try {
278
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
279
+ ;(editor.commands as any).setContent(sourceDraft)
280
+ } catch { /* ignore parse errors */ }
281
+ }
282
+ setTab('editor')
283
+ }
284
+
285
+ const enterSourceTab = (): void => {
286
+ if (editor) {
287
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
288
+ const storage = (editor.storage as any).markdown
289
+ const md = typeof storage?.getMarkdown === 'function' ? storage.getMarkdown() : ''
290
+ setSourceDraft(md)
291
+ }
292
+ setTab('source')
293
+ }
294
+
295
+ const enterPreviewTab = (): void => {
296
+ setTab('preview')
297
+ }
298
+
299
+ // Live preview HTML is the editor's own HTML output — same renderer that
300
+ // produced what the user sees in the Editor tab. Read-only.
301
+ const previewHtml = useMemo<string>(() => {
302
+ if (tab !== 'preview' || !editor) return ''
303
+ try {
304
+ return editor.getHTML()
305
+ } catch {
306
+ return ''
307
+ }
308
+ }, [tab, editor])
309
+
310
+ const uploadAndInsert = async (file: File): Promise<void> => {
311
+ if (!uploadUrl || !editor) return
312
+ setUploading(true)
313
+ try {
314
+ const fd = new FormData()
315
+ fd.append('file', file)
316
+ if (fileAttachmentsDirectory) fd.append('directory', fileAttachmentsDirectory)
317
+ if (fileAttachmentsVisibility) fd.append('visibility', fileAttachmentsVisibility)
318
+ fd.append('fieldName', name)
319
+ const res = await fetch(uploadUrl, { method: 'POST', body: fd, headers: { Accept: 'application/json' } })
320
+ const data = await res.json().catch(() => ({} as { ok?: boolean; url?: string; error?: string }))
321
+ if (!res.ok || !data.ok || !data.url) return
322
+ const isImage = file.type.startsWith('image/')
323
+ if (isImage) {
324
+ editor.chain().focus().setImage({ src: data.url, alt: file.name }).run()
325
+ } else {
326
+ editor.chain().focus().insertContent(`[${file.name}](${data.url})`).run()
327
+ }
328
+ } finally {
329
+ setUploading(false)
330
+ }
331
+ }
332
+
333
+ const onAttachClick = (): void => {
334
+ const el = fileInputRef.current
335
+ if (el) el.click()
336
+ }
337
+
338
+ const onFilePicked = (e: React.ChangeEvent<HTMLInputElement>): void => {
339
+ const file = e.target.files?.[0]
340
+ if (file) void uploadAndInsert(file)
341
+ e.target.value = ''
342
+ }
343
+
344
+ // Toolbar item resolution. The pilotiq-side ids map onto Tiptap commands.
345
+ // attachFiles is gated on a configured uploadUrl (server strips it server-
346
+ // side when no adapter is registered, but defensive double-gate here too).
347
+ const allow = useMemo(() => new Set(toolbarButtons), [toolbarButtons])
348
+ const canAttach = allow.has('attachFiles') && !!uploadUrl
349
+
350
+ const exec = (id: string): void => {
351
+ if (!editor) return
352
+ const c = editor.chain().focus()
353
+ switch (id) {
354
+ case 'bold': c.toggleBold().run(); break
355
+ case 'italic': c.toggleItalic().run(); break
356
+ case 'strike': c.toggleStrike().run(); break
357
+ case 'link': {
358
+ const prev = editor.getAttributes('link').href as string | undefined
359
+ const url = window.prompt('URL', prev ?? '') ?? ''
360
+ if (url === '') c.unsetLink().run()
361
+ else c.extendMarkRange('link').setLink({ href: url }).run()
362
+ break
363
+ }
364
+ case 'heading': c.toggleHeading({ level: 2 }).run(); break
365
+ case 'bulletList': c.toggleBulletList().run(); break
366
+ case 'orderedList': c.toggleOrderedList().run(); break
367
+ case 'blockquote': c.toggleBlockquote().run(); break
368
+ case 'codeBlock': c.toggleCodeBlock().run(); break
369
+ case 'attachFiles': onAttachClick(); break
370
+ default: /* unknown id — skip */ break
371
+ }
372
+ }
373
+
374
+ const isActive = (id: string): boolean => {
375
+ if (!editor) return false
376
+ switch (id) {
377
+ case 'bold': return editor.isActive('bold')
378
+ case 'italic': return editor.isActive('italic')
379
+ case 'strike': return editor.isActive('strike')
380
+ case 'link': return editor.isActive('link')
381
+ case 'heading': return editor.isActive('heading', { level: 2 })
382
+ case 'bulletList': return editor.isActive('bulletList')
383
+ case 'orderedList': return editor.isActive('orderedList')
384
+ case 'blockquote': return editor.isActive('blockquote')
385
+ case 'codeBlock': return editor.isActive('codeBlock')
386
+ default: return false
387
+ }
388
+ }
389
+
390
+ const labels: Record<string, string> = {
391
+ bold: 'Bold (⌘B)',
392
+ italic: 'Italic (⌘I)',
393
+ strike: 'Strikethrough',
394
+ link: 'Link (⌘K)',
395
+ heading: 'Heading',
396
+ bulletList: 'Bulleted list',
397
+ orderedList: 'Numbered list',
398
+ blockquote: 'Quote',
399
+ codeBlock: 'Code block',
400
+ attachFiles: 'Attach file',
401
+ }
402
+
403
+ const wrapperStyle: React.CSSProperties = {}
404
+ if (minHeight) wrapperStyle.minHeight = minHeight
405
+ if (maxHeight) wrapperStyle.maxHeight = maxHeight
406
+
407
+ return (
408
+ <div className="flex flex-col rounded-md border bg-background">
409
+ {canAttach && (
410
+ <input
411
+ ref={fileInputRef}
412
+ type="file"
413
+ className="hidden"
414
+ onChange={onFilePicked}
415
+ />
416
+ )}
417
+ <div className="flex items-center justify-between border-b px-2 py-1 gap-2">
418
+ <div className="flex items-center gap-0.5">
419
+ <TabButton active={tab === 'editor'} onClick={enterEditorTab}>
420
+ {SvgIcons['pencil']} Editor
421
+ </TabButton>
422
+ <TabButton active={tab === 'source'} onClick={enterSourceTab}>
423
+ {SvgIcons['source']} Source
424
+ </TabButton>
425
+ <TabButton active={tab === 'preview'} onClick={enterPreviewTab}>
426
+ {SvgIcons['eye']} Preview
427
+ </TabButton>
428
+ </div>
429
+ {tab === 'editor' && toolbarButtons.length > 0 && (
430
+ <div className="flex items-center gap-0.5">
431
+ {toolbarButtons.map((b: string) => {
432
+ if (b === 'attachFiles' && !canAttach) return null
433
+ const icon = SvgIcons[b]
434
+ if (!icon) return null
435
+ const isAttach = b === 'attachFiles'
436
+ const active = isActive(b)
437
+ return (
438
+ <button
439
+ key={b}
440
+ type="button"
441
+ className={[
442
+ 'inline-flex size-7 items-center justify-center rounded text-foreground transition-colors',
443
+ active
444
+ ? 'bg-accent text-accent-foreground'
445
+ : 'hover:bg-accent hover:text-accent-foreground',
446
+ 'disabled:opacity-50',
447
+ ].join(' ')}
448
+ onClick={() => exec(b)}
449
+ disabled={disabled || (isAttach && uploading)}
450
+ title={labels[b] ?? b}
451
+ aria-label={labels[b] ?? b}
452
+ aria-pressed={active}
453
+ >
454
+ {isAttach && uploading ? Spinner : icon}
455
+ </button>
456
+ )
457
+ })}
458
+ </div>
459
+ )}
460
+ </div>
461
+
462
+ {tab === 'editor' && (
463
+ <div
464
+ className="prose prose-sm dark:prose-invert max-w-none px-3 py-2 [&_.ProseMirror]:outline-none [&_.ProseMirror]:min-h-[6rem]"
465
+ style={wrapperStyle}
466
+ >
467
+ <EditorContent editor={editor} />
468
+ </div>
469
+ )}
470
+
471
+ {tab === 'source' && (
472
+ <textarea
473
+ className="w-full resize-y bg-transparent px-3 py-2 text-sm font-mono leading-relaxed outline-none disabled:opacity-50"
474
+ style={wrapperStyle}
475
+ value={sourceDraft}
476
+ onChange={(e) => setSourceDraft(e.target.value)}
477
+ {...(placeholder !== undefined ? { placeholder } : {})}
478
+ disabled={disabled}
479
+ aria-label={`${name} (markdown source)`}
480
+ />
481
+ )}
482
+
483
+ {tab === 'preview' && (
484
+ <div
485
+ className="prose prose-sm dark:prose-invert max-w-none px-3 py-2"
486
+ style={wrapperStyle}
487
+ dangerouslySetInnerHTML={{ __html: previewHtml || '<p class="text-muted-foreground italic">Nothing to preview</p>' }}
488
+ />
489
+ )}
490
+ </div>
491
+ )
492
+ }
493
+
494
+ function TabButton({ active, onClick, children }: {
495
+ active: boolean
496
+ onClick: () => void
497
+ children: React.ReactNode
498
+ }): React.ReactElement {
499
+ return (
500
+ <button
501
+ type="button"
502
+ className={[
503
+ 'inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium transition-colors',
504
+ active
505
+ ? 'bg-accent text-accent-foreground'
506
+ : 'text-muted-foreground hover:text-foreground',
507
+ ].join(' ')}
508
+ onClick={onClick}
509
+ >
510
+ {children}
511
+ </button>
512
+ )
513
+ }
package/src/register.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { registerFieldRenderer, registerCollabTextRenderer } from '@pilotiq/pilotiq/react'
1
+ import { registerFieldRenderer, registerCollabTextRenderer, registerMarkdownEditor } from '@pilotiq/pilotiq/react'
2
2
  import { registerRichTextRenderer } from '@pilotiq/pilotiq/richtext'
3
3
  import { TiptapEditor } from './react/TiptapEditor.js'
4
4
  import { CollabTextRenderer } from './react/CollabTextRenderer.js'
5
+ import { MarkdownEditor } from './react/MarkdownEditor.js'
5
6
  import { renderRichTextToHtml, isRichTextValue } from './render.js'
6
7
 
7
8
  /**
@@ -31,4 +32,11 @@ export function registerTiptap(): void {
31
32
  // out via `.collab(false)`, the renderer mounts `CollabTextRenderer`
32
33
  // instead of the legacy `Y.Text` + `computeDelta` + `preserveCursor` path.
33
34
  registerCollabTextRenderer(CollabTextRenderer)
35
+ // WYSIWYG markdown editor — replaces `MarkdownField`'s legacy textarea +
36
+ // manual-toolbar path with a real rich editor that serializes to markdown
37
+ // via `tiptap-markdown` on every change. Collab-aware on the same
38
+ // `useCollabRoom()` + `getCollabExtensions()` plumbing as the rich-text
39
+ // editor. Without `@pilotiq/tiptap` installed, `MarkdownInput` falls back
40
+ // to the textarea path so panels that skip the adapter still work.
41
+ registerMarkdownEditor(MarkdownEditor)
34
42
  }