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:
parent
8eb1caa72b
commit
27e58d91af
29 changed files with 848 additions and 999 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
25
apps/web/components/markdown/mentions.ts
Normal file
25
apps/web/components/markdown/mentions.ts
Normal 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})`;
|
||||
},
|
||||
);
|
||||
}
|
||||
173
apps/web/features/editor/content-editor.tsx
Normal file
173
apps/web/features/editor/content-editor.tsx
Normal 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 };
|
||||
119
apps/web/features/editor/extensions/file-upload.ts
Normal file
119
apps/web/features/editor/extensions/file-upload.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
110
apps/web/features/editor/extensions/index.ts
Normal file
110
apps/web/features/editor/extensions/index.ts
Normal 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;
|
||||
}
|
||||
31
apps/web/features/editor/extensions/markdown-paste.ts
Normal file
31
apps/web/features/editor/extensions/markdown-paste.ts
Normal 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);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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" ? "" : "@";
|
||||
|
|
@ -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,
|
||||
15
apps/web/features/editor/extensions/submit-shortcut.ts
Normal file
15
apps/web/features/editor/extensions/submit-shortcut.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
11
apps/web/features/editor/index.ts
Normal file
11
apps/web/features/editor/index.ts
Normal 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";
|
||||
6
apps/web/features/editor/utils/clipboard.ts
Normal file
6
apps/web/features/editor/utils/clipboard.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Copy markdown content to the clipboard.
|
||||
*/
|
||||
export async function copyMarkdown(markdown: string): Promise<void> {
|
||||
await navigator.clipboard.writeText(markdown);
|
||||
}
|
||||
16
apps/web/features/editor/utils/preprocess.ts
Normal file
16
apps/web/features/editor/utils/preprocess.ts
Normal 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 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())}
|
||||
|
|
|
|||
|
|
@ -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 || ""}
|
||||
|
|
|
|||
|
|
@ -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())}
|
||||
|
|
|
|||
|
|
@ -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..."
|
||||
|
|
|
|||
|
|
@ -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
553
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue