From df272e38ef2e2c5634c519a21d54c5f5ca1aa480 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sat, 21 Mar 2026 18:38:07 +0100
Subject: [PATCH] feat: add resizable image component and rich text editor with
image upload support
---
app/[locale]/calendar/page.tsx | 1 +
app/globals.css | 92 ++
components/calendar/event-detail-popover.tsx | 4 +
components/email/email-composer.tsx | 159 ++--
components/email/resizable-image.tsx | 135 +++
components/email/rich-text-editor.tsx | 337 +++++++
components/files/image-preview-modal.tsx | 6 +-
package-lock.json | 882 ++++++++++++++++++-
package.json | 10 +
9 files changed, 1521 insertions(+), 105 deletions(-)
create mode 100644 components/email/resizable-image.tsx
create mode 100644 components/email/rich-text-editor.tsx
diff --git a/app/[locale]/calendar/page.tsx b/app/[locale]/calendar/page.tsx
index ef46445e..c2c839e0 100644
--- a/app/[locale]/calendar/page.tsx
+++ b/app/[locale]/calendar/page.tsx
@@ -644,6 +644,7 @@ export default function CalendarPage() {
const handleKey = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") return;
+ if (target.getAttribute("contenteditable") === "true") return;
if (showEventModal || detailEvent) return;
switch (e.key) {
diff --git a/app/globals.css b/app/globals.css
index eac64432..f8acf899 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -496,3 +496,95 @@ body {
-webkit-backdrop-filter: none !important;
}
}
+
+/* TipTap Rich Text Editor */
+.tiptap {
+ outline: none;
+}
+
+.tiptap p {
+ margin: 0.25rem 0;
+}
+
+.tiptap h1 {
+ font-size: 1.5rem;
+ font-weight: 700;
+ margin: 0.5rem 0;
+}
+
+.tiptap h2 {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin: 0.5rem 0;
+}
+
+.tiptap ul {
+ list-style-type: disc;
+ padding-left: 1.5rem;
+ margin: 0.25rem 0;
+}
+
+.tiptap ol {
+ list-style-type: decimal;
+ padding-left: 1.5rem;
+ margin: 0.25rem 0;
+}
+
+.tiptap li {
+ margin: 0.125rem 0;
+}
+
+.tiptap blockquote {
+ border-left: 3px solid var(--color-border);
+ padding-left: 1rem;
+ margin: 0.5rem 0;
+ color: var(--color-muted-foreground);
+}
+
+.tiptap pre {
+ background-color: var(--color-muted);
+ border: 1px solid var(--color-border);
+ border-radius: 0.375rem;
+ padding: 0.75rem;
+ font-family: monospace;
+ font-size: 0.875rem;
+ overflow-x: auto;
+ margin: 0.5rem 0;
+}
+
+.tiptap code {
+ background-color: var(--color-muted);
+ padding: 0.125rem 0.25rem;
+ border-radius: 0.25rem;
+ font-family: monospace;
+ font-size: 0.875rem;
+}
+
+.tiptap a {
+ color: var(--color-primary);
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.tiptap img {
+ max-width: 100%;
+ height: auto;
+}
+
+.tiptap hr {
+ border: none;
+ border-top: 1px solid var(--color-border);
+ margin: 1rem 0;
+}
+
+.tiptap p.is-editor-empty:first-child::before {
+ content: attr(data-placeholder);
+ float: left;
+ color: var(--color-muted-foreground);
+ pointer-events: none;
+ height: 0;
+}
+
+.tiptap .ProseMirror-selectednode img {
+ outline: none;
+}
diff --git a/components/calendar/event-detail-popover.tsx b/components/calendar/event-detail-popover.tsx
index c5495ad1..7db26f89 100644
--- a/components/calendar/event-detail-popover.tsx
+++ b/components/calendar/event-detail-popover.tsx
@@ -192,6 +192,10 @@ export function EventDetailPopover({
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
+ const target = e.target as HTMLElement;
+ const tag = target?.tagName?.toLowerCase();
+ if (tag === "input" || tag === "textarea" || tag === "select") return;
+ if (target?.getAttribute("contenteditable") === "true") return;
if (e.key === "e" && !noteExpanded) {
e.preventDefault();
onEdit();
diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx
index 85a4480b..a463f4ba 100644
--- a/components/email/email-composer.tsx
+++ b/components/email/email-composer.tsx
@@ -28,6 +28,14 @@ import { TemplatePicker } from "@/components/templates/template-picker";
import { TemplateForm } from "@/components/templates/template-form";
import type { EmailTemplate } from "@/lib/template-types";
import { appendPlainTextSignature, getPlainTextSignature } from "@/lib/signature-utils";
+import { RichTextEditor } from "@/components/email/rich-text-editor";
+
+/** Strip HTML tags and decode entities to get a plain-text version */
+function htmlToPlainText(html: string): string {
+ const tmp = document.createElement('div');
+ tmp.innerHTML = html;
+ return tmp.textContent || tmp.innerText || '';
+}
export interface ComposerDraftData {
to: string;
@@ -125,23 +133,28 @@ export function EmailComposer({
};
const getInitialBody = () => {
- const prefix = initialDraftText || "";
+ const prefix = initialDraftText ? `
${initialDraftText.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, ' ')}
` : "";
if (!replyTo?.body && !replyTo?.htmlBody) return prefix;
const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : "";
const from = replyTo.from?.[0];
const fromStr = from ? `${from.name || from.email}` : tCommon('unknown');
- // When HTML body is available, don't include quoted text in the textarea
- // The HTML original will be shown separately below the textarea
+ // Build quoted content as HTML
if (replyTo.htmlBody && (mode === 'reply' || mode === 'replyAll' || mode === 'forward')) {
- return prefix;
+ const quoteHeader = mode === 'forward'
+ ? `---------- Forwarded message ---------- From: ${fromStr} Date: ${date} Subject: ${replyTo.subject || ''} `
+ : `On ${date}, ${fromStr} wrote: `;
+ return `${prefix}${quoteHeader}
${replyTo.htmlBody} `;
}
- if (mode === 'forward') {
- return `${prefix}\n\n---------- Forwarded message ----------\nFrom: ${fromStr}\nDate: ${date}\nSubject: ${replyTo.subject || ""}\n\n${replyTo.body}`;
- } else if (mode === 'reply' || mode === 'replyAll') {
- return `${prefix}\n\nOn ${date}, ${fromStr} wrote:\n> ${(replyTo.body || '').split('\n').join('\n> ')}`;
+ if (replyTo.body) {
+ const escapedOriginal = replyTo.body.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, ' ');
+ if (mode === 'forward') {
+ return `${prefix} ---------- Forwarded message ---------- From: ${fromStr} Date: ${date} Subject: ${replyTo.subject || ''} ${escapedOriginal}`;
+ } else if (mode === 'reply' || mode === 'replyAll') {
+ return `${prefix} On ${date}, ${fromStr} wrote:${escapedOriginal} `;
+ }
}
return prefix;
};
@@ -157,18 +170,6 @@ export function EmailComposer({
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
const saveTimeoutRef = useRef(null);
const lastSavedDataRef = useRef("");
- const textareaRef = useRef(null);
-
- const autoResizeTextarea = useCallback(() => {
- const el = textareaRef.current;
- if (!el) return;
- el.style.height = 'auto';
- el.style.height = el.scrollHeight + 'px';
- }, []);
-
- useEffect(() => {
- autoResizeTextarea();
- }, [body, autoResizeTextarea]);
const [attachments, setAttachments] = useState>([]);
const fileInputRef = useRef(null);
const [validationErrors, setValidationErrors] = useState<{ to?: boolean; subject?: boolean; body?: boolean }>({});
@@ -367,9 +368,12 @@ export function EmailComposer({
? substitutePlaceholders(template.body, filledValues)
: template.body;
+ // Convert template plain text body to HTML for the rich text editor
+ const htmlBody = `${filledBody.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, ' ')}
`;
+
if (mode === 'compose') {
setSubject(filledSubject);
- setBody(filledBody);
+ setBody(htmlBody);
if (template.defaultRecipients?.to?.length) {
setTo(template.defaultRecipients.to.join(', ') + ', ');
}
@@ -382,7 +386,7 @@ export function EmailComposer({
setShowBcc(true);
}
} else {
- setBody((prev) => filledBody + prev);
+ setBody((prev) => htmlBody + prev);
}
if (template.identityId) {
@@ -394,8 +398,10 @@ export function EmailComposer({
useEffect(() => {
const handleTemplateKey = (e: KeyboardEvent) => {
- const tag = (e.target as HTMLElement)?.tagName?.toLowerCase();
+ const target = e.target as HTMLElement;
+ const tag = target?.tagName?.toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
+ if (target?.getAttribute('contenteditable') === 'true') return;
if (e.key === 't' && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
setShowTemplatePicker(true);
@@ -445,6 +451,18 @@ export function EmailComposer({
}
}, [client, t]);
+ const handleImageUpload = useCallback(async (file: File): Promise => {
+ if (!client) return null;
+ try {
+ const { blobId } = await client.uploadBlob(file);
+ return await client.fetchBlobAsObjectUrl(blobId, file.name, file.type);
+ } catch (error) {
+ debug.error(`Failed to upload inline image ${file.name}:`, error);
+ toast.error(t('upload_failed', { filename: file.name }));
+ return null;
+ }
+ }, [client, t]);
+
const handleFileSelect = async (event: React.ChangeEvent) => {
if (!event.target.files) return;
await addFiles(Array.from(event.target.files));
@@ -511,7 +529,7 @@ export function EmailComposer({
const ccAddresses = cc.split(",").map(e => e.trim()).filter(Boolean);
const bccAddresses = bcc.split(",").map(e => e.trim()).filter(Boolean);
- if (!toAddresses.length && !subject && !body) {
+ if (!toAddresses.length && !subject && !htmlToPlainText(body).trim()) {
return null;
}
@@ -547,7 +565,7 @@ export function EmailComposer({
const savedDraftId = await client.createDraft(
toAddresses,
subject || t('no_subject'),
- body,
+ htmlToPlainText(body),
ccAddresses,
bccAddresses,
currentIdentity?.id,
@@ -611,7 +629,8 @@ export function EmailComposer({
}, []);
const toAddresses = to.split(",").map(e => e.trim()).filter(Boolean);
- const hasContent = body || attachments.some(att => att.blobId && !att.uploading);
+ const bodyPlainText = htmlToPlainText(body).trim();
+ const hasContent = bodyPlainText || attachments.some(att => att.blobId && !att.uploading);
const canSend = toAddresses.length > 0 && !!subject && hasContent;
const getSendTooltip = (): string | undefined => {
@@ -660,26 +679,8 @@ export function EmailComposer({
: currentIdentity.email
: undefined;
- // Append signature from the selected identity
- let finalBody = appendPlainTextSignature(body, currentIdentity);
-
- // Append quoted original text for the plain text part in reply/forward
- if (replyTo && (mode === 'reply' || mode === 'replyAll' || mode === 'forward')) {
- const originalText = replyTo.body || '';
- if (originalText) {
- const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : '';
- const fromAddr = replyTo.from?.[0];
- const fromStr = fromAddr ? `${fromAddr.name || fromAddr.email}` : tCommon('unknown');
-
- if (mode === 'forward') {
- finalBody += `\n\n---------- ${t('prefix.forward')} ----------\nFrom: ${fromStr}\nDate: ${date}\nSubject: ${replyTo.subject || ''}\n\n${originalText}`;
- } else {
- finalBody += `\n\nOn ${date}, ${fromStr} wrote:\n> ${originalText.split('\n').join('\n> ')}`;
- }
- }
- }
-
- // Build HTML signature block (prefer htmlSignature, fall back to escaped textSignature)
+ // Body is already HTML from the rich text editor.
+ // Build HTML signature block
const buildSignatureHtml = (): string => {
if (currentIdentity?.htmlSignature) {
return ` -- ${sanitizeEmailHtml(currentIdentity.htmlSignature)}`;
@@ -690,26 +691,13 @@ export function EmailComposer({
return '';
};
- // Build HTML body
- let finalHtmlBody: string | undefined;
const signatureHtml = buildSignatureHtml();
- if (replyTo?.htmlBody && (mode === 'reply' || mode === 'replyAll' || mode === 'forward')) {
- // Reply/forward with original HTML content
- const escapedBody = body.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, ' ');
- const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : '';
- const fromAddr = replyTo.from?.[0];
- const fromStr = fromAddr ? `${fromAddr.name || fromAddr.email}` : tCommon('unknown');
- const quoteHeader = mode === 'forward'
- ? `---------- ${t('prefix.forward')} ---------- From: ${fromStr} Date: ${date} Subject: ${replyTo.subject || ''} `
- : `On ${date}, ${fromStr} wrote: `;
+ // Build final HTML body: editor content + signature
+ const finalHtmlBody = `${body}
${signatureHtml}`;
- finalHtmlBody = `${escapedBody}
${signatureHtml}${quoteHeader}
${replyTo.htmlBody} `;
- } else if (signatureHtml) {
- // New compose or plain-text reply — include HTML body with signature
- const escapedBody = body.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, ' ');
- finalHtmlBody = `${escapedBody}
${signatureHtml}`;
- }
+ // Generate plain text version from the HTML body for multipart/alternative
+ const finalBody = appendPlainTextSignature(htmlToPlainText(body), currentIdentity);
try {
// S/MIME send pipeline: build raw MIME → sign → encrypt → sendRawEmail
@@ -1107,23 +1095,17 @@ export function EmailComposer({
- {/* Body */}
-
-
+ {/* Body - Rich Text Editor */}
+ {
+ setBody(html);
+ if (validationErrors.body) setValidationErrors(prev => ({ ...prev, body: false }));
+ }}
+ onImageUpload={handleImageUpload}
+ placeholder={t('body_placeholder')}
+ hasError={validationErrors.body}
+ />
{composerSignatureHtml && (
--
${composerSignatureHtml}` }}
/>
)}
-
- {/* Quoted original HTML */}
- {replyTo?.htmlBody && (mode === 'reply' || mode === 'replyAll' || mode === 'forward') && (
-
-
- {mode === 'forward'
- ? `---------- ${t('prefix.forward')} ----------`
- : `${replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : ''}, ${replyTo.from?.[0]?.name || replyTo.from?.[0]?.email || tCommon('unknown')}:`
- }
-
-
-
- )}
{/* Attachments */}
diff --git a/components/email/resizable-image.tsx b/components/email/resizable-image.tsx
new file mode 100644
index 00000000..309ef3f7
--- /dev/null
+++ b/components/email/resizable-image.tsx
@@ -0,0 +1,135 @@
+"use client";
+
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { Node, mergeAttributes } from "@tiptap/core";
+import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
+import type { NodeViewProps } from "@tiptap/react";
+
+function ResizableImageView({ node, updateAttributes, selected }: NodeViewProps) {
+ const imgRef = useRef(null);
+ const [resizing, setResizing] = useState(false);
+ const startState = useRef<{ x: number; y: number; width: number; height: number; handle: string }>({
+ x: 0, y: 0, width: 0, height: 0, handle: "",
+ });
+
+ const onMouseDown = useCallback((e: React.MouseEvent, handle: string) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const img = imgRef.current;
+ if (!img) return;
+ startState.current = {
+ x: e.clientX,
+ y: e.clientY,
+ width: img.offsetWidth,
+ height: img.offsetHeight,
+ handle,
+ };
+ setResizing(true);
+ }, []);
+
+ useEffect(() => {
+ if (!resizing) return;
+
+ const onMouseMove = (e: MouseEvent) => {
+ const { x, width, handle } = startState.current;
+ const dx = e.clientX - x;
+ let newWidth: number;
+
+ if (handle === "right" || handle === "bottom-right" || handle === "top-right") {
+ newWidth = Math.max(50, width + dx);
+ } else {
+ newWidth = Math.max(50, width - dx);
+ }
+
+ updateAttributes({ width: Math.round(newWidth) });
+ };
+
+ const onMouseUp = () => {
+ setResizing(false);
+ };
+
+ document.addEventListener("mousemove", onMouseMove);
+ document.addEventListener("mouseup", onMouseUp);
+ return () => {
+ document.removeEventListener("mousemove", onMouseMove);
+ document.removeEventListener("mouseup", onMouseUp);
+ };
+ }, [resizing, updateAttributes]);
+
+ const width = node.attrs.width;
+ const style: React.CSSProperties = {
+ ...(width ? { width: `${width}px` } : {}),
+ maxWidth: "100%",
+ };
+
+ return (
+
+
+
+ {selected && (
+ <>
+ {/* Resize handle: right */}
+ onMouseDown(e, "right")}
+ className="absolute top-1/2 -right-1.5 -translate-y-1/2 w-3 h-8 bg-primary rounded cursor-ew-resize"
+ />
+ {/* Resize handle: left */}
+ onMouseDown(e, "left")}
+ className="absolute top-1/2 -left-1.5 -translate-y-1/2 w-3 h-8 bg-primary rounded cursor-ew-resize"
+ />
+ {/* Resize handle: bottom-right corner */}
+ onMouseDown(e, "bottom-right")}
+ className="absolute -bottom-1.5 -right-1.5 w-3 h-3 bg-primary rounded cursor-nwse-resize"
+ />
+ >
+ )}
+
+
+ );
+}
+
+export const ResizableImage = Node.create({
+ name: "image",
+ group: "inline",
+ inline: true,
+ draggable: true,
+ selectable: true,
+
+ addAttributes() {
+ return {
+ src: { default: null },
+ alt: { default: null },
+ title: { default: null },
+ width: { default: null },
+ };
+ },
+
+ parseHTML() {
+ return [{ tag: "img[src]" }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ const attrs: Record = { ...HTMLAttributes };
+ if (attrs.width) {
+ attrs.style = `width: ${attrs.width}px; max-width: 100%;`;
+ delete attrs.width;
+ }
+ return ["img", mergeAttributes(attrs)];
+ },
+
+ addNodeView() {
+ return ReactNodeViewRenderer(ResizableImageView);
+ },
+});
diff --git a/components/email/rich-text-editor.tsx b/components/email/rich-text-editor.tsx
new file mode 100644
index 00000000..144286bf
--- /dev/null
+++ b/components/email/rich-text-editor.tsx
@@ -0,0 +1,337 @@
+"use client";
+
+import React, { useEffect, useCallback } from "react";
+import { useEditor, EditorContent } from "@tiptap/react";
+import StarterKit from "@tiptap/starter-kit";
+import Underline from "@tiptap/extension-underline";
+import Link from "@tiptap/extension-link";
+import TextAlign from "@tiptap/extension-text-align";
+import { TextStyle } from "@tiptap/extension-text-style";
+import Color from "@tiptap/extension-color";
+import { ResizableImage } from "@/components/email/resizable-image";
+import Placeholder from "@tiptap/extension-placeholder";
+import { cn } from "@/lib/utils";
+import {
+ Bold,
+ Italic,
+ Underline as UnderlineIcon,
+ Strikethrough,
+ List,
+ ListOrdered,
+ AlignLeft,
+ AlignCenter,
+ AlignRight,
+ Link as LinkIcon,
+ Undo,
+ Redo,
+ Quote,
+ Code,
+ RemoveFormatting,
+ Heading1,
+ Heading2,
+} from "lucide-react";
+
+interface RichTextEditorProps {
+ content: string;
+ onChange: (html: string) => void;
+ onImageUpload?: (file: File) => Promise;
+ placeholder?: string;
+ className?: string;
+ hasError?: boolean;
+}
+
+function ToolbarButton({
+ active,
+ onClick,
+ children,
+ title,
+ disabled,
+}: {
+ active?: boolean;
+ onClick: () => void;
+ children: React.ReactNode;
+ title: string;
+ disabled?: boolean;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+function ToolbarSeparator() {
+ return
;
+}
+
+export function RichTextEditor({
+ content,
+ onChange,
+ onImageUpload,
+ placeholder,
+ className,
+ hasError,
+}: RichTextEditorProps) {
+ const onImageUploadRef = React.useRef(onImageUpload);
+ onImageUploadRef.current = onImageUpload;
+
+ const editor = useEditor({
+ extensions: [
+ StarterKit.configure({
+ heading: { levels: [1, 2] },
+ }),
+ Underline,
+ Link.configure({
+ openOnClick: false,
+ HTMLAttributes: { rel: "noopener noreferrer nofollow" },
+ }),
+ TextAlign.configure({
+ types: ["heading", "paragraph"],
+ }),
+ TextStyle,
+ Color,
+ ResizableImage,
+ Placeholder.configure({
+ placeholder,
+ }),
+ ],
+ content,
+ editorProps: {
+ attributes: {
+ class: "tiptap min-h-[100px] px-4 py-3 text-sm text-foreground",
+ },
+ handleDrop: (view, event) => {
+ const upload = onImageUploadRef.current;
+ if (!upload || !event.dataTransfer?.files?.length) return false;
+ const imageFiles = Array.from(event.dataTransfer.files).filter(f =>
+ f.type.startsWith("image/")
+ );
+ if (imageFiles.length === 0) return false;
+ event.preventDefault();
+ for (const file of imageFiles) {
+ upload(file).then((url) => {
+ if (url) {
+ const { state } = view;
+ const pos = view.posAtCoords({ left: event.clientX, top: event.clientY });
+ const node = state.schema.nodes.image.create({ src: url, alt: file.name });
+ const tr = state.tr.insert(pos?.pos ?? state.selection.anchor, node);
+ view.dispatch(tr);
+ }
+ });
+ }
+ return true;
+ },
+ handlePaste: (view, event) => {
+ const upload = onImageUploadRef.current;
+ if (!upload || !event.clipboardData?.files?.length) return false;
+ const imageFiles = Array.from(event.clipboardData.files).filter(f =>
+ f.type.startsWith("image/")
+ );
+ if (imageFiles.length === 0) return false;
+ event.preventDefault();
+ for (const file of imageFiles) {
+ upload(file).then((url) => {
+ if (url) {
+ const { state } = view;
+ const node = state.schema.nodes.image.create({ src: url, alt: file.name });
+ const tr = state.tr.replaceSelectionWith(node);
+ view.dispatch(tr);
+ }
+ });
+ }
+ return true;
+ },
+ },
+ onUpdate: ({ editor }) => {
+ onChange(editor.getHTML());
+ },
+ immediatelyRender: false,
+ });
+
+ // Sync external content changes (e.g. template application)
+ useEffect(() => {
+ if (editor && content !== editor.getHTML()) {
+ editor.commands.setContent(content, { emitUpdate: false });
+ }
+ }, [content, editor]);
+
+ const addLink = useCallback(() => {
+ if (!editor) return;
+ const previousUrl = editor.getAttributes("link").href;
+ const url = window.prompt("URL", previousUrl);
+ if (url === null) return;
+ if (url === "") {
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
+ return;
+ }
+ editor
+ .chain()
+ .focus()
+ .extendMarkRange("link")
+ .setLink({ href: url })
+ .run();
+ }, [editor]);
+
+ if (!editor) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Toolbar */}
+
+
editor.chain().focus().toggleBold().run()}
+ title="Bold"
+ >
+
+
+
editor.chain().focus().toggleItalic().run()}
+ title="Italic"
+ >
+
+
+
editor.chain().focus().toggleUnderline().run()}
+ title="Underline"
+ >
+
+
+
editor.chain().focus().toggleStrike().run()}
+ title="Strikethrough"
+ >
+
+
+
+
+
+
editor.chain().focus().toggleHeading({ level: 1 }).run()}
+ title="Heading 1"
+ >
+
+
+
editor.chain().focus().toggleHeading({ level: 2 }).run()}
+ title="Heading 2"
+ >
+
+
+
+
+
+
editor.chain().focus().toggleBulletList().run()}
+ title="Bullet List"
+ >
+
+
+
editor.chain().focus().toggleOrderedList().run()}
+ title="Ordered List"
+ >
+
+
+
editor.chain().focus().toggleBlockquote().run()}
+ title="Quote"
+ >
+
+
+
editor.chain().focus().toggleCodeBlock().run()}
+ title="Code Block"
+ >
+
+
+
+
+
+
editor.chain().focus().setTextAlign("left").run()}
+ title="Align Left"
+ >
+
+
+
editor.chain().focus().setTextAlign("center").run()}
+ title="Align Center"
+ >
+
+
+
editor.chain().focus().setTextAlign("right").run()}
+ title="Align Right"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
editor.chain().focus().clearNodes().unsetAllMarks().run()}
+ title="Clear Formatting"
+ >
+
+
+
+
+
+
editor.chain().focus().undo().run()}
+ disabled={!editor.can().undo()}
+ title="Undo"
+ >
+
+
+
editor.chain().focus().redo().run()}
+ disabled={!editor.can().redo()}
+ title="Redo"
+ >
+
+
+
+
+ {/* Editor */}
+
+
+ );
+}
diff --git a/components/files/image-preview-modal.tsx b/components/files/image-preview-modal.tsx
index 900585d2..353bbbc9 100644
--- a/components/files/image-preview-modal.tsx
+++ b/components/files/image-preview-modal.tsx
@@ -42,7 +42,11 @@ export function ImagePreviewModal({ name, onClose, onDownload, getImageUrl }: Im
}, [name, getImageUrl]);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
- if (e.key === "Escape") onClose();
+ if (e.key === "Escape") { onClose(); return; }
+ const target = e.target as HTMLElement;
+ const tag = target?.tagName?.toLowerCase();
+ if (tag === "input" || tag === "textarea" || tag === "select") return;
+ if (target?.getAttribute("contenteditable") === "true") return;
if (e.key === "+" || e.key === "=") setZoom((z) => Math.min(z + 0.25, 5));
if (e.key === "-") setZoom((z) => Math.max(z - 0.25, 0.25));
if (e.key === "r") setRotation((r) => r + 90);
diff --git a/package-lock.json b/package-lock.json
index dc306867..fbe66aa3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,25 @@
{
"name": "bulwark-webmail",
- "version": "1.4.4",
+ "version": "1.4.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bulwark-webmail",
- "version": "1.4.4",
+ "version": "1.4.6",
"license": "AGPL-3.0-only",
"dependencies": {
"@tanstack/react-virtual": "^3.13.18",
+ "@tiptap/extension-color": "^3.20.4",
+ "@tiptap/extension-image": "^3.20.4",
+ "@tiptap/extension-link": "^3.20.4",
+ "@tiptap/extension-placeholder": "^3.20.4",
+ "@tiptap/extension-text-align": "^3.20.4",
+ "@tiptap/extension-text-style": "^3.20.4",
+ "@tiptap/extension-underline": "^3.20.4",
+ "@tiptap/pm": "^3.20.4",
+ "@tiptap/react": "^3.20.4",
+ "@tiptap/starter-kit": "^3.20.4",
"asn1js": "^3.0.7",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
@@ -1287,6 +1297,34 @@
}
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@formatjs/ecma402-abstract": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz",
@@ -2422,6 +2460,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@remirror/core-constants": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
+ "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
+ "license": "MIT"
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
@@ -3387,6 +3431,505 @@
}
}
},
+ "node_modules/@tiptap/core": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz",
+ "integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-blockquote": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.4.tgz",
+ "integrity": "sha512-9sskyyhYj2oKat//lyZVXCp9YrPt4oJAZnGHYWXS0xlskjsLElrfKKlM4vpbhGss3VrhQRoEGqWLnIaJYPF1zw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-bold": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.4.tgz",
+ "integrity": "sha512-Md7/mNAeJCY+VLJc8JRGI+8XkVPKiOGB1NgqQPdh3aYtxXQDChQOZoJEQl6TuudDxZ85bLZB67NjZlx3jo8/0g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-bubble-menu": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.4.tgz",
+ "integrity": "sha512-EXywPlI8wjPcAb8ozymgVhjtMjFrnhtoyNTy8ZcObdpUi5CdO9j892Y7aPbKe5hLhlDpvJk7rMfir4FFKEmfng==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-bullet-list": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.4.tgz",
+ "integrity": "sha512-1RTGrur1EKoxfnLZ3M6xeNj8GITAz74jH2DHGcjLsd2Xr7Q7BozGaIq6GkkvKguMwbI1zCOxTHFCpUETXAIQQA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-code": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.4.tgz",
+ "integrity": "sha512-7j8Hi964bH1SZ9oLdZC1fkqWz27mliSDV7M8lmL/M14+Qw42D/VOAKS4Aw9OCFtHMlTsjLR6qsoVxL8Lpkt6NA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-code-block": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.4.tgz",
+ "integrity": "sha512-Zlw3FrXTy01+o1yISeX/LC+iJeHA+ym602bMXGmtA6lyl7QSOSO7WExweJ6xeJGhbCjldwT5al6fkRAs8iGJZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-color": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-3.20.4.tgz",
+ "integrity": "sha512-+OT9wWEJnqoWmzfqPYt0oWm8LZcH+D44Z3jA2TNzBj4tLGQ2YPxN2SyS12AlRi7MuguVT7utFy7qDXrfir8eUA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-text-style": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-document": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.4.tgz",
+ "integrity": "sha512-zF1CIFVLt8MfSpWWnPwtGyxPOsT0xYM2qJKcXf2yZcTG37wDKmUi6heG53vGigIavbQlLaAFvs+1mNdOu2x/0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-dropcursor": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.4.tgz",
+ "integrity": "sha512-TgMwvZ8myXYdmd6bUV7qkpZXv7ZUiSmX/8eo+iPEzYo2CnDLAGvDKgC50nfq/g87SDvfBgPuAiBfFvsMQQWaTw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-floating-menu": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.4.tgz",
+ "integrity": "sha512-AaPTFhoO8DBIElJyd/RTVJjkctvJuL+GHURX0npbtTxXq5HXbebVwf2ARNR7jMd/GThsmBaNJiGxZg4A2oeDqQ==",
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@floating-ui/dom": "^1.0.0",
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-gapcursor": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.4.tgz",
+ "integrity": "sha512-JJ6f1iQ1e0s4kISgq55U3UYGwWV/N9f0PYMtB6e3L+SBQjXnywaLK0g6vfN6IvTCC2vdIuqeSOX8VlSO97sJLw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-hard-break": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.4.tgz",
+ "integrity": "sha512-gJbq58d8zB1gzyqVEopowej5CpW4/Fpg6oGJvlZxaCukqd0gJRWGC89K+jE62YA1Td4sfcKrekKvN7jm2y/ZUg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-heading": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.4.tgz",
+ "integrity": "sha512-xsnkmTGggJc5P2iCwS1lv8KFG31xC/GNPJKoi/3UH67j/lKDhA3AdtshsLeyv2FKtTtYDb8oV0IqzHB1MM6a7w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-horizontal-rule": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.4.tgz",
+ "integrity": "sha512-y6joCi49haAA0bo3EGUY+dWUMHH1GPUc84hxrBY/0pMs+Bn+kQ1+DQJErZDTWGJrlHPWU/yekBZT72SNdp0DNA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-image": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.20.4.tgz",
+ "integrity": "sha512-57w2TevHQljTh6Xiry9duIm7NNOQAUSTwtwRn4GGLoKwHR8qXTxzp513ASrFOgR2kgs2TP471Au6RHf947P+jg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-italic": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.4.tgz",
+ "integrity": "sha512-4ZqiWr7cmqPFux8tj1ZLiYytyWf343IvQemNX6AvVWvscrJcrfj3YX4Le2BA0RW3A3M6RpLQXXozuF8vxYFDeQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-link": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.4.tgz",
+ "integrity": "sha512-JNDSkWrVdb8NSvbQXwHWvK5tCMbTWwOHFOweknQZ1JPK4dei9FJVofYQaHyW4bJBdcCjds3NZSnXE8DM9iAWmg==",
+ "license": "MIT",
+ "dependencies": {
+ "linkifyjs": "^4.3.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-list": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.4.tgz",
+ "integrity": "sha512-X+5plTKhOioNcQ4KsAFJJSb/3+zR8Xhdpow4HzXtoV1KcbdDey1fhZdpsfkbrzCL0s6/wAgwZuAchCK7HujurQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-list-item": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.4.tgz",
+ "integrity": "sha512-QoTc5RACXaZF+vIIBBxjGO7D0oWFUDgBKJCpvUZ0CoGGKosnfe4a9I5THFyLj4201cf0oUqgf1oZhTqETGxlVw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-list-keymap": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.4.tgz",
+ "integrity": "sha512-RIqXM649+8IP7p/KVfaGlJiwjCylm1m6OPlaoM3K8O7oEOGRQzNeexexECCD2jsXRxew4E+vBNMD2orXqJmu8A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-ordered-list": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.4.tgz",
+ "integrity": "sha512-3budNL8BgBon3TcXZ4hjT0YpFvx1Ka3uSIECKDxHgES+OQcR+6cagxSb60gFEccf3Dr0PIwcVTY6g14lC1qKRQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extension-list": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-paragraph": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.4.tgz",
+ "integrity": "sha512-lm6fOScWuZAF/Sfp97igUwFd3L1QHIVLAWP5NVdh0DTLrEIt4rMBmsww+yOpMQRhvz2uTgMbMXynrimhzi/QVw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-placeholder": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.20.4.tgz",
+ "integrity": "sha512-GB0KWtqm83YHG8cnqBLijvUBm+xvLfQHDfFRRH2fb3EzH3eIsM9jKRC31ADT27RSV1zVpHMFGcP3/pWpdrN1Lw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/extensions": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-strike": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.4.tgz",
+ "integrity": "sha512-It1Px9uDGTsVqyyg6cy7DigLoenljpQwqdI0jssM7QclZrHnsrye9fZxBBiiuCzzV1305MxKgHvratkHwqmVNA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-text": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.4.tgz",
+ "integrity": "sha512-jchJcBZixDEO2J66Zx5dchsI2mA6IYsROqF8P1poxL4ienH7RVQRCTsBNnSfIeOtREKKWeOU/tEs5fcpvvGwIQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-text-align": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.20.4.tgz",
+ "integrity": "sha512-6ZuRyClIyCimXu+S5LQ54DueEsYg5VOVOmubOVbG+WAjM9svn9Z8gv2sNDah2yEqXrX06B02zYcSyMiD7CHbfA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-text-style": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.20.4.tgz",
+ "integrity": "sha512-PvW0Ja7ahWpo4bRuR8YCCVv4PH8lXjzhzlBAa4bMbsumOg+GbhX8Su7fwqd+IIPrHqfPXz9HTBMApSfzP6/08A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extension-underline": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.4.tgz",
+ "integrity": "sha512-0OjMc3FDujX16G+jhvqcY/mLot8SrNtDu8ggUwNLAfiI/QIvMVgk7giFD71DATC/4Nb8i/iwAEegTD8MxBIXCg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/extensions": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.4.tgz",
+ "integrity": "sha512-8p6hVT65DjuQjtEdlH6ewX9SOJHlVQAOee3sWIJQmeJNRnZNvqPIBLleebUqDiljNTpxBv6s6QWkSTKgf3btwg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ }
+ },
+ "node_modules/@tiptap/pm": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz",
+ "integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-changeset": "^2.3.0",
+ "prosemirror-collab": "^1.3.1",
+ "prosemirror-commands": "^1.6.2",
+ "prosemirror-dropcursor": "^1.8.1",
+ "prosemirror-gapcursor": "^1.3.2",
+ "prosemirror-history": "^1.4.1",
+ "prosemirror-inputrules": "^1.4.0",
+ "prosemirror-keymap": "^1.2.2",
+ "prosemirror-markdown": "^1.13.1",
+ "prosemirror-menu": "^1.2.4",
+ "prosemirror-model": "^1.24.1",
+ "prosemirror-schema-basic": "^1.2.3",
+ "prosemirror-schema-list": "^1.5.0",
+ "prosemirror-state": "^1.4.3",
+ "prosemirror-tables": "^1.6.4",
+ "prosemirror-trailing-node": "^3.0.0",
+ "prosemirror-transform": "^1.10.2",
+ "prosemirror-view": "^1.38.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
+ "node_modules/@tiptap/react": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.20.4.tgz",
+ "integrity": "sha512-1B8iWsHWwb5TeyVaUs8BRPzwWo4PsLQcl03urHaz0zTJ8DauopqvxzV3+lem1OkzRHn7wnrapDvwmIGoROCaQw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "fast-equals": "^5.3.3",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "optionalDependencies": {
+ "@tiptap/extension-bubble-menu": "^3.20.4",
+ "@tiptap/extension-floating-menu": "^3.20.4"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/pm": "^3.20.4",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@tiptap/starter-kit": {
+ "version": "3.20.4",
+ "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.20.4.tgz",
+ "integrity": "sha512-WcyK6hsTl8eBsQhQ+d9Sq8fYZKOYdL+D45MyH3hz583elXqJlW3h3JPFYb0o87gddGxn8Mm57OA/gA1zEdeDMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tiptap/core": "^3.20.4",
+ "@tiptap/extension-blockquote": "^3.20.4",
+ "@tiptap/extension-bold": "^3.20.4",
+ "@tiptap/extension-bullet-list": "^3.20.4",
+ "@tiptap/extension-code": "^3.20.4",
+ "@tiptap/extension-code-block": "^3.20.4",
+ "@tiptap/extension-document": "^3.20.4",
+ "@tiptap/extension-dropcursor": "^3.20.4",
+ "@tiptap/extension-gapcursor": "^3.20.4",
+ "@tiptap/extension-hard-break": "^3.20.4",
+ "@tiptap/extension-heading": "^3.20.4",
+ "@tiptap/extension-horizontal-rule": "^3.20.4",
+ "@tiptap/extension-italic": "^3.20.4",
+ "@tiptap/extension-link": "^3.20.4",
+ "@tiptap/extension-list": "^3.20.4",
+ "@tiptap/extension-list-item": "^3.20.4",
+ "@tiptap/extension-list-keymap": "^3.20.4",
+ "@tiptap/extension-ordered-list": "^3.20.4",
+ "@tiptap/extension-paragraph": "^3.20.4",
+ "@tiptap/extension-strike": "^3.20.4",
+ "@tiptap/extension-text": "^3.20.4",
+ "@tiptap/extension-underline": "^3.20.4",
+ "@tiptap/extensions": "^3.20.4",
+ "@tiptap/pm": "^3.20.4"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -3482,6 +4025,28 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "25.3.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
@@ -3496,7 +4061,6 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -3506,7 +4070,6 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -3519,6 +4082,12 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
@@ -3986,7 +4555,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
"license": "Python-2.0"
},
"node_modules/aria-query": {
@@ -4467,6 +5035,12 @@
"url": "https://opencollective.com/core-js"
}
},
+ "node_modules/crelt": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
+ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4533,7 +5107,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/data-urls": {
@@ -5043,7 +5616,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -5414,6 +5986,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-equals": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -6820,6 +7401,21 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
+ "node_modules/linkifyjs": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
+ "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
+ "license": "MIT"
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -6895,6 +7491,35 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/markdown-it": {
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
+ "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/markdown-it/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -6912,6 +7537,12 @@
"dev": true,
"license": "CC0-1.0"
},
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "license": "MIT"
+ },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -7348,6 +7979,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/orderedmap": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
+ "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
+ "license": "MIT"
+ },
"node_modules/own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -7633,6 +8270,201 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/prosemirror-changeset": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
+ "integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-transform": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-collab": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
+ "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-commands": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
+ "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.10.2"
+ }
+ },
+ "node_modules/prosemirror-dropcursor": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
+ "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0",
+ "prosemirror-view": "^1.1.0"
+ }
+ },
+ "node_modules/prosemirror-gapcursor": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
+ "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-keymap": "^1.0.0",
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-view": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-history": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
+ "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.2.2",
+ "prosemirror-transform": "^1.0.0",
+ "prosemirror-view": "^1.31.0",
+ "rope-sequence": "^1.3.0"
+ }
+ },
+ "node_modules/prosemirror-inputrules": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
+ "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-keymap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
+ "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "w3c-keyname": "^2.2.0"
+ }
+ },
+ "node_modules/prosemirror-markdown": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
+ "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/markdown-it": "^14.0.0",
+ "markdown-it": "^14.0.0",
+ "prosemirror-model": "^1.25.0"
+ }
+ },
+ "node_modules/prosemirror-menu": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz",
+ "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==",
+ "license": "MIT",
+ "dependencies": {
+ "crelt": "^1.0.0",
+ "prosemirror-commands": "^1.0.0",
+ "prosemirror-history": "^1.0.0",
+ "prosemirror-state": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-model": {
+ "version": "1.25.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
+ "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
+ "license": "MIT",
+ "dependencies": {
+ "orderedmap": "^2.0.0"
+ }
+ },
+ "node_modules/prosemirror-schema-basic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
+ "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.25.0"
+ }
+ },
+ "node_modules/prosemirror-schema-list": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
+ "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.7.3"
+ }
+ },
+ "node_modules/prosemirror-state": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
+ "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-transform": "^1.0.0",
+ "prosemirror-view": "^1.27.0"
+ }
+ },
+ "node_modules/prosemirror-tables": {
+ "version": "1.8.5",
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
+ "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-keymap": "^1.2.3",
+ "prosemirror-model": "^1.25.4",
+ "prosemirror-state": "^1.4.4",
+ "prosemirror-transform": "^1.10.5",
+ "prosemirror-view": "^1.41.4"
+ }
+ },
+ "node_modules/prosemirror-trailing-node": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
+ "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@remirror/core-constants": "3.0.0",
+ "escape-string-regexp": "^4.0.0"
+ },
+ "peerDependencies": {
+ "prosemirror-model": "^1.22.1",
+ "prosemirror-state": "^1.4.2",
+ "prosemirror-view": "^1.33.8"
+ }
+ },
+ "node_modules/prosemirror-transform": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz",
+ "integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.21.0"
+ }
+ },
+ "node_modules/prosemirror-view": {
+ "version": "1.41.7",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz",
+ "integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==",
+ "license": "MIT",
+ "dependencies": {
+ "prosemirror-model": "^1.20.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -7643,6 +8475,15 @@
"node": ">=6"
}
},
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/pvtsutils": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
@@ -7822,6 +8663,12 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/rope-sequence": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
+ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
+ "license": "MIT"
+ },
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -8637,6 +9484,12 @@
"node": ">=14.17"
}
},
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
+ },
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -8735,6 +9588,15 @@
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/vite": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
@@ -8947,6 +9809,12 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "license": "MIT"
+ },
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
diff --git a/package.json b/package.json
index fd3942f9..3a48fd84 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,16 @@
},
"dependencies": {
"@tanstack/react-virtual": "^3.13.18",
+ "@tiptap/extension-color": "^3.20.4",
+ "@tiptap/extension-image": "^3.20.4",
+ "@tiptap/extension-link": "^3.20.4",
+ "@tiptap/extension-placeholder": "^3.20.4",
+ "@tiptap/extension-text-align": "^3.20.4",
+ "@tiptap/extension-text-style": "^3.20.4",
+ "@tiptap/extension-underline": "^3.20.4",
+ "@tiptap/pm": "^3.20.4",
+ "@tiptap/react": "^3.20.4",
+ "@tiptap/starter-kit": "^3.20.4",
"asn1js": "^3.0.7",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",