diff --git a/apps/web/app/(dashboard)/issues/[id]/page.test.tsx b/apps/web/app/(dashboard)/issues/[id]/page.test.tsx index b9d0d4ff..7ec44b49 100644 --- a/apps/web/app/(dashboard)/issues/[id]/page.test.tsx +++ b/apps/web/app/(dashboard)/issues/[id]/page.test.tsx @@ -104,9 +104,9 @@ vi.mock("@/components/ui/calendar", () => ({ Calendar: () => null, })); -// Mock RichTextEditor (Tiptap needs real DOM) -vi.mock("@/components/common/rich-text-editor", () => ({ - RichTextEditor: forwardRef(({ defaultValue, onUpdate, placeholder, onSubmit }: any, ref: any) => { +// Mock ContentEditor (Tiptap needs real DOM) +vi.mock("@/features/editor", () => ({ + ContentEditor: forwardRef(({ defaultValue, onUpdate, placeholder, onSubmit }: any, ref: any) => { const valueRef = useRef(defaultValue || ""); const [value, setValue] = useState(defaultValue || ""); useImperativeHandle(ref, () => ({ @@ -132,6 +132,27 @@ vi.mock("@/components/common/rich-text-editor", () => ({ /> ); }), + TitleEditor: forwardRef(({ defaultValue, placeholder, onBlur, onChange }: any, ref: any) => { + const valueRef = useRef(defaultValue || ""); + const [value, setValue] = useState(defaultValue || ""); + useImperativeHandle(ref, () => ({ + getText: () => valueRef.current, + focus: () => {}, + })); + return ( + { + valueRef.current = e.target.value; + setValue(e.target.value); + onChange?.(e.target.value); + }} + onBlur={() => onBlur?.(valueRef.current)} + placeholder={placeholder} + data-testid="title-editor" + /> + ); + }), })); // Mock Markdown renderer diff --git a/apps/web/components/common/markdown-to-html.ts b/apps/web/components/common/markdown-to-html.ts deleted file mode 100644 index aff8ca22..00000000 --- a/apps/web/components/common/markdown-to-html.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Marked } from "marked"; -import { preprocessLinks } from "@/components/markdown/linkify"; - -/** - * Dedicated Marked instance for converting markdown → Tiptap-compatible HTML. - * - * Uses a separate instance (not the global `marked`) to avoid interfering with - * @tiptap/markdown's internal marked instance. Custom renderer ensures output - * matches Tiptap's ProseMirror schema requirements (e.g. block content in cells). - */ -const tiptapMarked = new Marked(); - -tiptapMarked.use({ - renderer: { - // Tiptap's TableCell/TableHeader nodes require `content: "block+"`. - // Default marked outputs bare inline content in /, which - // ProseMirror silently drops. Wrap in

so it's valid block content. - tablecell({ tokens, header }) { - const tag = header ? "th" : "td"; - const content = this.parser.parseInline(tokens); - return `<${tag}>

${content}

\n`; - }, - }, -}); - -// --------------------------------------------------------------------------- -// Mention preprocessing -// --------------------------------------------------------------------------- - -/** - * Convert mention link syntax to HTML spans matching Tiptap's Mention - * extension parseHTML expectations (data-type, data-id, data-label, data-mention-type). - */ -function mentionsToHtml(text: string): string { - return text.replace( - /\[@?([^\]]+)\]\(mention:\/\/(\w+)\/([^)]+)\)/g, - (_match, label: string, type: string, id: string) => { - const prefix = type === "issue" ? "" : "@"; - return ( - `${prefix}${label}` - ); - }, - ); -} - -/** - * Convert legacy mention shortcodes [@ id="UUID" label="LABEL"] to the - * standard markdown link format before further processing. - */ -function preprocessMentionShortcodes(text: string): string { - if (!text.includes("[@ ")) return text; - return text.replace( - /\[@\s+([^\]]*)\]/g, - (match: string, attrString: string) => { - const attrs: Record = {}; - const re = /(\w+)="([^"]*)"/g; - let m; - while ((m = re.exec(attrString)) !== null) { - if (m[1] && m[2] !== undefined) attrs[m[1]] = m[2]; - } - const { id, label } = attrs; - if (!id || !label) return match; - return `[@${label}](mention://member/${id})`; - }, - ); -} - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -/** - * Convert a markdown string to Tiptap-compatible HTML. - * - * Pipeline: - * 1. Legacy mention shortcodes → standard mention links - * 2. Raw URLs → markdown links (linkify) - * 3. Mention links → HTML - * 4. Marked renders everything else (tables, lists, headings, code, hr…) - * with custom renderer ensuring ProseMirror schema compatibility - * - * The result is loaded into Tiptap as HTML (no contentType: "markdown"), - * bypassing @tiptap/markdown's beta parser entirely. The Markdown extension - * is still loaded for getMarkdown() serialization on save. - */ -export function markdownToHtml(markdown: string): string { - if (!markdown) return ""; - const step1 = preprocessMentionShortcodes(markdown); - const step2 = preprocessLinks(step1); - const step3 = mentionsToHtml(step2); - return tiptapMarked.parse(step3) as string; -} diff --git a/apps/web/components/common/readonly-editor.tsx b/apps/web/components/common/readonly-editor.tsx deleted file mode 100644 index c2a6690a..00000000 --- a/apps/web/components/common/readonly-editor.tsx +++ /dev/null @@ -1,124 +0,0 @@ -"use client"; - -import { useEffect, useRef, memo } from "react"; -import { useEditor, EditorContent, ReactNodeViewRenderer } from "@tiptap/react"; -import StarterKit from "@tiptap/starter-kit"; -import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; -import { common, createLowlight } from "lowlight"; -import Link from "@tiptap/extension-link"; -import Image from "@tiptap/extension-image"; -import TableRow from "@tiptap/extension-table-row"; -import TableHeader from "@tiptap/extension-table-header"; -import TableCell from "@tiptap/extension-table-cell"; -import { Table } from "@tiptap/extension-table"; -import { Markdown } from "@tiptap/markdown"; -import { cn } from "@/lib/utils"; -import { BaseMentionExtension } from "./mention-extension"; -import { CodeBlockView } from "./code-block-view"; -import { markdownToHtml } from "./markdown-to-html"; -import "./rich-text-editor.css"; - -const lowlight = createLowlight(common); - -// --------------------------------------------------------------------------- -// Module-level extension singletons (prevent useEditor re-creation) -// --------------------------------------------------------------------------- - -const extensions = [ - StarterKit.configure({ - heading: { levels: [1, 2, 3] }, - link: false, - codeBlock: false, - }), - CodeBlockLowlight.extend({ - addNodeView() { - return ReactNodeViewRenderer(CodeBlockView); - }, - }).configure({ lowlight }), - Link.configure({ - openOnClick: false, - autolink: false, - HTMLAttributes: { - class: "text-primary hover:underline cursor-pointer", - }, - }), - BaseMentionExtension.configure({ - HTMLAttributes: { class: "mention" }, - }), - Image.configure({ - inline: false, - allowBase64: false, - HTMLAttributes: { - class: "rounded-md my-2", - style: "max-width: 100%; height: auto;", - }, - }), - Table.configure({ resizable: false }), - TableRow, - TableHeader, - TableCell, - Markdown, -]; - -// --------------------------------------------------------------------------- -// ReadonlyEditor -// --------------------------------------------------------------------------- - -interface ReadonlyEditorProps { - content: string; - className?: string; -} - -/** - * ReadonlyEditor — lightweight Tiptap wrapper for displaying markdown content. - * - * Content is converted from markdown to HTML via `marked` before loading, - * bypassing @tiptap/markdown's beta parser which drops complex content. - * The Markdown extension is kept for getMarkdown() serialization only. - */ -const ReadonlyEditor = memo(function ReadonlyEditor({ - content, - className, -}: ReadonlyEditorProps) { - const prevContentRef = useRef(content); - - const editor = useEditor({ - immediatelyRender: false, - editable: false, - content: markdownToHtml(content), - extensions, - editorProps: { - attributes: { - class: cn("rich-text-editor readonly text-sm", className), - }, - handleDOMEvents: { - click(_view, event) { - const target = event.target as HTMLElement; - // Skip links inside NodeView wrappers — they handle their own clicks - // (e.g. IssueMentionCard uses Next.js Link for client-side navigation) - if (target.closest("[data-node-view-wrapper]")) return false; - const link = target.closest("a"); - const href = link?.getAttribute("href"); - if (href && !href.startsWith("mention://")) { - event.preventDefault(); - window.open(href, "_blank", "noopener,noreferrer"); - return true; - } - return false; - }, - }, - }, - }); - - // Update content when prop changes (e.g. after editing a comment) - useEffect(() => { - if (!editor || content === prevContentRef.current) return; - prevContentRef.current = content; - editor.commands.setContent(markdownToHtml(content)); - }, [editor, content]); - - if (!editor) return null; - return ; -}); - -export { ReadonlyEditor, type ReadonlyEditorProps }; diff --git a/apps/web/components/common/rich-text-editor.tsx b/apps/web/components/common/rich-text-editor.tsx deleted file mode 100644 index 0af17464..00000000 --- a/apps/web/components/common/rich-text-editor.tsx +++ /dev/null @@ -1,418 +0,0 @@ -"use client"; - -import { - forwardRef, - useEffect, - useImperativeHandle, - useRef, -} from "react"; -import { useEditor, EditorContent, ReactNodeViewRenderer } from "@tiptap/react"; -import StarterKit from "@tiptap/starter-kit"; -import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; -import { common, createLowlight } from "lowlight"; -import Placeholder from "@tiptap/extension-placeholder"; -import Link from "@tiptap/extension-link"; -import Typography from "@tiptap/extension-typography"; -import Image from "@tiptap/extension-image"; -import TableRow from "@tiptap/extension-table-row"; -import TableHeader from "@tiptap/extension-table-header"; -import TableCell from "@tiptap/extension-table-cell"; -import { Table } from "@tiptap/extension-table"; -import { Markdown } from "@tiptap/markdown"; -import { Extension } from "@tiptap/core"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; -import { Slice } from "@tiptap/pm/model"; -import { cn } from "@/lib/utils"; -import type { UploadResult } from "@/shared/hooks/use-file-upload"; -import { BaseMentionExtension } from "./mention-extension"; -import { createMentionSuggestion } from "./mention-suggestion"; -import { CodeBlockView } from "./code-block-view"; -import { markdownToHtml } from "./markdown-to-html"; -import "./rich-text-editor.css"; - -const lowlight = createLowlight(common); - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -interface RichTextEditorProps { - defaultValue?: string; - onUpdate?: (markdown: string) => void; - placeholder?: string; - editable?: boolean; - className?: string; - debounceMs?: number; - onSubmit?: () => void; - onBlur?: () => void; - onUploadFile?: (file: File) => Promise; -} - -interface RichTextEditorRef { - getMarkdown: () => string; - clearContent: () => void; - focus: () => void; - /** Upload a file and insert it into the editor (blob preview → upload → replace). */ - uploadFile: (file: File) => void; -} - -const LinkExtension = Link.extend({ inclusive: false }).configure({ - openOnClick: true, - autolink: true, - linkOnPaste: false, - HTMLAttributes: { - class: "text-primary hover:underline cursor-pointer", - }, -}); - -const MentionExtension = BaseMentionExtension.configure({ - HTMLAttributes: { class: "mention" }, - suggestion: createMentionSuggestion(), -}); - -// --------------------------------------------------------------------------- -// Submit shortcut extension (Mod+Enter) -// --------------------------------------------------------------------------- - -function createSubmitExtension(onSubmit: () => void) { - return Extension.create({ - name: "submitShortcut", - addKeyboardShortcuts() { - return { - "Mod-Enter": () => { - onSubmit(); - return true; - }, - }; - }, - }); -} - -// --------------------------------------------------------------------------- -// Markdown paste extension — parse pasted markdown text as rich text -// --------------------------------------------------------------------------- - -function createMarkdownPasteExtension() { - return Extension.create({ - name: "markdownPaste", - addProseMirrorPlugins() { - const { editor } = this; - return [ - new Plugin({ - key: new PluginKey("markdownPaste"), - props: { - clipboardTextParser(text, _context, plainText) { - if (!plainText && editor.markdown) { - const json = editor.markdown.parse(text); - const node = editor.schema.nodeFromJSON(json); - return Slice.maxOpen(node.content); - } - // Plain text fallback - const p = editor.schema.nodes.paragraph!; - const doc = editor.schema.nodes.doc!; - const paragraph = p.create(null, text ? editor.schema.text(text) : undefined); - return new Slice(doc.create(null, paragraph).content, 0, 0); - }, - }, - }), - ]; - }, - }); -} - -// --------------------------------------------------------------------------- -// File upload extension (paste + drop) with blob URL instant preview -// --------------------------------------------------------------------------- - -function removeImageBySrc(editor: ReturnType, src: string) { - if (!editor) return; - const { tr } = editor.state; - let deleted = false; - editor.state.doc.descendants((node, pos) => { - if (deleted) return false; - if (node.type.name === "image" && node.attrs.src === src) { - tr.delete(pos, pos + node.nodeSize); - deleted = true; - return false; - } - }); - if (deleted) editor.view.dispatch(tr); -} - -/** - * Shared upload flow: insert blob preview → upload → replace with real URL. - * Used by both paste/drop (at cursor) and button upload (at end of doc). - */ -async function uploadAndInsertFile( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - editor: any, - file: File, - handler: (file: File) => Promise, - pos?: number, -) { - const isImage = file.type.startsWith("image/"); - - if (isImage) { - const blobUrl = URL.createObjectURL(file); - const imgAttrs = { src: blobUrl, alt: file.name, uploading: true }; - if (pos !== undefined) { - editor.chain().focus().insertContentAt(pos, { type: "image", attrs: imgAttrs }).run(); - } else { - editor.chain().focus().setImage(imgAttrs).run(); - } - - try { - const result = await handler(file); - if (result) { - const { tr } = editor.state; - editor.state.doc.descendants((node: { type: { name: string }; attrs: { src: string } }, nodePos: number) => { - if (node.type.name === "image" && node.attrs.src === blobUrl) { - tr.setNodeMarkup(nodePos, undefined, { - ...node.attrs, - src: result.link, - alt: result.filename, - uploading: false, - }); - } - }); - editor.view.dispatch(tr); - } else { - removeImageBySrc(editor, blobUrl); - } - } catch { - removeImageBySrc(editor, blobUrl); - } finally { - URL.revokeObjectURL(blobUrl); - } - } else { - // Non-image: upload first, then insert link - const result = await handler(file); - if (!result) return; - const linkText = `[${result.filename}](${result.link})`; - if (pos !== undefined) { - editor.chain().focus().insertContentAt(pos, linkText).run(); - } else { - editor.chain().focus().insertContent(linkText).run(); - } - } -} - -function createFileUploadExtension( - onUploadFileRef: React.RefObject<((file: File) => Promise) | undefined>, -) { - return Extension.create({ - name: "fileUpload", - addProseMirrorPlugins() { - const { editor } = this; - - const handleFiles = async (files: FileList) => { - const handler = onUploadFileRef.current; - if (!handler) return false; - for (const file of Array.from(files)) { - await uploadAndInsertFile(editor, file, handler); - } - return true; - }; - - return [ - new Plugin({ - key: new PluginKey("fileUpload"), - props: { - handlePaste(_view, event) { - const files = event.clipboardData?.files; - if (!files?.length) return false; - if (!onUploadFileRef.current) return false; - handleFiles(files); - return true; - }, - handleDrop(_view, event) { - const files = (event as DragEvent).dataTransfer?.files; - if (!files?.length) return false; - if (!onUploadFileRef.current) return false; - handleFiles(files); - return true; - }, - }, - }), - ]; - }, - }); -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -const RichTextEditor = forwardRef( - function RichTextEditor( - { - defaultValue = "", - onUpdate, - placeholder: placeholderText = "", - editable = true, - className, - debounceMs = 300, - onSubmit, - onBlur, - onUploadFile, - }, - ref, - ) { - const debounceRef = useRef>(undefined); - const onUpdateRef = useRef(onUpdate); - const onSubmitRef = useRef(onSubmit); - const onBlurRef = useRef(onBlur); - const onUploadFileRef = useRef(onUploadFile); - - // Helper to get markdown from @tiptap/markdown extension. - // Post-processes mention shortcodes [@ id="..." label="..."] → markdown - // links, using the Tiptap JSON doc for type info, in case the - // renderMarkdown override doesn't take effect. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const getEditorMarkdown = (ed: any): string => { - const md: string = ed?.getMarkdown?.() ?? ""; - if (!md || !md.includes("[@ ")) return md; - - // Build type map from editor JSON (which always has the type attr) - const json = ed?.getJSON?.(); - const typeMap = new Map(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function walk(node: any) { - if (node?.type === "mention" && node.attrs?.id) { - typeMap.set(node.attrs.id, node.attrs.type || "member"); - } - if (node?.content) node.content.forEach(walk); - } - if (json) walk(json); - - return md.replace( - /\[@\s+([^\]]*)\]/g, - (match: string, attrString: string) => { - const attrs: Record = {}; - const re = /(\w+)="([^"]*)"/g; - let m; - while ((m = re.exec(attrString)) !== null) { - if (m[1] && m[2] !== undefined) attrs[m[1]] = m[2]; - } - const { id, label } = attrs; - if (!id || !label) return match; - const type = typeMap.get(id) || "member"; - const display = type === "issue" ? label : `@${label}`; - return `[${display}](mention://${type}/${id})`; - }, - ); - }; - - // Keep refs in sync without recreating editor - onUpdateRef.current = onUpdate; - onSubmitRef.current = onSubmit; - onBlurRef.current = onBlur; - onUploadFileRef.current = onUploadFile; - - const editor = useEditor({ - immediatelyRender: false, - editable, - content: defaultValue ? markdownToHtml(defaultValue) : "", - extensions: [ - StarterKit.configure({ - heading: { levels: [1, 2, 3] }, - link: false, - codeBlock: false, - }), - CodeBlockLowlight.extend({ - addNodeView() { - return ReactNodeViewRenderer(CodeBlockView); - }, - }).configure({ lowlight }), - Placeholder.configure({ - placeholder: placeholderText, - }), - LinkExtension, - Typography, - MentionExtension, - Image.extend({ - addAttributes() { - return { - ...this.parent?.(), - uploading: { - default: false, - renderHTML: (attrs) => (attrs.uploading ? { "data-uploading": "" } : {}), - parseHTML: (el) => el.hasAttribute("data-uploading"), - }, - }; - }, - }).configure({ - inline: false, - allowBase64: false, - HTMLAttributes: { style: "max-width: 100%; height: auto;" }, - }), - Table.configure({ resizable: false }), - TableRow, - TableHeader, - TableCell, - Markdown, - createMarkdownPasteExtension(), - createSubmitExtension(() => onSubmitRef.current?.()), - createFileUploadExtension(onUploadFileRef), - ], - onUpdate: ({ editor: ed }) => { - if (!onUpdateRef.current) return; - if (debounceRef.current) clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(() => { - onUpdateRef.current?.(ed.getMarkdown()); - }, debounceMs); - }, - onBlur: () => { - onBlurRef.current?.(); - }, - editorProps: { - handleDOMEvents: { - click(_view, event) { - if (event.metaKey || event.ctrlKey) { - const link = (event.target as HTMLElement).closest("a"); - const href = link?.getAttribute("href"); - if (href && !href.startsWith("mention://")) { - window.open(href, "_blank", "noopener,noreferrer"); - event.preventDefault(); - return true; - } - } - return false; - }, - }, - attributes: { - class: cn("rich-text-editor text-sm outline-none", className), - }, - }, - }); - - // Cleanup debounce on unmount - useEffect(() => { - return () => { - if (debounceRef.current) clearTimeout(debounceRef.current); - }; - }, []); - - useImperativeHandle(ref, () => ({ - getMarkdown: () => editor?.getMarkdown() ?? "", - clearContent: () => { - editor?.commands.clearContent(); - }, - focus: () => { - editor?.commands.focus(); - }, - uploadFile: (file: File) => { - if (!editor || !onUploadFileRef.current) return; - // Insert at end of doc to avoid replacing selection - const endPos = editor.state.doc.content.size; - uploadAndInsertFile(editor, file, onUploadFileRef.current, endPos); - }, - })); - - if (!editor) return null; - - return ; - }, -); - -export { RichTextEditor, type RichTextEditorProps, type RichTextEditorRef }; diff --git a/apps/web/components/markdown/Markdown.tsx b/apps/web/components/markdown/Markdown.tsx index 583780cf..c25e26f0 100644 --- a/apps/web/components/markdown/Markdown.tsx +++ b/apps/web/components/markdown/Markdown.tsx @@ -5,6 +5,7 @@ import remarkGfm from 'remark-gfm' import { cn } from '@/lib/utils' import { CodeBlock, InlineCode } from './CodeBlock' import { preprocessLinks } from './linkify' +import { preprocessMentionShortcodes } from './mentions' import { IssueMentionCard } from '@/features/issues/components/issue-mention-card' /** @@ -53,27 +54,6 @@ function urlTransform(url: string): string { return defaultUrlTransform(url) } -/** - * Convert legacy mention shortcodes [@ id="UUID" label="LABEL"] to markdown - * link format [@LABEL](mention://member/UUID) so they render as styled mentions. - */ -function preprocessMentionShortcodes(text: string): string { - if (!text.includes('[@ ')) return text - return text.replace( - /\[@\s+([^\]]*)\]/g, - (match, attrString: string) => { - const attrs: Record = {} - const re = /(\w+)="([^"]*)"/g - let m - while ((m = re.exec(attrString)) !== null) { - if (m[1] && m[2] !== undefined) attrs[m[1]] = m[2] - } - const { id, label } = attrs - if (!id || !label) return match - return `[@${label}](mention://member/${id})` - } - ) -} // File path detection regex - matches paths starting with /, ~/, or ./ const FILE_PATH_REGEX = diff --git a/apps/web/components/markdown/index.ts b/apps/web/components/markdown/index.ts index a2a89c50..7d05984d 100644 --- a/apps/web/components/markdown/index.ts +++ b/apps/web/components/markdown/index.ts @@ -2,3 +2,4 @@ export { Markdown, MemoizedMarkdown, type MarkdownProps, type RenderMode } from export { CodeBlock, InlineCode, type CodeBlockProps } from './CodeBlock' export { StreamingMarkdown, type StreamingMarkdownProps } from './StreamingMarkdown' export { preprocessLinks, detectLinks, hasLinks } from './linkify' +export { preprocessMentionShortcodes } from './mentions' diff --git a/apps/web/components/markdown/mentions.ts b/apps/web/components/markdown/mentions.ts new file mode 100644 index 00000000..06b041df --- /dev/null +++ b/apps/web/components/markdown/mentions.ts @@ -0,0 +1,25 @@ +/** + * Convert legacy mention shortcodes [@ id="UUID" label="LABEL"] to the + * standard markdown link format [@LABEL](mention://member/UUID). + * + * These shortcodes exist in older database records from a previous mention + * serialization format. This function normalises them so downstream parsers + * (Tiptap @tiptap/markdown, react-markdown) only need to handle one syntax. + */ +export function preprocessMentionShortcodes(text: string): string { + if (!text.includes("[@ ")) return text; + return text.replace( + /\[@\s+([^\]]*)\]/g, + (match, attrString: string) => { + const attrs: Record = {}; + const re = /(\w+)="([^"]*)"/g; + let m; + while ((m = re.exec(attrString)) !== null) { + if (m[1] && m[2] !== undefined) attrs[m[1]] = m[2]; + } + const { id, label } = attrs; + if (!id || !label) return match; + return `[@${label}](mention://member/${id})`; + }, + ); +} diff --git a/apps/web/components/common/rich-text-editor.css b/apps/web/features/editor/content-editor.css similarity index 100% rename from apps/web/components/common/rich-text-editor.css rename to apps/web/features/editor/content-editor.css diff --git a/apps/web/features/editor/content-editor.tsx b/apps/web/features/editor/content-editor.tsx new file mode 100644 index 00000000..1517bcbf --- /dev/null +++ b/apps/web/features/editor/content-editor.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, +} from "react"; +import { useEditor, EditorContent } from "@tiptap/react"; +import { cn } from "@/lib/utils"; +import type { UploadResult } from "@/shared/hooks/use-file-upload"; +import { createEditorExtensions } from "./extensions"; +import { uploadAndInsertFile } from "./extensions/file-upload"; +import { preprocessMarkdown } from "./utils/preprocess"; +import "./content-editor.css"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface ContentEditorProps { + defaultValue?: string; + onUpdate?: (markdown: string) => void; + placeholder?: string; + editable?: boolean; + className?: string; + debounceMs?: number; + onSubmit?: () => void; + onBlur?: () => void; + onUploadFile?: (file: File) => Promise; +} + +interface ContentEditorRef { + getMarkdown: () => string; + clearContent: () => void; + focus: () => void; + uploadFile: (file: File) => void; +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +const ContentEditor = forwardRef( + function ContentEditor( + { + defaultValue = "", + onUpdate, + placeholder: placeholderText = "", + editable = true, + className, + debounceMs = 300, + onSubmit, + onBlur, + onUploadFile, + }, + ref, + ) { + const debounceRef = useRef>(undefined); + const onUpdateRef = useRef(onUpdate); + const onSubmitRef = useRef(onSubmit); + const onBlurRef = useRef(onBlur); + const onUploadFileRef = useRef(onUploadFile); + const prevContentRef = useRef(defaultValue); + + // Keep refs in sync without recreating editor + onUpdateRef.current = onUpdate; + onSubmitRef.current = onSubmit; + onBlurRef.current = onBlur; + onUploadFileRef.current = onUploadFile; + + const editor = useEditor({ + immediatelyRender: false, + editable, + content: defaultValue ? preprocessMarkdown(defaultValue) : "", + contentType: defaultValue ? "markdown" : undefined, + extensions: createEditorExtensions({ + editable, + placeholder: placeholderText, + onSubmitRef, + onUploadFileRef, + }), + onUpdate: ({ editor: ed }) => { + if (!onUpdateRef.current) return; + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + onUpdateRef.current?.(ed.getMarkdown()); + }, debounceMs); + }, + onBlur: () => { + onBlurRef.current?.(); + }, + editorProps: { + handleDOMEvents: { + click(_view, event) { + const target = event.target as HTMLElement; + // Skip links inside NodeView wrappers — they handle their own clicks + if (target.closest("[data-node-view-wrapper]")) return false; + + const link = target.closest("a"); + const href = link?.getAttribute("href"); + if (!href || href.startsWith("mention://")) return false; + + if (!editable) { + // Readonly: any click on link opens new tab + event.preventDefault(); + window.open(href, "_blank", "noopener,noreferrer"); + return true; + } + + if (event.metaKey || event.ctrlKey) { + // Edit mode: Cmd/Ctrl+click opens link + window.open(href, "_blank", "noopener,noreferrer"); + event.preventDefault(); + return true; + } + + return false; + }, + }, + attributes: { + class: cn( + "rich-text-editor text-sm outline-none", + !editable && "readonly", + className, + ), + }, + }, + }); + + // Cleanup debounce on unmount + useEffect(() => { + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }; + }, []); + + // Readonly content update: when defaultValue changes and editor is readonly, + // re-set the content (e.g. after editing a comment, the readonly view updates) + useEffect(() => { + if (!editor || editable) return; + if (defaultValue === prevContentRef.current) return; + prevContentRef.current = defaultValue; + const processed = defaultValue ? preprocessMarkdown(defaultValue) : ""; + if (processed) { + editor.commands.setContent(processed, { contentType: "markdown" }); + } else { + editor.commands.clearContent(); + } + }, [editor, editable, defaultValue]); + + useImperativeHandle(ref, () => ({ + getMarkdown: () => editor?.getMarkdown() ?? "", + clearContent: () => { + editor?.commands.clearContent(); + }, + focus: () => { + editor?.commands.focus(); + }, + uploadFile: (file: File) => { + if (!editor || !onUploadFileRef.current) return; + const endPos = editor.state.doc.content.size; + uploadAndInsertFile(editor, file, onUploadFileRef.current, endPos); + }, + })); + + if (!editor) return null; + + return ; + }, +); + +export { ContentEditor, type ContentEditorProps, type ContentEditorRef }; diff --git a/apps/web/components/common/code-block-view.tsx b/apps/web/features/editor/extensions/code-block-view.tsx similarity index 100% rename from apps/web/components/common/code-block-view.tsx rename to apps/web/features/editor/extensions/code-block-view.tsx diff --git a/apps/web/features/editor/extensions/file-upload.ts b/apps/web/features/editor/extensions/file-upload.ts new file mode 100644 index 00000000..d6469fd4 --- /dev/null +++ b/apps/web/features/editor/extensions/file-upload.ts @@ -0,0 +1,119 @@ +import { Extension } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import type { UploadResult } from "@/shared/hooks/use-file-upload"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function removeImageBySrc(editor: any, src: string) { + if (!editor) return; + const { tr } = editor.state; + let deleted = false; + editor.state.doc.descendants((node: any, pos: number) => { + if (deleted) return false; + if (node.type.name === "image" && node.attrs.src === src) { + tr.delete(pos, pos + node.nodeSize); + deleted = true; + return false; + } + }); + if (deleted) editor.view.dispatch(tr); +} + +/** + * Shared upload flow: insert blob preview → upload → replace with real URL. + * Used by both paste/drop (at cursor) and button upload (at end of doc). + */ +export async function uploadAndInsertFile( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + editor: any, + file: File, + handler: (file: File) => Promise, + pos?: number, +) { + const isImage = file.type.startsWith("image/"); + + if (isImage) { + const blobUrl = URL.createObjectURL(file); + const imgAttrs = { src: blobUrl, alt: file.name, uploading: true }; + if (pos !== undefined) { + editor.chain().focus().insertContentAt(pos, { type: "image", attrs: imgAttrs }).run(); + } else { + editor.chain().focus().setImage(imgAttrs).run(); + } + + try { + const result = await handler(file); + if (result) { + const { tr } = editor.state; + editor.state.doc.descendants((node: { type: { name: string }; attrs: { src: string } }, nodePos: number) => { + if (node.type.name === "image" && node.attrs.src === blobUrl) { + tr.setNodeMarkup(nodePos, undefined, { + ...node.attrs, + src: result.link, + alt: result.filename, + uploading: false, + }); + } + }); + editor.view.dispatch(tr); + } else { + removeImageBySrc(editor, blobUrl); + } + } catch { + removeImageBySrc(editor, blobUrl); + } finally { + URL.revokeObjectURL(blobUrl); + } + } else { + // Non-image: upload first, then insert link + const result = await handler(file); + if (!result) return; + const linkText = `[${result.filename}](${result.link})`; + if (pos !== undefined) { + editor.chain().focus().insertContentAt(pos, linkText).run(); + } else { + editor.chain().focus().insertContent(linkText).run(); + } + } +} + +export function createFileUploadExtension( + onUploadFileRef: React.RefObject<((file: File) => Promise) | undefined>, +) { + return Extension.create({ + name: "fileUpload", + addProseMirrorPlugins() { + const { editor } = this; + + const handleFiles = async (files: FileList) => { + const handler = onUploadFileRef.current; + if (!handler) return false; + for (const file of Array.from(files)) { + await uploadAndInsertFile(editor, file, handler); + } + return true; + }; + + return [ + new Plugin({ + key: new PluginKey("fileUpload"), + props: { + handlePaste(_view, event) { + const files = event.clipboardData?.files; + if (!files?.length) return false; + if (!onUploadFileRef.current) return false; + handleFiles(files); + return true; + }, + handleDrop(_view, event) { + const files = (event as DragEvent).dataTransfer?.files; + if (!files?.length) return false; + if (!onUploadFileRef.current) return false; + handleFiles(files); + return true; + }, + }, + }), + ]; + }, + }); +} diff --git a/apps/web/features/editor/extensions/index.ts b/apps/web/features/editor/extensions/index.ts new file mode 100644 index 00000000..94c9bb00 --- /dev/null +++ b/apps/web/features/editor/extensions/index.ts @@ -0,0 +1,110 @@ +import type { RefObject } from "react"; +import StarterKit from "@tiptap/starter-kit"; +import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; +import { common, createLowlight } from "lowlight"; +import Placeholder from "@tiptap/extension-placeholder"; +import Link from "@tiptap/extension-link"; +import Typography from "@tiptap/extension-typography"; +import Image from "@tiptap/extension-image"; +import TableRow from "@tiptap/extension-table-row"; +import TableHeader from "@tiptap/extension-table-header"; +import TableCell from "@tiptap/extension-table-cell"; +import { Table } from "@tiptap/extension-table"; +import { Markdown } from "@tiptap/markdown"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import type { AnyExtension } from "@tiptap/core"; +import type { UploadResult } from "@/shared/hooks/use-file-upload"; +import { BaseMentionExtension } from "./mention-extension"; +import { createMentionSuggestion } from "./mention-suggestion"; +import { CodeBlockView } from "./code-block-view"; +import { createMarkdownPasteExtension } from "./markdown-paste"; +import { createSubmitExtension } from "./submit-shortcut"; +import { createFileUploadExtension } from "./file-upload"; + +const lowlight = createLowlight(common); + +const LinkEditable = Link.extend({ inclusive: false }).configure({ + openOnClick: true, + autolink: true, + linkOnPaste: false, + HTMLAttributes: { + class: "text-primary hover:underline cursor-pointer", + }, +}); + +const LinkReadonly = Link.configure({ + openOnClick: false, + autolink: false, + HTMLAttributes: { + class: "text-primary hover:underline cursor-pointer", + }, +}); + +const ImageExtension = Image.extend({ + addAttributes() { + return { + ...this.parent?.(), + uploading: { + default: false, + renderHTML: (attrs: Record) => + attrs.uploading ? { "data-uploading": "" } : {}, + parseHTML: (el: HTMLElement) => el.hasAttribute("data-uploading"), + }, + }; + }, +}).configure({ + inline: false, + allowBase64: false, + HTMLAttributes: { style: "max-width: 100%; height: auto;" }, +}); + +export interface EditorExtensionsOptions { + editable: boolean; + placeholder?: string; + onSubmitRef?: RefObject<(() => void) | undefined>; + onUploadFileRef?: RefObject< + ((file: File) => Promise) | undefined + >; +} + +export function createEditorExtensions( + options: EditorExtensionsOptions, +): AnyExtension[] { + const { editable, placeholder: placeholderText } = options; + + const extensions: AnyExtension[] = [ + StarterKit.configure({ + heading: { levels: [1, 2, 3] }, + link: false, + codeBlock: false, + }), + CodeBlockLowlight.extend({ + addNodeView() { + return ReactNodeViewRenderer(CodeBlockView); + }, + }).configure({ lowlight }), + editable ? LinkEditable : LinkReadonly, + ImageExtension, + Table.configure({ resizable: false }), + TableRow, + TableHeader, + TableCell, + Markdown, + BaseMentionExtension.configure({ + HTMLAttributes: { class: "mention" }, + ...(editable ? { suggestion: createMentionSuggestion() } : {}), + }), + ]; + + if (editable) { + extensions.push( + Typography, + Placeholder.configure({ placeholder: placeholderText }), + createMarkdownPasteExtension(), + createSubmitExtension(() => options.onSubmitRef?.current?.()), + createFileUploadExtension(options.onUploadFileRef!), + ); + } + + return extensions; +} diff --git a/apps/web/features/editor/extensions/markdown-paste.ts b/apps/web/features/editor/extensions/markdown-paste.ts new file mode 100644 index 00000000..fc549e8f --- /dev/null +++ b/apps/web/features/editor/extensions/markdown-paste.ts @@ -0,0 +1,31 @@ +import { Extension } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { Slice } from "@tiptap/pm/model"; + +export function createMarkdownPasteExtension() { + return Extension.create({ + name: "markdownPaste", + addProseMirrorPlugins() { + const { editor } = this; + return [ + new Plugin({ + key: new PluginKey("markdownPaste"), + props: { + clipboardTextParser(text, _context, plainText) { + if (!plainText && editor.markdown) { + const json = editor.markdown.parse(text); + const node = editor.schema.nodeFromJSON(json); + return Slice.maxOpen(node.content); + } + // Plain text fallback + const p = editor.schema.nodes.paragraph!; + const doc = editor.schema.nodes.doc!; + const paragraph = p.create(null, text ? editor.schema.text(text) : undefined); + return new Slice(doc.create(null, paragraph).content, 0, 0); + }, + }, + }), + ]; + }, + }); +} diff --git a/apps/web/components/common/mention-extension.ts b/apps/web/features/editor/extensions/mention-extension.ts similarity index 69% rename from apps/web/components/common/mention-extension.ts rename to apps/web/features/editor/extensions/mention-extension.ts index 7764bc4d..4b1bafe9 100644 --- a/apps/web/components/common/mention-extension.ts +++ b/apps/web/features/editor/extensions/mention-extension.ts @@ -3,19 +3,6 @@ import { mergeAttributes } from "@tiptap/core"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { MentionView } from "./mention-view"; -/** - * BaseMentionExtension — shared mention extension for both editing and readonly modes. - * - * Includes: NodeView (MentionView), renderHTML, addAttributes, markdownTokenizer, - * parseMarkdown, renderMarkdown. - * - * MentionView renders identically in both modes (issue → inline card, member/agent → span). - * Only difference: in readonly mode, issue mentions are clickable links. - * - * Usage: - * Editing: BaseMentionExtension.configure({ suggestion: createMentionSuggestion() }) - * Readonly: BaseMentionExtension.configure({}) - */ export const BaseMentionExtension = Mention.extend({ addNodeView() { return ReactNodeViewRenderer(MentionView); @@ -48,7 +35,6 @@ export const BaseMentionExtension = Mention.extend({ }, }; }, - // @tiptap/markdown: custom tokenizer to parse [@Label](mention://type/id) markdownTokenizer: { name: "mention", level: "inline" as const, @@ -56,7 +42,6 @@ export const BaseMentionExtension = Mention.extend({ return src.search(/\[@?[^\]]+\]\(mention:\/\//); }, tokenize(src: string) { - // Matches both [@Label](mention://type/id) and [Label](mention://issue/id) const match = src.match( /^\[@?([^\]]+)\]\(mention:\/\/(\w+)\/([^)]+)\)/, ); @@ -68,11 +53,9 @@ export const BaseMentionExtension = Mention.extend({ }; }, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any parseMarkdown: (token: any, helpers: any) => { return helpers.createNode("mention", token.attributes); }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any renderMarkdown: (node: any) => { const { id, label, type = "member" } = node.attrs || {}; const prefix = type === "issue" ? "" : "@"; diff --git a/apps/web/components/common/mention-suggestion.tsx b/apps/web/features/editor/extensions/mention-suggestion.tsx similarity index 100% rename from apps/web/components/common/mention-suggestion.tsx rename to apps/web/features/editor/extensions/mention-suggestion.tsx diff --git a/apps/web/components/common/mention-view.tsx b/apps/web/features/editor/extensions/mention-view.tsx similarity index 80% rename from apps/web/components/common/mention-view.tsx rename to apps/web/features/editor/extensions/mention-view.tsx index 4ac5df6a..d90c1d43 100644 --- a/apps/web/components/common/mention-view.tsx +++ b/apps/web/features/editor/extensions/mention-view.tsx @@ -5,12 +5,6 @@ import type { NodeViewProps } from "@tiptap/react"; import { useIssueStore } from "@/features/issues/store"; import { StatusIcon } from "@/features/issues/components/status-icon"; -/** - * MentionView — shared NodeView for mention nodes (both editing and readonly). - * - * Rendering and behavior are identical in both modes. - * Issue mentions are always clickable (open in new tab). - */ export function MentionView({ node }: NodeViewProps) { const { type, id, label } = node.attrs; @@ -29,10 +23,6 @@ export function MentionView({ node }: NodeViewProps) { ); } -// --------------------------------------------------------------------------- -// IssueMention — inline card, always opens in new tab -// --------------------------------------------------------------------------- - function IssueMention({ issueId, fallbackLabel, diff --git a/apps/web/features/editor/extensions/submit-shortcut.ts b/apps/web/features/editor/extensions/submit-shortcut.ts new file mode 100644 index 00000000..3708e4c9 --- /dev/null +++ b/apps/web/features/editor/extensions/submit-shortcut.ts @@ -0,0 +1,15 @@ +import { Extension } from "@tiptap/core"; + +export function createSubmitExtension(onSubmit: () => void) { + return Extension.create({ + name: "submitShortcut", + addKeyboardShortcuts() { + return { + "Mod-Enter": () => { + onSubmit(); + return true; + }, + }; + }, + }); +} diff --git a/apps/web/features/editor/index.ts b/apps/web/features/editor/index.ts new file mode 100644 index 00000000..f82dd6cc --- /dev/null +++ b/apps/web/features/editor/index.ts @@ -0,0 +1,11 @@ +export { + ContentEditor, + type ContentEditorProps, + type ContentEditorRef, +} from "./content-editor"; +export { + TitleEditor, + type TitleEditorProps, + type TitleEditorRef, +} from "./title-editor"; +export { copyMarkdown } from "./utils/clipboard"; diff --git a/apps/web/components/common/title-editor.css b/apps/web/features/editor/title-editor.css similarity index 100% rename from apps/web/components/common/title-editor.css rename to apps/web/features/editor/title-editor.css diff --git a/apps/web/components/common/title-editor.tsx b/apps/web/features/editor/title-editor.tsx similarity index 100% rename from apps/web/components/common/title-editor.tsx rename to apps/web/features/editor/title-editor.tsx diff --git a/apps/web/features/editor/utils/clipboard.ts b/apps/web/features/editor/utils/clipboard.ts new file mode 100644 index 00000000..4b661374 --- /dev/null +++ b/apps/web/features/editor/utils/clipboard.ts @@ -0,0 +1,6 @@ +/** + * Copy markdown content to the clipboard. + */ +export async function copyMarkdown(markdown: string): Promise { + await navigator.clipboard.writeText(markdown); +} diff --git a/apps/web/features/editor/utils/preprocess.ts b/apps/web/features/editor/utils/preprocess.ts new file mode 100644 index 00000000..d820b79a --- /dev/null +++ b/apps/web/features/editor/utils/preprocess.ts @@ -0,0 +1,16 @@ +import { preprocessLinks } from "@/components/markdown/linkify"; +import { preprocessMentionShortcodes } from "@/components/markdown/mentions"; + +/** + * Preprocess a markdown string before loading into Tiptap via contentType: 'markdown'. + * + * Two string→string transforms: + * 1. Legacy mention shortcodes [@ id="..." label="..."] → [@Label](mention://member/id) + * 2. Raw URLs → markdown links (so they render as clickable Link nodes) + */ +export function preprocessMarkdown(markdown: string): string { + if (!markdown) return ""; + const step1 = preprocessMentionShortcodes(markdown); + const step2 = preprocessLinks(step1); + return step2; +} diff --git a/apps/web/features/issues/components/comment-card.tsx b/apps/web/features/issues/components/comment-card.tsx index 110a7041..0ad2ecc2 100644 --- a/apps/web/features/issues/components/comment-card.tsx +++ b/apps/web/features/issues/components/comment-card.tsx @@ -30,8 +30,7 @@ import { QuickEmojiPicker } from "@/components/common/quick-emoji-picker"; import { cn } from "@/lib/utils"; import { useActorName } from "@/features/workspace"; import { timeAgo } from "@/shared/utils"; -import { RichTextEditor, type RichTextEditorRef } from "@/components/common/rich-text-editor"; -import { ReadonlyEditor } from "@/components/common/readonly-editor"; +import { ContentEditor, type ContentEditorRef, copyMarkdown } from "@/features/editor"; import { FileUploadButton } from "@/components/common/file-upload-button"; import { useFileUpload } from "@/shared/hooks/use-file-upload"; import { ReplyInput } from "./reply-input"; @@ -112,7 +111,7 @@ function CommentRow({ }) { const { getActorName } = useActorName(); const [editing, setEditing] = useState(false); - const editEditorRef = useRef(null); + const editEditorRef = useRef(null); const cancelledRef = useRef(false); const { uploadWithToast } = useFileUpload(); @@ -186,7 +185,7 @@ function CommentRow({ /> { - navigator.clipboard.writeText(entry.content ?? ""); + copyMarkdown(entry.content ?? ""); toast.success("Copied"); }}> @@ -223,7 +222,7 @@ function CommentRow({ onKeyDown={(e) => { if (e.key === "Escape") cancelEdit(); }} >
-
- +
{!isTemp && ( (null); + const editEditorRef = useRef(null); const cancelledRef = useRef(false); const isOwn = entry.actor_type === "member" && entry.actor_id === currentUserId; @@ -387,7 +386,7 @@ function CommentCard({ /> { - navigator.clipboard.writeText(entry.content ?? ""); + copyMarkdown(entry.content ?? ""); toast.success("Copied"); }}> @@ -430,7 +429,7 @@ function CommentCard({ onKeyDown={(e) => { if (e.key === "Escape") cancelEdit(); }} >
-
- +
{!isTemp && ( (null); + const editorRef = useRef(null); const [isEmpty, setIsEmpty] = useState(true); const [submitting, setSubmitting] = useState(false); const { uploadWithToast } = useFileUpload(); @@ -38,7 +38,7 @@ function CommentInput({ issueId, onSubmit }: CommentInputProps) { return (
- setIsEmpty(!md.trim())} diff --git a/apps/web/features/issues/components/issue-detail.tsx b/apps/web/features/issues/components/issue-detail.tsx index 742db74c..e986e63a 100644 --- a/apps/web/features/issues/components/issue-detail.tsx +++ b/apps/web/features/issues/components/issue-detail.tsx @@ -44,9 +44,9 @@ import { DropdownMenuSubContent, } from "@/components/ui/dropdown-menu"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"; -import { RichTextEditor } from "@/components/common/rich-text-editor"; +import { ContentEditor, type ContentEditorRef } from "@/features/editor"; import { FileUploadButton } from "@/components/common/file-upload-button"; -import { TitleEditor } from "@/components/common/title-editor"; +import { TitleEditor } from "@/features/editor"; import { Tooltip, TooltipTrigger, @@ -287,7 +287,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo [issue, id], ); - const descEditorRef = useRef(null); + const descEditorRef = useRef(null); const handleDescriptionUpload = useCallback( (file: File) => uploadWithToast(file, { issueId: id }), [uploadWithToast, id], @@ -641,7 +641,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo }} /> - (null); + const editorRef = useRef(null); const measureRef = useRef(null); const [isEmpty, setIsEmpty] = useState(true); const [isExpanded, setIsExpanded] = useState(false); @@ -87,7 +87,7 @@ function ReplyInput({ >
- setIsEmpty(!md.trim())} diff --git a/apps/web/features/modals/create-issue.tsx b/apps/web/features/modals/create-issue.tsx index df8252df..bc11bb46 100644 --- a/apps/web/features/modals/create-issue.tsx +++ b/apps/web/features/modals/create-issue.tsx @@ -25,8 +25,8 @@ import { import { Calendar } from "@/components/ui/calendar"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { Button } from "@/components/ui/button"; -import { RichTextEditor, type RichTextEditorRef } from "@/components/common/rich-text-editor"; -import { TitleEditor } from "@/components/common/title-editor"; +import { ContentEditor, type ContentEditorRef } from "@/features/editor"; +import { TitleEditor } from "@/features/editor"; import { StatusIcon, PriorityIcon } from "@/features/issues/components"; import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@/features/issues/config"; import { useWorkspaceStore, useActorName } from "@/features/workspace"; @@ -77,7 +77,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data? const clearDraft = useIssueDraftStore((s) => s.clearDraft); const [title, setTitle] = useState(draft.title); - const descEditorRef = useRef(null); + const descEditorRef = useRef(null); const [status, setStatus] = useState((data?.status as IssueStatus) || draft.status); const [priority, setPriority] = useState(draft.priority); const [submitting, setSubmitting] = useState(false); @@ -231,7 +231,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data? {/* Description — takes remaining space */}
- =7.21.4' - '@tiptap/core@3.20.5': - resolution: {integrity: sha512-Pkjd41UJ4F6Z8cPV+gEvqnt1VhY2g66xMjbpxREs0ECA5jRezCNKSZcc2pueQRTMtmn1SaSzGM9U/ifhVlVYOA==} + '@tiptap/core@3.22.1': + resolution: {integrity: sha512-6wPNhkdLIGYiKAGqepDCRtR0TYGJxV40SwOEN2vlPhsXqAgzmyG37UyREj5pGH5xTekugqMCgCnyRg7m5nYoYQ==} peerDependencies: - '@tiptap/pm': ^3.20.5 + '@tiptap/pm': ^3.22.1 - '@tiptap/extension-blockquote@3.20.5': - resolution: {integrity: sha512-0wU6H/MWWes0rGzgSW6MMU6YDs/3ofUDkqmqCqmb+Siu1ZD0bpzOYpBtujgOYDY8moB9+zCE3G9HSYGcmZxHew==} + '@tiptap/extension-blockquote@3.22.1': + resolution: {integrity: sha512-omPsJ/IMAZYhXqOaEenYE+HA9U2zju5rQbAn6Xktynvr4A5P95jqkgAwncXB82pCkNYU/uYxi51vyTweTeEUHA==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-bold@3.20.5': - resolution: {integrity: sha512-hraiiWkF58n8Jy0Wl3OGwjCTrGWwZZxez/IlexrzKQ/nMFdjDpensZucWwu59zhAM9fqZwGSLDtCFuak03WKnA==} + '@tiptap/extension-bold@3.22.1': + resolution: {integrity: sha512-0+q6Apu1Vx2+ReB2ktTpBrQ5/dCvGzTkJCy+MZ/t8WBcybqFXOKYRCr/i/VGPDpXZttxpk0EPl0+ao+NVcUTAA==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-bubble-menu@3.20.5': - resolution: {integrity: sha512-6FsASu4o32bp3FzBVb5N2ERjrBy83DtJQAGv9/ycYqsgv2kq9DNlhvtNI7GPiTW7a73ZcImjIX+jEWrARbzOlQ==} + '@tiptap/extension-bubble-menu@3.22.1': + resolution: {integrity: sha512-JJI63N55hLPjfqHgBnbG1ORZTXJiswnfBkfNd8YKytCC8D++g5qX3UMObxmJKLMBRGyqjEi6krzOyYtOix5ALA==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 - '@tiptap/extension-bullet-list@3.20.5': - resolution: {integrity: sha512-MT3321R6F8AoVUEMJ5RiI0PQMenwvtmrSXoO1ehPCWq5TrSJLyXeZMJvZU+1CgfXk4XQU70RN78ib5+Zg+/FCg==} + '@tiptap/extension-bullet-list@3.22.1': + resolution: {integrity: sha512-83L+4N2JziWORbWtlsM0xBm3LOKIw4YtIm+Kh4amV5kGvIgIL5I1KYzoxv20qjgFX2k08LtLMwPdvPSPSh4e7g==} peerDependencies: - '@tiptap/extension-list': ^3.20.5 + '@tiptap/extension-list': ^3.22.1 - '@tiptap/extension-code-block-lowlight@3.20.5': - resolution: {integrity: sha512-EINMkflwiUfCkBTAj1meP+nwEEUyXKmJF4yQVHzbt/iIswMtIc/7qvyld92VBgXWJkc+vo/lIPioaZGoSO7TsQ==} + '@tiptap/extension-code-block-lowlight@3.22.1': + resolution: {integrity: sha512-6Dj5AKGTi05EYqKJYS2NXpU72TQ8SVWOLDgnbsPDhoyl9hV4cnQ+1imnytfFrLX3wu5aOcKyk3tgV7BsNLIdvg==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/extension-code-block': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/extension-code-block': ^3.22.1 + '@tiptap/pm': ^3.22.1 highlight.js: ^11 lowlight: ^2 || ^3 - '@tiptap/extension-code-block@3.20.5': - resolution: {integrity: sha512-0YZnqfqZ1IjzKBM4aezw8j3LZWJFEfs4+mbizHNlnZSYpKzpESYLeaLWGO5SpqF9Z8tmYmSoCaf0fqi5LwgdIA==} + '@tiptap/extension-code-block@3.22.1': + resolution: {integrity: sha512-fr3b1seFsAeYHtPAb9fbATkGcgyfStD05GHsZXFLh7yCpf2ejWLNxdWJT/g+FggSEHYFKCXT06aixk0WbtRcWw==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 - '@tiptap/extension-code@3.20.5': - resolution: {integrity: sha512-jBZK/CfdMvg1gkNK/zNAk02IExpBPwUfNLRPiJvGhReL2Q73naKxZGQGp+5Lej9VaeFB70UKuRma/iIzuZbgsA==} + '@tiptap/extension-code@3.22.1': + resolution: {integrity: sha512-Ze+hjSLLCn+5gVpuE/Uv7mQ83AlG5A9OPsuDoyzTpJ2XNvZP2iZdwQMGqwXKC8eH7fIOJN6XQ3IDv/EhltQx/Q==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-document@3.20.5': - resolution: {integrity: sha512-BpNGHtOTAjjs/6QbkrafMTlaJqb0gsPngFzd5rB0csxx7rYRE9nIEY+oZ44qMw161+2YB4u20L17SX2mUJANBw==} + '@tiptap/extension-document@3.22.1': + resolution: {integrity: sha512-fBI/+PGtK6pzitqjSSSYL2+uZglX6T53zb5nLEmN/q8q7FzUuUpglp8toHVhBG05WDk4vx6Z7bC95uyxkYdoAA==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-dropcursor@3.20.5': - resolution: {integrity: sha512-/lDG9OjvAv0ynmgFH17mt/GUeGT5bqu0iPW8JMgaRqlKawk+uUIv5SF5WkXS4SwxXih+hXdPEQD3PWZnxlQxAQ==} + '@tiptap/extension-dropcursor@3.22.1': + resolution: {integrity: sha512-PuSNoTROZB564KpTG9ExVB3CsfRa0ridHx+1sWZajOBVZJiXSn4QlS/ShS509SOx8z17DyxEw06IH//OHY9XyQ==} peerDependencies: - '@tiptap/extensions': ^3.20.5 + '@tiptap/extensions': ^3.22.1 - '@tiptap/extension-floating-menu@3.20.5': - resolution: {integrity: sha512-mTzBNUeAocinrxa5xV+5hGnnNCQB0pVI1GSBwUTHwdB7jNwBqfKAILmtLZONgmhxKWLmGa6WCA59sk+yDI+N0A==} + '@tiptap/extension-floating-menu@3.22.1': + resolution: {integrity: sha512-TaZqmaoKv36FzbKTrBkkv74o0t8dYTftNZ7NotBqfSki0BB2PupTCJHafdu1YI0zmJ3xEzjB/XKcKPz2+10sDA==} peerDependencies: '@floating-ui/dom': ^1.0.0 - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 - '@tiptap/extension-gapcursor@3.20.5': - resolution: {integrity: sha512-H+bRr+mqU/DQq1vfoMlppK1o+RbfSKYBMIcAMHWOez+C96MWfj5bhooVU2HLtl4XGmQxKGr3oEOCKDPdtRNThg==} + '@tiptap/extension-gapcursor@3.22.1': + resolution: {integrity: sha512-qqsyy7unWM3elv+7ru+6paKAnw1PZTvjNVQu3UzB6d556Gx2uE4isXJNdBaslBZdp2EoaYdIkhhEccW9B/Nwqg==} peerDependencies: - '@tiptap/extensions': ^3.20.5 + '@tiptap/extensions': ^3.22.1 - '@tiptap/extension-hard-break@3.20.5': - resolution: {integrity: sha512-+aILNDO7BsXf0IJ4/0BYh570usFK3Q1t/ZQd8zhHuO2ATeWeDVu1x2F+ouFS4X8fmoCcioMzw15aoz93GET6kQ==} + '@tiptap/extension-hard-break@3.22.1': + resolution: {integrity: sha512-hzLwLEZVbZODa9q5UiCQpOUmDnyxN19FA4LhlqLP0/JSHewP/aol5igFZwuw0XVFp425BuzPjrB7tmr0GRTDWw==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-heading@3.20.5': - resolution: {integrity: sha512-zXxuIrCSpzgXzRxgCbRE8DZ/NFuinVaniE3pp/9LYAWgRlsAyko8pI2XrVvzzXmDQqRGi2HrNVkNy1yutUWSWQ==} + '@tiptap/extension-heading@3.22.1': + resolution: {integrity: sha512-EaIihzrOfXUHQlL6fFyJCkDrjgg0e/eD4jpkjhKpeuJDcqf7eJ1c0E2zcNRAiZkeXdN/hTQFaXKsSyNUE7T7Sg==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-horizontal-rule@3.20.5': - resolution: {integrity: sha512-4UtpUHg8cRzxWjJUGtni5VnXYbhsO7ygf1H1pr4Rv63XMBg9lfYDeSwByIuVy9biEFP7eGEFnezzb5Zlh1btmQ==} + '@tiptap/extension-horizontal-rule@3.22.1': + resolution: {integrity: sha512-Q18A8IN+gnfptIksPeVAI6oOBGYKAGf+QN0FEJ5OXO4BEAmA3hflflA1rWNfPC4aQNry/N7sAl8Gpd6HuIbz2w==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 - '@tiptap/extension-image@3.20.5': - resolution: {integrity: sha512-qxKupWKhX75Xc9GJ9Uel+KIFL9x6tb8W3RvQM1UolyJX/H7wyBO7sXp9XmKRkHZsDXRgLVbnkYBe+X83o16AIA==} + '@tiptap/extension-image@3.22.1': + resolution: {integrity: sha512-FtZCOWyyaEvSfaOPoH78IKb1BlG/Vao4PARdlrVCD1FlV1YGLAgSW5YkQAJ/vPTLwyNNZtqryaBpZrA8Wm25nQ==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-italic@3.20.5': - resolution: {integrity: sha512-7bZCgdJVTvhR5vSmNgFQbGvgRoC6m26KcUpHqWiKA95kLL5Wk4YlMCIqdiDpvJ1eakeFEvDcGZvFLg5+1NiQ+w==} + '@tiptap/extension-italic@3.22.1': + resolution: {integrity: sha512-EXPZWEsWJK9tUMypddOBvayaBeu8wFV2uH5PNrtDKrfRZ1Bf8GQ3lfcO0blHssaQ9nWqa9HwBC1mdfWcmfpxig==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-link@3.20.5': - resolution: {integrity: sha512-0PukrSYnHX2CrGSThlKfQWxpPWmL7QAvdpDUraKknGvVNSH7tUjchTshy5JdLrn/SQAU92REowRCB6zzCNEFjA==} + '@tiptap/extension-link@3.22.1': + resolution: {integrity: sha512-RHch/Bqv+QDvW3J1CXmiTB54pyrQYNQq8Vfa7is/O209dNPA8tdbkRP44rDjqn8NeDCriC/oJ4avWeXL4qNDVw==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 - '@tiptap/extension-list-item@3.20.5': - resolution: {integrity: sha512-pFJCGLIDEin1Xn6B3ctbrZvtYyALARE56ya4SmaNfnl+Hww5MfkRR40obbwYD3byA1yOpr+bECy+I2clQqzTDw==} + '@tiptap/extension-list-item@3.22.1': + resolution: {integrity: sha512-v0FgSX3cqLY3L1hIe2PFRTR3/+wlFOdFjv0p3fSJ5Tl7cgU7DR1OcljFqpw0exePcmt6dXqXVQua3PxSVV15eA==} peerDependencies: - '@tiptap/extension-list': ^3.20.5 + '@tiptap/extension-list': ^3.22.1 - '@tiptap/extension-list-keymap@3.20.5': - resolution: {integrity: sha512-rmrQgOrUb0jKtFzVUfT0UNEST2sGM2Ve4lOl+1luh66RW6TD+gvgMk/qo12/Kffl9PUiqz8oYfk2qXCwFb6Bug==} + '@tiptap/extension-list-keymap@3.22.1': + resolution: {integrity: sha512-00Nz4jJygYGJg6N1mdbQUslFG9QaGZq5P9MFwqoduWku7gYHWkZoZvrkxZrYtxGTHVIlLnF8LIfblAlOwNd76g==} peerDependencies: - '@tiptap/extension-list': ^3.20.5 + '@tiptap/extension-list': ^3.22.1 - '@tiptap/extension-list@3.20.5': - resolution: {integrity: sha512-s+Y8Q7Orq+WQiwgFB/VPMYZe+6EAR2F69xCpvOynlzTInLO4cF6QpXomuGEYAZxLHe8ZBmeIaR7y8MH/OgjrDw==} + '@tiptap/extension-list@3.22.1': + resolution: {integrity: sha512-6bVI5A12sFeyb0EngABV8/qCtC2IgiDbWC8mtNNLh5dAVGaUKo1KucL6vRYDhzXhyO/eHuGYepXZDLOOdS9LIQ==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 - '@tiptap/extension-mention@3.20.5': - resolution: {integrity: sha512-SEyIV500gAfzylvbWog2gUK6Z6fJhGYXCuGOHAGj+w2Vy3C262w8HXC9uQ+BrY/vdZp8iSpFY4AbTf5xkqkijA==} + '@tiptap/extension-mention@3.22.1': + resolution: {integrity: sha512-Z6TII6thuMdWZHMaY2dfjggmFOei7tTFR3fOBCmCKue69GnLiueM4EBi0PAl5brIepSerB09A8F9IaMGXauRdw==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 - '@tiptap/suggestion': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 + '@tiptap/suggestion': ^3.22.1 - '@tiptap/extension-ordered-list@3.20.5': - resolution: {integrity: sha512-Y/RIE3AxUNYAFKGMM5FLlTVKxxBvOh4JlLp/qYsOCY2nJdH0Jopl2FpfBYc4xoJwFSk8BELJ4Ow0adcYb15ksg==} + '@tiptap/extension-ordered-list@3.22.1': + resolution: {integrity: sha512-sbd99ZUa1lIemH7N6dLB+9aYxUgduwW2216VM3dLJBS9hmTA4iDRxWx0a1ApnAVv+sZasRSbb/wpYLtXviA1XQ==} peerDependencies: - '@tiptap/extension-list': ^3.20.5 + '@tiptap/extension-list': ^3.22.1 - '@tiptap/extension-paragraph@3.20.5': - resolution: {integrity: sha512-mwuhwmff67IpGfOViyRvUC14IlkpsOnB+hSExVnq5+hCntjt/Cr2Z8GGOgzHeIM2FIS0UqX9Lv/b6ttUg4+Now==} + '@tiptap/extension-paragraph@3.22.1': + resolution: {integrity: sha512-mnvGEZfZFysHGvmEqrSLjeddaNPB3UmomTInv9gsImw8hlB4/gQedvB6Qf2tFfIjl4ISKC5AbFxraSnJfjaL5g==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-placeholder@3.20.5': - resolution: {integrity: sha512-PcZJbzJ8j+YcRdYWFjmFFVnOOx3nETA0pzMj9fXADi28vNABnrWLwsHAseh3I5QfLmywKQb9SpTSTU2LxQgBoA==} + '@tiptap/extension-placeholder@3.22.1': + resolution: {integrity: sha512-f8NJNEJTDuT9UIZdVIAPoySgzQ/nKxR/gWRqCnwtR4O26zo/JdKI2XvrTE/iNrV3Khme8rjCtO7/8CQgTeMMxA==} peerDependencies: - '@tiptap/extensions': ^3.20.5 + '@tiptap/extensions': ^3.22.1 - '@tiptap/extension-strike@3.20.5': - resolution: {integrity: sha512-uwhvmfS4ciGYJRLUg0AHbWsprMCwyWVWd2RXOLRm0ZQeWkvzonPXZhJvzIhIgsFkPLj/dsN5t0+LdiK4UQMnyA==} + '@tiptap/extension-strike@3.22.1': + resolution: {integrity: sha512-LTdnGmglK1f/AW//36k+Km8URA1wrTLENi3R5N+/ipv+yP2rZ2Ki1R1m6yJx3KSFzR55c91xE6659/vz1uZ6iA==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-table-cell@3.20.5': - resolution: {integrity: sha512-NEobjpZ9f9CpQjnqTAsUHgcjWjTXcgWxqVfMmOWMyZLVh5kmEzDb7V8+lNplLnUUOFYynJcnzPTV7WieaD6Reg==} + '@tiptap/extension-table-cell@3.22.1': + resolution: {integrity: sha512-sDMKaQjtuAxs7j4MTezmCq5rzAFfx3igsHgGPv1rW0ibqDx5rObtOZ6oiPSts8a6cPW5/NGqLaVl0Oa5rxrV/g==} peerDependencies: - '@tiptap/extension-table': ^3.20.5 + '@tiptap/extension-table': ^3.22.1 - '@tiptap/extension-table-header@3.20.5': - resolution: {integrity: sha512-pGKVMPpfvKYIIerCUdGXD9OavFRriKd8+9PSoCR1+wtPsD8EhFbGRR3d8InLFq/G7V77pmsO6Tbws5b+M2LGNQ==} + '@tiptap/extension-table-header@3.22.1': + resolution: {integrity: sha512-avkNqG4nxgLoAKFz5+qNZRQJMCmHMDy2Fzg3aB030bJnVzCKoC7RJgWQ8d9T+Sy3LQTR7tngpW1NIozS4TI/wg==} peerDependencies: - '@tiptap/extension-table': ^3.20.5 + '@tiptap/extension-table': ^3.22.1 - '@tiptap/extension-table-row@3.20.5': - resolution: {integrity: sha512-zDW4GtnWnKPW3EdPHY5LOhW6ztuIlMxGRUYS7KGVWj9Qm8JWMPWSRsluNwajQacuZOo4ODVfG1GUooFibkjZLA==} + '@tiptap/extension-table-row@3.22.1': + resolution: {integrity: sha512-EKbwq4h47y+4UrsvOIN8LwFzSpUpYkQQhhk3x6G5xtDsZXc1kRMAowe/S1n3gcXvSkRDF4PxmepzsHsOcaSJIA==} peerDependencies: - '@tiptap/extension-table': ^3.20.5 + '@tiptap/extension-table': ^3.22.1 - '@tiptap/extension-table@3.20.5': - resolution: {integrity: sha512-YvTB5OfGqjqHqutkSyywplouFvJwlsDTpZAjtAh5TzKfOan42aiVepmHVpteoQP6LH0mSjw69RndFMIYhIGmSQ==} + '@tiptap/extension-table@3.22.1': + resolution: {integrity: sha512-wGioCPgrAhqQ9NNQitVM4sm8WVsu6MBs+4hdgTCtBTA7oEv7EqKWAujY6DA/aPE8uV236pUmosZX3iloHmvpOA==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 - '@tiptap/extension-text@3.20.5': - resolution: {integrity: sha512-DMa9g5cH2d/Gx1KXtV7txTxaa6FBqgG8glmfug+N93VMb8sEZR1Yu1az++yAep4SGGq9GWIGZCUS3H6W66et6Q==} + '@tiptap/extension-text@3.22.1': + resolution: {integrity: sha512-wFCNCATSTTFhvA9wOPkAgzPVyG3RM6+jOlDeRhHUCHsFWFWj0w9ZPwA/nP+Qi5hEW7kGG9V8o62RjBdHNvK2PQ==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-typography@3.20.5': - resolution: {integrity: sha512-eZJq5K7cwO1211nZ+MjXs+GeVD2HPFUr11wcZ0zTKlpRSq7yA3zidSOaBJOJ3zJ3iVbis2Ja9XVgv5aEsgMriw==} + '@tiptap/extension-typography@3.22.1': + resolution: {integrity: sha512-8gAAsJkVxMeJDO7EKKVtIdMaecws++3Fq86byYucl/MSklj4godSlgOJGer+Fx/l3ToYPTXEQbiL1fnaIWUwkA==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extension-underline@3.20.5': - resolution: {integrity: sha512-HMhr5KIAqZsEhlN8RxKHr/ql1a8OvBa9fLf69IwUVFolBcDExHWUtaEV/axYVRQJvvIy2oKGJxlJWDZ4hkotHQ==} + '@tiptap/extension-underline@3.22.1': + resolution: {integrity: sha512-p8/ErqQInWJbpncBycIggmtCjdrMwHmA3GNhOugo6F4fYfeVxgy7pVb7ZF+ss62d0mpQvEd81pyrzhkBtb0nBg==} peerDependencies: - '@tiptap/core': ^3.20.5 + '@tiptap/core': ^3.22.1 - '@tiptap/extensions@3.20.5': - resolution: {integrity: sha512-c4am6SznqfMnbUNSh4MvufiD7cMLdqL1BArok22uBgSWkS1sB9RVBYe8+x0jrOkk0UPEVlzDHbQ+nU+WmIyS2Q==} + '@tiptap/extensions@3.22.1': + resolution: {integrity: sha512-BKpp371Pl1CVcLRLrWH7PC1I+IsXOhet80+pILqCMlwkJnsVtOOVRr5uCF6rbPP4xK5H/ehkQWmxA8rqpv42aA==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 - '@tiptap/markdown@3.20.5': - resolution: {integrity: sha512-meSibJEeCrh6kPJbdXUNnwexZEgdxWDRu7YzPml8TCy+Djo+g50YwzOfY5bfTYs7/mwGANJ7Y8OnWcnwT2IbzQ==} + '@tiptap/markdown@3.22.1': + resolution: {integrity: sha512-0w4d6HRKeIsUlemxsxzgdiCURTGJhONrNFyL777zZIgCAbDsTKrUeI+2WNdRJBOIiNdpQiZzUL36vm2JiIDZqw==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 - '@tiptap/pm@3.20.5': - resolution: {integrity: sha512-yJhDa7Chx2EqJMX/jlewBv0za7slf1dKHWYve1XaApuVHEkxl0Ul3EDbwnx316vIITkuFW/pWSwkSsAplyBeCw==} + '@tiptap/pm@3.22.1': + resolution: {integrity: sha512-OSqSg2974eLJT5PNKFLM7156lBXCUf/dsKTQXWSzsLTf6HOP4dYP6c0YbAk6lgbNI+BdszsHNClmLVLA8H/L9A==} - '@tiptap/react@3.20.5': - resolution: {integrity: sha512-in37o1Eo7JCflcHyK/SDfgkJBgX0LRN3LMk+NdLPTerRnC0zhGLQlpfBL4591TLTOUQde7QIrLv98smYO2mj+w==} + '@tiptap/react@3.22.1': + resolution: {integrity: sha512-1pIRfgK9wape4nDXVJRfgUcYVZdPPkuECbGtz8bo0rgtdsVN7B8PBVCDyuitZ7acdLbMuuX5+TxeUOvME8np7Q==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 '@types/react': ^19.2.0 '@types/react-dom': ^19.2.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tiptap/starter-kit@3.20.5': - resolution: {integrity: sha512-L5E2TCGK0EiwmGIlwMsiwNTW1TLbfPF1Dsji4bSKRJnPbccZIMCB6qdId8v/Z+QGm85NVcBHeruQrDlKDddXBA==} + '@tiptap/starter-kit@3.22.1': + resolution: {integrity: sha512-1fFmURkgofxgP9GW993bSpxf2rIJzQbWZ9rPw17qbAVuGouIArG+Fd/A1WUD95Vdbx6JIrc1QxbNlLs7bhcoPA==} - '@tiptap/suggestion@3.20.5': - resolution: {integrity: sha512-5fqRNgnzYdJ1oDpyLqwrbVsZwvI+5VW/U89LPMvBYM7sFS7Xd0xfyxyAOWcJN4V0zLeTcuElWN3R+IUTLKbU+Q==} + '@tiptap/suggestion@3.22.1': + resolution: {integrity: sha512-jNe8WcEQfPj8CkV4uh+gzINDOhjjOz3fEMFmhzDrZrlmwUscYl0NHgvle+LPncCGTy4QSLSK/lG0GP23UAPdqA==} peerDependencies: - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^3.22.1 + '@tiptap/pm': ^3.22.1 '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} @@ -4948,168 +4951,168 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@tiptap/core@3.20.5(@tiptap/pm@3.20.5)': + '@tiptap/core@3.22.1(@tiptap/pm@3.22.1)': dependencies: - '@tiptap/pm': 3.20.5 + '@tiptap/pm': 3.22.1 - '@tiptap/extension-blockquote@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-blockquote@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-bold@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-bold@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-bubble-menu@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)': + '@tiptap/extension-bubble-menu@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': dependencies: '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 optional: true - '@tiptap/extension-bullet-list@3.20.5(@tiptap/extension-list@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-bullet-list@3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/extension-list': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-code-block-lowlight@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/extension-code-block@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)(highlight.js@11.11.1)(lowlight@3.3.0)': + '@tiptap/extension-code-block-lowlight@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/extension-code-block@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)(highlight.js@11.11.1)(lowlight@3.3.0)': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/extension-code-block': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/extension-code-block': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 highlight.js: 11.11.1 lowlight: 3.3.0 - '@tiptap/extension-code-block@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)': + '@tiptap/extension-code-block@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 - '@tiptap/extension-code@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-code@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-document@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-document@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-dropcursor@3.20.5(@tiptap/extensions@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-dropcursor@3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/extensions': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extensions': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-floating-menu@3.20.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)': + '@tiptap/extension-floating-menu@3.22.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': dependencies: '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 optional: true - '@tiptap/extension-gapcursor@3.20.5(@tiptap/extensions@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-gapcursor@3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/extensions': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extensions': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-hard-break@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-hard-break@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-heading@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-heading@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-horizontal-rule@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)': + '@tiptap/extension-horizontal-rule@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 - '@tiptap/extension-image@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-image@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-italic@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-italic@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-link@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)': + '@tiptap/extension-link@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 linkifyjs: 4.3.2 - '@tiptap/extension-list-item@3.20.5(@tiptap/extension-list@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-list-item@3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/extension-list': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-list-keymap@3.20.5(@tiptap/extension-list@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-list-keymap@3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/extension-list': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-list@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)': + '@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 - '@tiptap/extension-mention@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)(@tiptap/suggestion@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-mention@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)(@tiptap/suggestion@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 - '@tiptap/suggestion': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 + '@tiptap/suggestion': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-ordered-list@3.20.5(@tiptap/extension-list@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-ordered-list@3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/extension-list': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-paragraph@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-paragraph@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-placeholder@3.20.5(@tiptap/extensions@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-placeholder@3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/extensions': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extensions': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-strike@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-strike@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-table-cell@3.20.5(@tiptap/extension-table@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-table-cell@3.22.1(@tiptap/extension-table@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/extension-table': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extension-table': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-table-header@3.20.5(@tiptap/extension-table@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-table-header@3.22.1(@tiptap/extension-table@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/extension-table': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extension-table': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-table-row@3.20.5(@tiptap/extension-table@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))': + '@tiptap/extension-table-row@3.22.1(@tiptap/extension-table@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/extension-table': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extension-table': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) - '@tiptap/extension-table@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)': + '@tiptap/extension-table@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 - '@tiptap/extension-text@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-text@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-typography@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-typography@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extension-underline@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))': + '@tiptap/extension-underline@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) - '@tiptap/extensions@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)': + '@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 - '@tiptap/markdown@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)': + '@tiptap/markdown@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 marked: 17.0.5 - '@tiptap/pm@3.20.5': + '@tiptap/pm@3.22.1': dependencies: prosemirror-changeset: 2.4.0 prosemirror-collab: 1.3.1 @@ -5130,10 +5133,10 @@ snapshots: prosemirror-transform: 1.11.0 prosemirror-view: 1.41.7 - '@tiptap/react@3.20.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tiptap/react@3.22.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) '@types/use-sync-external-store': 0.0.6 @@ -5142,42 +5145,42 @@ snapshots: react-dom: 19.2.3(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: - '@tiptap/extension-bubble-menu': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) - '@tiptap/extension-floating-menu': 3.20.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) + '@tiptap/extension-bubble-menu': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-floating-menu': 3.22.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) transitivePeerDependencies: - '@floating-ui/dom' - '@tiptap/starter-kit@3.20.5': + '@tiptap/starter-kit@3.22.1': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/extension-blockquote': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extension-bold': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extension-bullet-list': 3.20.5(@tiptap/extension-list@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)) - '@tiptap/extension-code': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extension-code-block': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) - '@tiptap/extension-document': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extension-dropcursor': 3.20.5(@tiptap/extensions@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)) - '@tiptap/extension-gapcursor': 3.20.5(@tiptap/extensions@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)) - '@tiptap/extension-hard-break': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extension-heading': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extension-horizontal-rule': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) - '@tiptap/extension-italic': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extension-link': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) - '@tiptap/extension-list': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) - '@tiptap/extension-list-item': 3.20.5(@tiptap/extension-list@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)) - '@tiptap/extension-list-keymap': 3.20.5(@tiptap/extension-list@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)) - '@tiptap/extension-ordered-list': 3.20.5(@tiptap/extension-list@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)) - '@tiptap/extension-paragraph': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extension-strike': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extension-text': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extension-underline': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5)) - '@tiptap/extensions': 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/extension-blockquote': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-bold': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-bullet-list': 3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-code': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-code-block': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-document': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-dropcursor': 3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-gapcursor': 3.22.1(@tiptap/extensions@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-hard-break': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-heading': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-horizontal-rule': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-italic': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-link': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-list': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/extension-list-item': 3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-list-keymap': 3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-ordered-list': 3.22.1(@tiptap/extension-list@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)) + '@tiptap/extension-paragraph': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-strike': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-text': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extension-underline': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1)) + '@tiptap/extensions': 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 - '@tiptap/suggestion@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)': + '@tiptap/suggestion@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)': dependencies: - '@tiptap/core': 3.20.5(@tiptap/pm@3.20.5) - '@tiptap/pm': 3.20.5 + '@tiptap/core': 3.22.1(@tiptap/pm@3.22.1) + '@tiptap/pm': 3.22.1 '@ts-morph/common@0.27.0': dependencies: