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} ${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 (
- `${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:
|