refactor(editor): unify editor into features/editor with single markdown pipeline

Replace three divergent data paths (Marked HTML loading, regex post-processing
saving, separate paste parsing) with one symmetric path through @tiptap/markdown.

Key changes:
- Create features/editor/ module with ContentEditor (unified edit+readonly)
  and TitleEditor, replacing components/common/ editor files
- Load content via contentType: 'markdown' instead of markdownToHtml() hack
- Save content via editor.getMarkdown() directly, no post-processing
- Merge RichTextEditor + ReadonlyEditor into single ContentEditor with
  editable prop
- Extract extensions into separate modules (mention, file-upload,
  markdown-paste, submit-shortcut, code-block-view)
- Extract shared preprocessMentionShortcodes to components/markdown/mentions.ts
- Add copyMarkdown utility for clipboard operations
- Upgrade all @tiptap packages from 3.20.5 to 3.22.1 (lexer isolation fix,
  HTML entity roundtrip fix, table alignment support)
- Delete markdownToHtml.ts, readonly-editor.tsx, and 10 old component files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-04-03 10:28:29 +08:00
parent 8eb1caa72b
commit 27e58d91af
29 changed files with 848 additions and 999 deletions

View file

@ -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 (
<input
value={value}
onChange={(e) => {
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

View file

@ -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 <td>/<th>, which
// ProseMirror silently drops. Wrap in <p> so it's valid block content.
tablecell({ tokens, header }) {
const tag = header ? "th" : "td";
const content = this.parser.parseInline(tokens);
return `<${tag}><p>${content}</p></${tag}>\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 (
`<span data-type="mention" data-id="${id}" data-label="${label}"` +
` data-mention-type="${type}">${prefix}${label}</span>`
);
},
);
}
/**
* 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<string, string> = {};
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 <span data-type="mention" ...> 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;
}

View file

@ -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 <EditorContent editor={editor} />;
});
export { ReadonlyEditor, type ReadonlyEditorProps };

View file

@ -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<UploadResult | null>;
}
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<typeof useEditor>, 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<UploadResult | null>,
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<UploadResult | null>) | 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<RichTextEditorRef, RichTextEditorProps>(
function RichTextEditor(
{
defaultValue = "",
onUpdate,
placeholder: placeholderText = "",
editable = true,
className,
debounceMs = 300,
onSubmit,
onBlur,
onUploadFile,
},
ref,
) {
const debounceRef = useRef<ReturnType<typeof setTimeout>>(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<string, string>();
// 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<string, string> = {};
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 <EditorContent editor={editor} />;
},
);
export { RichTextEditor, type RichTextEditorProps, type RichTextEditorRef };

View file

@ -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<string, string> = {}
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 =

View file

@ -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'

View file

@ -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<string, string> = {};
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})`;
},
);
}

View file

@ -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<UploadResult | null>;
}
interface ContentEditorRef {
getMarkdown: () => string;
clearContent: () => void;
focus: () => void;
uploadFile: (file: File) => void;
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
const ContentEditor = forwardRef<ContentEditorRef, ContentEditorProps>(
function ContentEditor(
{
defaultValue = "",
onUpdate,
placeholder: placeholderText = "",
editable = true,
className,
debounceMs = 300,
onSubmit,
onBlur,
onUploadFile,
},
ref,
) {
const debounceRef = useRef<ReturnType<typeof setTimeout>>(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 <EditorContent editor={editor} />;
},
);
export { ContentEditor, type ContentEditorProps, type ContentEditorRef };

View file

@ -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<UploadResult | null>,
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<UploadResult | null>) | 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;
},
},
}),
];
},
});
}

View file

@ -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<string, unknown>) =>
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<UploadResult | null>) | 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;
}

View file

@ -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);
},
},
}),
];
},
});
}

View file

@ -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" ? "" : "@";

View file

@ -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,

View file

@ -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;
},
};
},
});
}

View file

@ -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";

View file

@ -0,0 +1,6 @@
/**
* Copy markdown content to the clipboard.
*/
export async function copyMarkdown(markdown: string): Promise<void> {
await navigator.clipboard.writeText(markdown);
}

View file

@ -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 stringstring 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;
}

View file

@ -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<RichTextEditorRef>(null);
const editEditorRef = useRef<ContentEditorRef>(null);
const cancelledRef = useRef(false);
const { uploadWithToast } = useFileUpload();
@ -186,7 +185,7 @@ function CommentRow({
/>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => {
navigator.clipboard.writeText(entry.content ?? "");
copyMarkdown(entry.content ?? "");
toast.success("Copied");
}}>
<Copy className="h-3.5 w-3.5" />
@ -223,7 +222,7 @@ function CommentRow({
onKeyDown={(e) => { if (e.key === "Escape") cancelEdit(); }}
>
<div className="max-h-48 overflow-y-auto text-sm leading-relaxed">
<RichTextEditor
<ContentEditor
ref={editEditorRef}
defaultValue={entry.content ?? ""}
placeholder="Edit comment..."
@ -246,7 +245,7 @@ function CommentRow({
) : (
<>
<div className="mt-1.5 pl-8 text-sm leading-relaxed text-foreground/85">
<ReadonlyEditor content={entry.content ?? ""} />
<ContentEditor defaultValue={entry.content ?? ""} editable={false} />
</div>
{!isTemp && (
<ReactionBar
@ -282,7 +281,7 @@ function CommentCard({
const { uploadWithToast } = useFileUpload();
const [open, setOpen] = useState(true);
const [editing, setEditing] = useState(false);
const editEditorRef = useRef<RichTextEditorRef>(null);
const editEditorRef = useRef<ContentEditorRef>(null);
const cancelledRef = useRef(false);
const isOwn = entry.actor_type === "member" && entry.actor_id === currentUserId;
@ -387,7 +386,7 @@ function CommentCard({
/>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => {
navigator.clipboard.writeText(entry.content ?? "");
copyMarkdown(entry.content ?? "");
toast.success("Copied");
}}>
<Copy className="h-3.5 w-3.5" />
@ -430,7 +429,7 @@ function CommentCard({
onKeyDown={(e) => { if (e.key === "Escape") cancelEdit(); }}
>
<div className="max-h-48 overflow-y-auto text-sm leading-relaxed">
<RichTextEditor
<ContentEditor
ref={editEditorRef}
defaultValue={entry.content ?? ""}
placeholder="Edit comment..."
@ -452,7 +451,7 @@ function CommentCard({
) : (
<>
<div className="pl-10 text-sm leading-relaxed text-foreground/85">
<ReadonlyEditor content={entry.content ?? ""} />
<ContentEditor defaultValue={entry.content ?? ""} editable={false} />
</div>
{!isTemp && (
<ReactionBar

View file

@ -3,7 +3,7 @@
import { useRef, useState } from "react";
import { ArrowUp, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { RichTextEditor, type RichTextEditorRef } from "@/components/common/rich-text-editor";
import { ContentEditor, type ContentEditorRef } from "@/features/editor";
import { FileUploadButton } from "@/components/common/file-upload-button";
import { useFileUpload } from "@/shared/hooks/use-file-upload";
@ -13,7 +13,7 @@ interface CommentInputProps {
}
function CommentInput({ issueId, onSubmit }: CommentInputProps) {
const editorRef = useRef<RichTextEditorRef>(null);
const editorRef = useRef<ContentEditorRef>(null);
const [isEmpty, setIsEmpty] = useState(true);
const [submitting, setSubmitting] = useState(false);
const { uploadWithToast } = useFileUpload();
@ -38,7 +38,7 @@ function CommentInput({ issueId, onSubmit }: CommentInputProps) {
return (
<div className="relative flex max-h-56 flex-col rounded-lg bg-card pb-8 ring-1 ring-border">
<div className="flex-1 min-h-0 overflow-y-auto px-3 py-2">
<RichTextEditor
<ContentEditor
ref={editorRef}
placeholder="Leave a comment..."
onUpdate={(md) => setIsEmpty(!md.trim())}

View file

@ -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<import("@/components/common/rich-text-editor").RichTextEditorRef>(null);
const descEditorRef = useRef<ContentEditorRef>(null);
const handleDescriptionUpload = useCallback(
(file: File) => uploadWithToast(file, { issueId: id }),
[uploadWithToast, id],
@ -641,7 +641,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
}}
/>
<RichTextEditor
<ContentEditor
ref={descEditorRef}
key={id}
defaultValue={issue.description || ""}

View file

@ -2,7 +2,7 @@
import { useRef, useState, useEffect } from "react";
import { ArrowUp, Loader2 } from "lucide-react";
import { RichTextEditor, type RichTextEditorRef } from "@/components/common/rich-text-editor";
import { ContentEditor, type ContentEditorRef } from "@/features/editor";
import { FileUploadButton } from "@/components/common/file-upload-button";
import { ActorAvatar } from "@/components/common/actor-avatar";
import { useFileUpload } from "@/shared/hooks/use-file-upload";
@ -33,7 +33,7 @@ function ReplyInput({
onSubmit,
size = "default",
}: ReplyInputProps) {
const editorRef = useRef<RichTextEditorRef>(null);
const editorRef = useRef<ContentEditorRef>(null);
const measureRef = useRef<HTMLDivElement>(null);
const [isEmpty, setIsEmpty] = useState(true);
const [isExpanded, setIsExpanded] = useState(false);
@ -87,7 +87,7 @@ function ReplyInput({
>
<div className="flex-1 min-h-0 overflow-y-auto pr-14">
<div ref={measureRef}>
<RichTextEditor
<ContentEditor
ref={editorRef}
placeholder={placeholder}
onUpdate={(md) => setIsEmpty(!md.trim())}

View file

@ -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<RichTextEditorRef>(null);
const descEditorRef = useRef<ContentEditorRef>(null);
const [status, setStatus] = useState<IssueStatus>((data?.status as IssueStatus) || draft.status);
const [priority, setPriority] = useState<IssuePriority>(draft.priority);
const [submitting, setSubmitting] = useState(false);
@ -231,7 +231,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
{/* Description — takes remaining space */}
<div className="flex-1 min-h-0 overflow-y-auto px-5">
<RichTextEditor
<ContentEditor
ref={descEditorRef}
defaultValue={draft.description}
placeholder="Add description..."

View file

@ -18,20 +18,21 @@
"@dnd-kit/utilities": "^3.2.2",
"@emoji-mart/data": "^1.2.1",
"@floating-ui/dom": "^1.7.6",
"@tiptap/extension-code-block-lowlight": "3.20.5",
"@tiptap/extension-image": "^3.20.5",
"@tiptap/extension-link": "^3.20.5",
"@tiptap/extension-mention": "^3.20.5",
"@tiptap/extension-placeholder": "^3.20.5",
"@tiptap/extension-table": "^3.20.5",
"@tiptap/extension-table-cell": "^3.20.5",
"@tiptap/extension-table-header": "^3.20.5",
"@tiptap/extension-table-row": "^3.20.5",
"@tiptap/extension-typography": "^3.20.5",
"@tiptap/markdown": "^3.20.5",
"@tiptap/pm": "^3.20.5",
"@tiptap/react": "^3.20.5",
"@tiptap/starter-kit": "^3.20.5",
"@tiptap/extension-code-block-lowlight": "^3.22.1",
"@tiptap/extension-image": "^3.22.1",
"@tiptap/extension-link": "^3.22.1",
"@tiptap/extension-mention": "^3.22.1",
"@tiptap/suggestion": "^3.22.1",
"@tiptap/extension-placeholder": "^3.22.1",
"@tiptap/extension-table": "^3.22.1",
"@tiptap/extension-table-cell": "^3.22.1",
"@tiptap/extension-table-header": "^3.22.1",
"@tiptap/extension-table-row": "^3.22.1",
"@tiptap/extension-typography": "^3.22.1",
"@tiptap/markdown": "^3.22.1",
"@tiptap/pm": "^3.22.1",
"@tiptap/react": "^3.22.1",
"@tiptap/starter-kit": "^3.22.1",
"@types/linkify-it": "^5.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

553
pnpm-lock.yaml generated
View file

@ -76,47 +76,50 @@ importers:
specifier: ^1.7.6
version: 1.7.6
'@tiptap/extension-code-block-lowlight':
specifier: 3.20.5
version: 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)
specifier: ^3.22.1
version: 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)
'@tiptap/extension-image':
specifier: ^3.20.5
version: 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))
specifier: ^3.22.1
version: 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))
'@tiptap/extension-link':
specifier: ^3.20.5
version: 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)
specifier: ^3.22.1
version: 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)
'@tiptap/extension-mention':
specifier: ^3.20.5
version: 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))
specifier: ^3.22.1
version: 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))
'@tiptap/extension-placeholder':
specifier: ^3.20.5
version: 3.20.5(@tiptap/extensions@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))
specifier: ^3.22.1
version: 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-table':
specifier: ^3.20.5
version: 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)
specifier: ^3.22.1
version: 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)
'@tiptap/extension-table-cell':
specifier: ^3.20.5
version: 3.20.5(@tiptap/extension-table@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))
specifier: ^3.22.1
version: 3.22.1(@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':
specifier: ^3.20.5
version: 3.20.5(@tiptap/extension-table@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))
specifier: ^3.22.1
version: 3.22.1(@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':
specifier: ^3.20.5
version: 3.20.5(@tiptap/extension-table@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))
specifier: ^3.22.1
version: 3.22.1(@tiptap/extension-table@3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1))
'@tiptap/extension-typography':
specifier: ^3.20.5
version: 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))
specifier: ^3.22.1
version: 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))
'@tiptap/markdown':
specifier: ^3.20.5
version: 3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5)
specifier: ^3.22.1
version: 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)
'@tiptap/pm':
specifier: ^3.20.5
version: 3.20.5
specifier: ^3.22.1
version: 3.22.1
'@tiptap/react':
specifier: ^3.20.5
version: 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)
specifier: ^3.22.1
version: 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)
'@tiptap/starter-kit':
specifier: ^3.20.5
version: 3.20.5
specifier: ^3.22.1
version: 3.22.1
'@tiptap/suggestion':
specifier: ^3.22.1
version: 3.22.1(@tiptap/core@3.22.1(@tiptap/pm@3.22.1))(@tiptap/pm@3.22.1)
'@types/linkify-it':
specifier: ^5.0.0
version: 5.0.0
@ -1314,218 +1317,218 @@ packages:
peerDependencies:
'@testing-library/dom': '>=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: