feat(editor): audio and PDF nodes (#2064)

* use local resizable

* feat: aduio

* support audio imports

* feat: use confluence real file names

* cleanup

* error handling

* hide notice

* add audio

* fix pulse

* Fix import and export

* unify pulse

* hide in readonly mode

* keywords

* keyword

* translations

* better sort

* feat: PDF embed

* cleanup

* remove audio menu

* open active

* hide focus on readonly mode

* increase iframe default dimension
This commit is contained in:
Philip Okugbe
2026-03-28 17:33:29 +00:00
committed by GitHub
parent 2d835da0e3
commit 7981ef462e
49 changed files with 2870 additions and 209 deletions
@@ -341,6 +341,7 @@
"Insert horizontal rule divider": "Insert horizontal rule divider",
"Upload any image from your device.": "Upload any image from your device.",
"Upload any video from your device.": "Upload any video from your device.",
"Upload any audio from your device.": "Upload any audio from your device.",
"Upload any file from your device.": "Upload any file from your device.",
"Uploading {{name}}": "Uploading {{name}}",
"Uploading file": "Uploading file",
@@ -351,6 +352,12 @@
"Divider": "Divider.",
"Quote": "Quote.",
"Image": "Image.",
"Audio": "Audio.",
"Embed PDF": "Embed PDF",
"Upload and embed a PDF file.": "Upload and embed a PDF file.",
"Embed as PDF": "Embed as PDF",
"Failed to load PDF": "Failed to load PDF",
"Convert to attachment": "Convert to attachment",
"File attachment": "File attachment.",
"Toggle block": "Toggle block.",
"Callout": "Callout.",
@@ -723,5 +730,7 @@
"Publish": "Publish.",
"Security": "Security.",
"Enforce SSO": "Enforce SSO.",
"Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to log in with email and password."
"Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to log in with email and password.",
"Uploading {{name}}": "Uploading {{name}}",
"Uploading file": "Uploading file"
}
@@ -71,7 +71,10 @@ export function PageShareModal({ readOnly }: PageShareModalProps) {
) : null
}
variant="default"
onClick={open}
onClick={() => {
setActiveTab(isPubliclyShared ? "publish" : hasPagePermissions ? "access" : "publish");
open();
}}
>
{t("Share")}
</Button>
@@ -1,17 +1,43 @@
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { Group, Text, Paper, ActionIcon, Loader } from "@mantine/core";
import { Group, Text, Paper, ActionIcon, Loader, Tooltip } from "@mantine/core";
import { getFileUrl } from "@/lib/config.ts";
import { IconDownload, IconPaperclip } from "@tabler/icons-react";
import { IconDownload, IconFileTypePdf, IconPaperclip } from "@tabler/icons-react";
import { useHover } from "@mantine/hooks";
import { formatBytes } from "@/lib";
import { useTranslation } from "react-i18next";
import { useCallback } from "react";
export default function AttachmentView(props: NodeViewProps) {
const { t } = useTranslation();
const { node, selected } = props;
const { url, name, size } = node.attrs;
const { editor, node, getPos, selected } = props;
const { url, name, size, mime, attachmentId } = node.attrs;
const { hovered, ref } = useHover();
const isPdf = mime === "application/pdf" || name?.toLowerCase().endsWith(".pdf");
const handleEmbedAsPdf = useCallback(() => {
const pos = getPos();
if (pos === undefined || !url) return;
const nodeSize = node.nodeSize;
editor
.chain()
.insertContentAt(
{ from: pos, to: pos + nodeSize },
{
type: "pdf",
attrs: {
src: url,
name,
attachmentId,
size,
},
},
)
.run();
}, [editor, getPos, node, url, name, attachmentId]);
return (
<NodeViewWrapper>
<Paper withBorder p="4px" ref={ref} data-drag-handle>
@@ -39,11 +65,20 @@ export default function AttachmentView(props: NodeViewProps) {
</Group>
{url && (selected || hovered) && (
<a href={getFileUrl(url)} target="_blank">
<ActionIcon variant="default" aria-label="download file">
<IconDownload size={18} />
</ActionIcon>
</a>
<Group gap={4} wrap="nowrap" style={{ flexShrink: 0 }}>
{isPdf && editor.isEditable && (
<Tooltip label={t("Embed as PDF")} position="top" withinPortal={false}>
<ActionIcon variant="default" aria-label={t("Embed as PDF")} onClick={handleEmbedAsPdf}>
<IconFileTypePdf size={18} />
</ActionIcon>
</Tooltip>
)}
<a href={getFileUrl(url)} target="_blank">
<ActionIcon variant="default" aria-label="download file">
<IconDownload size={18} />
</ActionIcon>
</a>
</Group>
)}
</Group>
</Paper>
@@ -0,0 +1,123 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { useCallback } from "react";
import { Node as PMNode } from "@tiptap/pm/model";
import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { ActionIcon, Tooltip } from "@mantine/core";
import {
IconDownload,
IconTrash,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { getFileUrl } from "@/lib/config.ts";
import classes from "../common/toolbar-menu.module.css";
export function AudioMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
const editorState = useEditorState({
editor,
selector: (ctx) => {
if (!ctx.editor) {
return null;
}
const audioAttrs = ctx.editor.getAttributes("audio");
return {
isAudio: ctx.editor.isActive("audio"),
src: audioAttrs?.src || null,
};
},
});
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
return false;
}
return editor.isActive("audio") && editor.getAttributes("audio").src;
},
[editor],
);
const getReferencedVirtualElement = useCallback(() => {
if (!editor) return;
const { selection } = editor.state;
const predicate = (node: PMNode) => node.type.name === "audio";
const parent = findParentNode(predicate)(selection);
if (parent) {
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
const domRect = dom.getBoundingClientRect();
return {
getBoundingClientRect: () => domRect,
getClientRects: () => [domRect],
};
}
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
return {
getBoundingClientRect: () => domRect,
getClientRects: () => [domRect],
};
}, [editor]);
const handleDownload = useCallback(() => {
if (!editorState?.src) return;
const url = getFileUrl(editorState.src);
const a = document.createElement("a");
a.href = url;
a.download = "";
a.click();
}, [editorState?.src]);
const handleDelete = useCallback(() => {
editor.commands.deleteSelection();
}, [editor]);
return (
<BaseBubbleMenu
editor={editor}
pluginKey={`audio-menu`}
updateDelay={0}
getReferencedVirtualElement={getReferencedVirtualElement}
options={{
placement: "top",
offset: 8,
flip: false,
}}
shouldShow={shouldShow}
>
<div className={classes.toolbar}>
<Tooltip position="top" label={t("Download")} withinPortal={false}>
<ActionIcon
onClick={handleDownload}
size="lg"
aria-label={t("Download")}
variant="subtle"
>
<IconDownload size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Delete")} withinPortal={false}>
<ActionIcon
onClick={handleDelete}
size="lg"
aria-label={t("Delete")}
variant="subtle"
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</div>
</BaseBubbleMenu>
);
}
export default AudioMenu;
@@ -0,0 +1,37 @@
.audioWrapper {
display: flex;
justify-content: center;
align-items: center;
max-width: 100%;
border-radius: 8px;
overflow: hidden;
}
.skeleton {
animation: pulse 1.2s ease-in-out infinite;
@mixin light {
background: linear-gradient(-90deg, var(--mantine-color-gray-3) 0%, var(--mantine-color-gray-1) 50%, var(--mantine-color-gray-3) 100%);
background-size: 400% 400%;
}
@mixin dark {
background: linear-gradient(-90deg, var(--mantine-color-dark-6) 0%, var(--mantine-color-dark-5) 50%, var(--mantine-color-dark-6) 100%);
background-size: 400% 400%;
}
@keyframes pulse {
0% {
background-position: 0% 0%;
}
100% {
background-position: -135% 0%;
}
}
}
.audio {
display: block;
width: 100%;
border-radius: 8px;
}
@@ -0,0 +1,65 @@
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { Group, Loader, Text } from "@mantine/core";
import { useMemo } from "react";
import { getFileUrl } from "@/lib/config.ts";
import { isInternalFileUrl } from "@docmost/editor-ext";
import classes from "./audio-view.module.css";
import { useTranslation } from "react-i18next";
export default function AudioView(props: NodeViewProps) {
const { t } = useTranslation();
const { editor, node } = props;
const { src, placeholder } = node.attrs;
const safeSrc = useMemo(() => {
if (!src || !isInternalFileUrl(src)) return null;
return getFileUrl(src);
}, [src]);
const previewSrc = useMemo(() => {
editor.storage.shared.audioPreviews =
editor.storage.shared.audioPreviews || {};
if (placeholder?.id) {
return editor.storage.shared.audioPreviews[placeholder.id];
}
return null;
}, [placeholder, editor]);
return (
<NodeViewWrapper data-drag-handle>
<div className={`${classes.audioWrapper} ${!safeSrc ? classes.skeleton : ''}`}>
{safeSrc && (
<audio
className={classes.audio}
preload="metadata"
controls
src={safeSrc}
/>
)}
{!safeSrc && previewSrc && (
<Group pos="relative" w="100%">
<audio
className={classes.audio}
preload="metadata"
controls
src={previewSrc}
/>
<Loader size={20} pos="absolute" top={6} right={6} />
</Group>
)}
{!safeSrc && !previewSrc && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md" h={54}>
<Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end">
{placeholder?.name
? t("Uploading {{name}}", { name: placeholder.name })
: t("Uploading file")}
</Text>
</Group>
)}
</div>
</NodeViewWrapper>
);
}
@@ -0,0 +1,36 @@
import { handleAudioUpload } from "@docmost/editor-ext";
import { uploadFile } from "@/features/page/services/page-service.ts";
import { notifications } from "@mantine/notifications";
import { getFileUploadSizeLimit } from "@/lib/config.ts";
import { formatBytes } from "@/lib";
import i18n from "@/i18n.ts";
export const uploadAudioAction = handleAudioUpload({
onUpload: async (file: File, pageId: string): Promise<any> => {
try {
return await uploadFile(file, pageId);
} catch (err) {
notifications.show({
color: "red",
message: err?.response.data.message,
});
throw err;
}
},
validateFn: (file) => {
if (!file.type.includes("audio/")) {
return false;
}
if (file.size > getFileUploadSizeLimit()) {
notifications.show({
color: "red",
message: i18n.t("File exceeds the {{limit}} attachment limit", {
limit: formatBytes(getFileUploadSizeLimit()),
}),
});
return false;
}
return true;
},
});
@@ -1,6 +1,7 @@
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
import { uploadAttachmentAction } from "../attachment/upload-attachment-action";
import { uploadPdfAction } from "../pdf/upload-pdf-action";
import { createMentionAction } from "@/features/editor/components/link/internal-link-paste.ts";
import { INTERNAL_LINK_REGEX } from "@/lib/constants.ts";
import { Editor } from "@tiptap/core";
@@ -12,6 +13,8 @@ import {
const ATTACHMENT_NODE_TYPES = [
"image",
"video",
"audio",
"pdf",
"attachment",
"excalidraw",
"drawio",
@@ -63,6 +66,7 @@ export const handlePaste = (
const pos = editor.state.selection.from;
uploadImageAction(file, editor, pos, pageId);
uploadVideoAction(file, editor, pos, pageId);
uploadPdfAction(file, editor, pos, pageId);
uploadAttachmentAction(file, editor, pos, pageId);
}
return true;
@@ -229,6 +233,7 @@ export const handleFileDrop = (
uploadImageAction(file, editor, coordinates?.pos ?? 0 - 1, pageId);
uploadVideoAction(file, editor, coordinates?.pos ?? 0 - 1, pageId);
uploadPdfAction(file, editor, coordinates?.pos ?? 0 - 1, pageId);
uploadAttachmentAction(file, editor, coordinates?.pos ?? 0 - 1, pageId);
}
return true;
@@ -1,5 +1,5 @@
import type { ResizableNodeViewDirection } from "@tiptap/core";
import classes from "./node-resize.module.css";
import { ResizableNodeViewDirection } from "@docmost/editor-ext";
export function createResizeHandle(
direction: ResizableNodeViewDirection,
@@ -20,8 +20,8 @@
.cornerHandle {
position: absolute;
width: 36px;
height: 36px;
width: 24px;
height: 24px;
z-index: 2;
opacity: 0;
transition: opacity 0.2s ease;
@@ -42,13 +42,13 @@
}
&::before {
width: 28px;
width: 20px;
height: 3px;
}
&::after {
width: 3px;
height: 28px;
height: 20px;
}
&:hover::before,
@@ -74,6 +74,15 @@ export const ResizableWrapper: React.FC<ResizableWrapperProps> = ({
const constraintsRef = useRef({ minWidth, maxWidth, minHeight, maxHeight });
constraintsRef.current = { minWidth, maxWidth, minHeight, maxHeight };
useEffect(() => {
if (!dragRef.current && wrapperRef.current) {
widthRef.current = initialWidth;
heightRef.current = initialHeight;
wrapperRef.current.style.width = `${initialWidth}px`;
wrapperRef.current.style.height = `${initialHeight}px`;
}
}, [initialWidth, initialHeight]);
const handleMouseMove = useRef((e: MouseEvent) => {
const drag = dragRef.current;
if (!drag || !wrapperRef.current) return;
@@ -86,8 +86,8 @@ export default function EmbedView(props: NodeViewProps) {
{embedUrl ? (
<div className={classes.embedContainer}>
<ResizableWrapper
initialWidth={nodeWidth || 640}
initialHeight={nodeHeight || 480}
initialWidth={nodeWidth || 800}
initialHeight={nodeHeight || 600}
minWidth={200}
maxWidth={1200}
minHeight={200}
@@ -102,8 +102,9 @@ export default function EmbedView(props: NodeViewProps) {
<iframe
className={classes.embedIframe}
src={sanitizeUrl(embedUrl)}
allow="encrypted-media"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
allow="encrypted-media; clipboard-read; clipboard-write; picture-in-picture;"
loading="lazy"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads"
allowFullScreen
frameBorder="0"
/>
@@ -5,6 +5,9 @@
max-width: 100%;
border-radius: 8px;
overflow: hidden;
}
.skeleton {
animation: pulse 1.2s ease-in-out infinite;
@mixin light {
@@ -33,6 +33,7 @@ export default function ImageView(props: NodeViewProps) {
className={clsx(
selected && "ProseMirror-selectednode",
classes.imageWrapper,
!src && classes.skeleton,
alignClass,
)}
style={{
@@ -0,0 +1,145 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { useCallback } from "react";
import { Node as PMNode } from "@tiptap/pm/model";
import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { ActionIcon, Tooltip } from "@mantine/core";
import {
IconPaperclip,
IconTrash,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import classes from "../common/toolbar-menu.module.css";
export function PdfMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
const editorState = useEditorState({
editor,
selector: (ctx) => {
if (!ctx.editor) {
return null;
}
const pdfAttrs = ctx.editor.getAttributes("pdf");
return {
isPdf: ctx.editor.isActive("pdf"),
src: pdfAttrs?.src || null,
name: pdfAttrs?.name || null,
attachmentId: pdfAttrs?.attachmentId || null,
};
},
});
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state || !editor.isActive("pdf")) {
return false;
}
const { selection } = state;
const dom = editor.view.nodeDOM(selection.from) as HTMLElement | null;
if (!dom) return false;
return !!dom.querySelector("[data-pdf-error]");
},
[editor],
);
const getReferencedVirtualElement = useCallback(() => {
if (!editor) return;
const { selection } = editor.state;
const predicate = (node: PMNode) => node.type.name === "pdf";
const parent = findParentNode(predicate)(selection);
if (parent) {
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
const domRect = dom.getBoundingClientRect();
return {
getBoundingClientRect: () => domRect,
getClientRects: () => [domRect],
};
}
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
return {
getBoundingClientRect: () => domRect,
getClientRects: () => [domRect],
};
}, [editor]);
const handleConvertToAttachment = useCallback(() => {
if (!editorState?.src) return;
const { selection } = editor.state;
const { from } = selection;
const node = editor.state.doc.nodeAt(from);
if (!node || node.type.name !== "pdf") return;
editor
.chain()
.insertContentAt(
{ from, to: from + node.nodeSize },
{
type: "attachment",
attrs: {
url: node.attrs.src,
name: node.attrs.name,
attachmentId: node.attrs.attachmentId,
size: node.attrs.size,
mime: "application/pdf",
},
},
)
.run();
}, [editor, editorState]);
const handleDelete = useCallback(() => {
editor.commands.deleteSelection();
}, [editor]);
return (
<BaseBubbleMenu
editor={editor}
pluginKey={`pdf-menu`}
updateDelay={0}
getReferencedVirtualElement={getReferencedVirtualElement}
options={{
placement: "top",
offset: 8,
flip: false,
}}
shouldShow={shouldShow}
>
<div className={classes.toolbar}>
<Tooltip position="top" label={t("Convert to attachment")} withinPortal={false}>
<ActionIcon
onClick={handleConvertToAttachment}
size="lg"
aria-label={t("Convert to attachment")}
variant="subtle"
>
<IconPaperclip size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Delete")} withinPortal={false}>
<ActionIcon
onClick={handleDelete}
size="lg"
aria-label={t("Delete")}
variant="subtle"
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</div>
</BaseBubbleMenu>
);
}
export default PdfMenu;
@@ -0,0 +1,100 @@
.pdfWrapper {
display: flex;
justify-content: center;
align-items: center;
max-width: 100%;
border-radius: 8px;
overflow: hidden;
}
.skeleton {
animation: pulse 1.2s ease-in-out infinite;
@mixin light {
background: linear-gradient(-90deg, var(--mantine-color-gray-3) 0%, var(--mantine-color-gray-1) 50%, var(--mantine-color-gray-3) 100%);
background-size: 400% 400%;
}
@mixin dark {
background: linear-gradient(-90deg, var(--mantine-color-dark-6) 0%, var(--mantine-color-dark-5) 50%, var(--mantine-color-dark-6) 100%);
background-size: 400% 400%;
}
@keyframes pulse {
0% {
background-position: 0% 0%;
}
100% {
background-position: -135% 0%;
}
}
}
.pdfContainer {
display: flex;
justify-content: center;
}
.pdfResizeWrapper {
@mixin light {
background-color: var(--mantine-color-gray-0);
}
@mixin dark {
background-color: var(--mantine-color-dark-7);
}
}
.pdfIframe {
width: 100%;
height: 100%;
border: none;
border-radius: 8px;
}
.hoverMenu {
position: absolute;
top: 56px;
right: 8px;
z-index: 2;
display: flex;
gap: 4px;
padding: 4px;
border-radius: 6px;
opacity: 0;
transition: opacity 0.15s ease;
background-color: rgba(0, 0, 0, 0.5);
}
.hoverMenu::before {
content: "";
position: absolute;
inset: -12px;
}
.hoverMenu:hover {
opacity: 1;
}
.pdfResizeWrapper:hover .hoverMenu {
opacity: 1;
}
.pdfError {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 32px;
border-radius: 8px;
cursor: pointer;
@mixin light {
background-color: var(--mantine-color-gray-0);
}
@mixin dark {
background-color: var(--mantine-color-dark-7);
}
}
@@ -0,0 +1,168 @@
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { ActionIcon, Group, Loader, Text, Tooltip } from "@mantine/core";
import { useCallback, useMemo, useState } from "react";
import { getFileUrl } from "@/lib/config.ts";
import { ResizableWrapper } from "../common/resizable-wrapper";
import clsx from "clsx";
import classes from "./pdf-view.module.css";
import { useTranslation } from "react-i18next";
import { isInternalFileUrl } from "@docmost/editor-ext";
import {
IconFileTypePdf,
IconPaperclip,
IconTrash,
} from "@tabler/icons-react";
export default function PdfView(props: NodeViewProps) {
const { t } = useTranslation();
const { editor, node, getPos, selected, updateAttributes } = props;
const { src, placeholder, width: nodeWidth, height: nodeHeight } = node.attrs;
const [hasError, setHasError] = useState(false);
const safeSrc = useMemo(() => {
if (!src || !isInternalFileUrl(src)) return null;
return getFileUrl(src);
}, [src]);
const handleSelect = useCallback(() => {
const pos = getPos();
if (pos !== undefined) {
editor.commands.setNodeSelection(pos);
}
}, [editor, getPos]);
const handleResize = useCallback(
(newWidth: number, newHeight: number) => {
updateAttributes({ width: newWidth, height: newHeight });
},
[updateAttributes],
);
const handleConvertToAttachment = useCallback(() => {
if (!src) return;
const pos = getPos();
if (pos === undefined) return;
const currentNode = editor.state.doc.nodeAt(pos);
if (!currentNode || currentNode.type.name !== "pdf") return;
editor
.chain()
.insertContentAt(
{ from: pos, to: pos + currentNode.nodeSize },
{
type: "attachment",
attrs: {
url: currentNode.attrs.src,
name: currentNode.attrs.name,
attachmentId: currentNode.attrs.attachmentId,
size: currentNode.attrs.size,
mime: "application/pdf",
},
},
)
.run();
}, [editor, src, getPos]);
const handleDelete = useCallback(() => {
const pos = getPos();
if (pos === undefined) return;
editor.commands.setNodeSelection(pos);
editor.commands.deleteSelection();
}, [editor, getPos]);
if (!src || !safeSrc) {
return (
<NodeViewWrapper data-drag-handle>
<div className={`${classes.pdfWrapper} ${classes.skeleton}`} style={{ height: 600 }}>
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end">
{placeholder?.name
? t("Uploading {{name}}", { name: placeholder.name })
: t("Uploading file")}
</Text>
</Group>
</div>
</NodeViewWrapper>
);
}
if (hasError) {
return (
<NodeViewWrapper data-drag-handle>
<div data-pdf-error className={clsx(classes.pdfError, { "ProseMirror-selectednode": selected })} onClick={handleSelect}>
<IconFileTypePdf size={32} stroke={1.5} />
<Text size="sm" c="dimmed">
{t("Failed to load PDF")}
</Text>
</div>
</NodeViewWrapper>
);
}
return (
<NodeViewWrapper data-drag-handle className={classes.pdfNodeView}>
<div className={classes.pdfContainer}>
<ResizableWrapper
initialWidth={nodeWidth || 800}
initialHeight={nodeHeight || 600}
minWidth={200}
maxWidth={1200}
minHeight={200}
maxHeight={1200}
onResize={handleResize}
isEditable={editor.isEditable}
selected={selected}
className={clsx(classes.pdfResizeWrapper, {
"ProseMirror-selectednode": selected,
})}
>
<iframe
className={classes.pdfIframe}
src={safeSrc}
loading="lazy"
frameBorder="0"
onError={() => setHasError(true)}
onLoad={(e) => {
try {
const iframe = e.currentTarget;
const status = iframe.contentDocument?.querySelector("pre")?.textContent;
if (status && status.includes('"statusCode":404')) {
setHasError(true);
}
} catch {
// cross-origin - can't inspect, assume OK
}
}}
/>
{editor.isEditable && (
<div className={classes.hoverMenu}>
<Tooltip position="top" label={t("Convert to attachment")} withinPortal>
<ActionIcon
size="sm"
variant="filled"
color="dark"
onClick={handleConvertToAttachment}
aria-label={t("Convert to attachment")}
>
<IconPaperclip size={14} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Delete")} withinPortal>
<ActionIcon
size="sm"
variant="filled"
color="dark"
onClick={handleDelete}
aria-label={t("Delete")}
>
<IconTrash size={14} />
</ActionIcon>
</Tooltip>
</div>
)}
</ResizableWrapper>
</div>
</NodeViewWrapper>
);
}
@@ -0,0 +1,36 @@
import { handlePdfUpload } from "@docmost/editor-ext";
import { uploadFile } from "@/features/page/services/page-service.ts";
import { notifications } from "@mantine/notifications";
import { getFileUploadSizeLimit } from "@/lib/config.ts";
import { formatBytes } from "@/lib";
import i18n from "@/i18n.ts";
export const uploadPdfAction = handlePdfUpload({
onUpload: async (file: File, pageId: string): Promise<any> => {
try {
return await uploadFile(file, pageId);
} catch (err) {
notifications.show({
color: "red",
message: err?.response.data.message,
});
throw err;
}
},
validateFn: (file) => {
if (file.type !== "application/pdf") {
return false;
}
if (file.size > getFileUploadSizeLimit()) {
notifications.show({
color: "red",
message: i18n.t("File exceeds the {{limit}} attachment limit", {
limit: formatBytes(getFileUploadSizeLimit()),
}),
});
return false;
}
return true;
},
});
@@ -12,7 +12,9 @@ import {
IconMath,
IconMathFunction,
IconMovie,
IconMusic,
IconPaperclip,
IconFileTypePdf,
IconPhoto,
IconTable,
IconTypography,
@@ -30,7 +32,9 @@ import {
} from "@/features/editor/components/slash-menu/types";
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
import { uploadAudioAction } from "@/features/editor/components/audio/upload-audio-action.tsx";
import { uploadAttachmentAction } from "@/features/editor/components/attachment/upload-attachment-action.tsx";
import { uploadPdfAction } from "@/features/editor/components/pdf/upload-pdf-action.tsx";
import IconExcalidraw from "@/components/icons/icon-excalidraw";
import IconMermaid from "@/components/icons/icon-mermaid";
import IconDrawio from "@/components/icons/icon-drawio";
@@ -161,7 +165,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
{
title: "Image",
description: "Upload any image from your device.",
searchTerms: ["photo", "picture", "media"],
searchTerms: ["photo", "picture", "media", "file", "attachment"],
icon: IconPhoto,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
@@ -194,7 +198,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
{
title: "Video",
description: "Upload any video from your device.",
searchTerms: ["video", "mp4", "media"],
searchTerms: ["video", "mp4", "media", "file", "attachment"],
icon: IconMovie,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
@@ -224,10 +228,74 @@ const CommandGroups: SlashMenuGroupedItemsType = {
input.click();
},
},
{
title: "Audio",
description: "Upload any audio from your device.",
searchTerms: ["audio", "music", "sound", "mp3", "media", "file", "attachment"],
icon: IconMusic,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
// @ts-ignore
const pageId = editor.storage?.pageId;
if (!pageId) return;
// upload audio
const input = document.createElement("input");
input.type = "file";
input.accept = "audio/*";
input.multiple = true;
input.style.display = "none";
document.body.appendChild(input);
input.onchange = async () => {
if (input.files?.length) {
for (const file of input.files) {
const pos = editor.view.state.selection.from;
uploadAudioAction(file, editor, pos, pageId);
}
}
input.remove();
};
input.click();
},
},
{
title: "Embed PDF",
description: "Upload and embed a PDF file.",
searchTerms: ["pdf", "document", "embed"],
icon: IconFileTypePdf,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
// @ts-ignore
const pageId = editor.storage?.pageId;
if (!pageId) return;
const input = document.createElement("input");
input.type = "file";
input.accept = "application/pdf";
input.style.display = "none";
document.body.appendChild(input);
input.onchange = async () => {
if (input.files?.length) {
for (const file of input.files) {
const pos = editor.view.state.selection.from;
uploadPdfAction(file, editor, pos, pageId);
}
}
input.remove();
};
input.click();
},
},
{
title: "File attachment",
description: "Upload any file from your device.",
searchTerms: ["file", "attachment", "upload", "pdf", "csv", "zip"],
searchTerms: ["file", "attachment", "upload", "csv", "zip"],
icon: IconPaperclip,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
@@ -359,7 +427,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
editor.chain().focus().deleteRange(range).setDrawio().run(),
},
{
title: "Excalidraw diagram",
title: "Excalidraw (Whiteboard)",
description: "Draw and sketch excalidraw diagrams",
searchTerms: ["diagrams", "draw", "sketch", "whiteboard"],
icon: IconExcalidraw,
@@ -548,7 +616,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
{
title: "YouTube",
description: "Embed YouTube video",
searchTerms: ["youtube", "yt"],
searchTerms: ["youtube", "yt", "media", "video"],
icon: YoutubeIcon,
command: ({ editor, range }: CommandProps) => {
editor
@@ -647,7 +715,11 @@ export const getSuggestionItems = ({
});
if (filteredItems.length) {
filteredGroups[group] = filteredItems;
filteredGroups[group] = filteredItems.sort((a, b) => {
const aTitle = a.title.toLowerCase().includes(search) ? 0 : 1;
const bTitle = b.title.toLowerCase().includes(search) ? 0 : 1;
return aTitle - bTitle;
});
}
}
@@ -5,6 +5,9 @@
max-width: 100%;
border-radius: 8px;
overflow: hidden;
}
.skeleton {
animation: pulse 1.2s ease-in-out infinite;
@mixin light {
@@ -26,6 +29,7 @@
}
}
}
.video {
display: block;
width: 100%;
@@ -33,6 +33,7 @@ export default function VideoView(props: NodeViewProps) {
className={clsx(
selected && "ProseMirror-selectednode",
classes.videoWrapper,
!src && classes.skeleton,
alignClass,
)}
style={{
@@ -30,6 +30,7 @@ import {
TiptapImage,
Callout,
TiptapVideo,
TiptapAudio,
LinkExtension,
Selection,
Attachment,
@@ -37,6 +38,7 @@ import {
Drawio,
Excalidraw,
Embed,
TiptapPdf,
SearchAndReplace,
Mention,
TableDndExtension,
@@ -68,11 +70,13 @@ import ImageView from "@/features/editor/components/image/image-view.tsx";
import CalloutView from "@/features/editor/components/callout/callout-view.tsx";
import StatusView from "@/features/editor/components/status/status-view.tsx";
import VideoView from "@/features/editor/components/video/video-view.tsx";
import AudioView from "@/features/editor/components/audio/audio-view.tsx";
import AttachmentView from "@/features/editor/components/attachment/attachment-view.tsx";
import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx";
import DrawioView from "../components/drawio/drawio-view";
import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx";
import EmbedView from "@/features/editor/components/embed/embed-view.tsx";
import PdfView from "@/features/editor/components/pdf/pdf-view.tsx";
import SubpagesView from "@/features/editor/components/subpages/subpages-view.tsx";
import { common, createLowlight } from "lowlight";
import plaintext from "highlight.js/lib/languages/plaintext";
@@ -269,6 +273,9 @@ export const mainExtensions = [
className: buildResizeClasses("node-video"),
},
}),
TiptapAudio.configure({
view: AudioView,
}),
Callout.configure({
view: CalloutView,
}),
@@ -313,6 +320,9 @@ export const mainExtensions = [
Embed.configure({
view: EmbedView,
}),
TiptapPdf.configure({
view: PdfView,
}),
Subpages.configure({
view: SubpagesView,
}),
@@ -45,6 +45,7 @@ import TableMenu from "@/features/editor/components/table/table-menu.tsx";
import ImageMenu from "@/features/editor/components/image/image-menu.tsx";
import CalloutMenu from "@/features/editor/components/callout/callout-menu.tsx";
import VideoMenu from "@/features/editor/components/video/video-menu.tsx";
import PdfMenu from "@/features/editor/components/pdf/pdf-menu.tsx";
import SubpagesMenu from "@/features/editor/components/subpages/subpages-menu.tsx";
import {
handleFileDrop,
@@ -414,6 +415,7 @@ export default function PageEditor({
<TableCellMenu editor={editor} appendTo={menuContainerRef} />
<ImageMenu editor={editor} />
<VideoMenu editor={editor} />
<PdfMenu editor={editor} />
<CalloutMenu editor={editor} />
<SubpagesMenu editor={editor} />
<ExcalidrawMenu editor={editor} />
@@ -133,10 +133,18 @@
border-top: 1px solid #68cef8;
}
&[contenteditable="false"] hr.ProseMirror-selectednode {
border-top: none;
}
.ProseMirror-selectednode {
outline: 2px solid #70cff8;
}
&[contenteditable="false"] .ProseMirror-selectednode {
outline: none;
}
& > .react-renderer {
margin-top: var(--mantine-spacing-sm);
margin-bottom: var(--mantine-spacing-sm);
@@ -8,7 +8,7 @@
}
}
.node-image, .node-video, .node-excalidraw, .node-drawio {
.node-image, .node-video, .node-pdf, .node-excalidraw, .node-drawio {
&.ProseMirror-selectednode {
outline: none;
}
@@ -37,5 +37,28 @@
font-size: var(--mantine-font-size-md);
line-height: var(--mantine-line-height-md);
}
.media-pulse {
animation: media-pulse 1.2s ease-in-out infinite;
@mixin light {
background: linear-gradient(-90deg, var(--mantine-color-gray-3) 0%, var(--mantine-color-gray-1) 50%, var(--mantine-color-gray-3) 100%);
background-size: 400% 400%;
}
@mixin dark {
background: linear-gradient(-90deg, var(--mantine-color-dark-6) 0%, var(--mantine-color-dark-5) 50%, var(--mantine-color-dark-6) 100%);
background-size: 400% 400%;
}
@keyframes media-pulse {
0% {
background-position: 0% 0%;
}
100% {
background-position: -135% 0%;
}
}
}
}
+1
View File
@@ -14,6 +14,7 @@ i18n
.init({
fallbackLng: "en-US",
debug: false,
showSupportNotice: false,
load: 'currentOnly',
interpolation: {
@@ -24,6 +24,8 @@ import {
CustomTable,
TiptapImage,
TiptapVideo,
TiptapAudio,
TiptapPdf,
TrailingNode,
Attachment,
Drawio,
@@ -86,6 +88,8 @@ export const tiptapExtensions = [
Youtube,
TiptapImage,
TiptapVideo,
TiptapAudio,
TiptapPdf,
Callout,
Attachment,
CustomCodeBlock,
@@ -102,6 +102,8 @@ export function isAttachmentNode(nodeType: string) {
'attachment',
'image',
'video',
'audio',
'pdf',
'excalidraw',
'drawio',
];
@@ -15,4 +15,9 @@ export const inlineFileExtensions = [
'.pdf',
'.mp4',
'.mov',
'.mp3',
'.wav',
'.ogg',
'.m4a',
'.webm',
];
@@ -457,6 +457,10 @@ export class AttachmentController {
const rangeHeader = req.headers.range;
res.header('Accept-Ranges', 'bytes');
res.header(
'Content-Security-Policy',
"base-uri 'none'; object-src 'self'; default-src 'self';",
);
if (!inlineFileExtensions.includes(attachment.fileExt)) {
res.header(
@@ -190,13 +190,32 @@ export class ImportAttachmentService {
}
}
// Build a map from resolved archive path → real filename from Confluence
// metadata. Confluence Server archives often store files under numeric IDs
// (e.g. "attachments/65601/65602") instead of the original filename.
const pageDir = path.dirname(pageRelativePath);
const attachmentNameByRelPath = new Map<string, string>();
for (const attachment of pageAttachments) {
const relPath = resolveRelativeAttachmentPath(
attachment.href,
pageDir,
attachmentCandidates,
);
if (relPath && attachment.fileName) {
attachmentNameByRelPath.set(relPath, attachment.fileName);
}
}
const uploadOnce = (relPath: string) => {
const abs = attachmentCandidates.get(relPath)!;
const attachmentId = v7();
const ext = path.extname(abs);
const realName = attachmentNameByRelPath.get(relPath);
const baseName = realName || path.basename(abs);
const ext = path.extname(baseName);
const fileNameWithExt =
sanitizeFileName(path.basename(abs, ext)) + ext.toLowerCase();
sanitizeFileName(path.basename(baseName, ext)) + ext.toLowerCase();
const storageFilePath = `${getAttachmentFolderPath(
AttachmentType.File,
@@ -240,7 +259,6 @@ export class ImportAttachmentService {
return fresh;
};
const pageDir = path.dirname(pageRelativePath);
const $ = load(html);
// image
@@ -335,6 +353,28 @@ export class ImportAttachmentService {
unwrapFromParagraph($, $vid);
}
// audio
for (const audEl of $('audio').toArray()) {
const $aud = $(audEl);
const src = cleanUrlString($aud.attr('src') ?? '')!;
if (!src || src.startsWith('http')) continue;
const relPath = resolveRelativeAttachmentPath(
src,
pageDir,
attachmentCandidates,
);
if (!relPath) continue;
const { attachmentId, apiFilePath } = processFile(relPath);
$aud
.attr('src', apiFilePath)
.attr('data-attachment-id', attachmentId);
unwrapFromParagraph($, $aud);
}
// <div data-type="attachment">
for (const el of $('div[data-type="attachment"]').toArray()) {
const $oldDiv = $(el);
@@ -401,7 +441,18 @@ export class ImportAttachmentService {
const { attachmentId, apiFilePath, abs } = processFile(relPath);
const ext = path.extname(relPath).toLowerCase();
if (ext === '.mp4') {
const audioExtensions = new Set(['.mp3', '.wav', '.ogg', '.m4a', '.webm', '.flac', '.aac']);
if (ext === '.pdf') {
const $pdf = $('<div>')
.attr('data-type', 'pdf')
.attr('src', apiFilePath)
.attr('data-attachment-id', attachmentId)
.attr('width', '800')
.attr('height', '600');
$a.replaceWith($pdf);
unwrapFromParagraph($, $pdf);
} else if (ext === '.mp4') {
const $video = $('<video>')
.attr('src', apiFilePath)
.attr('data-attachment-id', attachmentId)
@@ -409,6 +460,12 @@ export class ImportAttachmentService {
.attr('data-align', 'center');
$a.replaceWith($video);
unwrapFromParagraph($, $video);
} else if (audioExtensions.has(ext)) {
const $audio = $('<audio>')
.attr('src', apiFilePath)
.attr('data-attachment-id', attachmentId);
$a.replaceWith($audio);
unwrapFromParagraph($, $audio);
} else {
const confAliasName = $a.attr('data-linked-resource-default-alias');
let attachmentName = path.basename(abs);
@@ -555,7 +612,7 @@ export class ImportAttachmentService {
// Post-process DOM elements to add file sizes after uploads complete
// This avoids blocking file operations during initial DOM processing
const elementsNeedingSize = $(
'[data-attachment-id]:not([data-attachment-size])',
'[data-attachment-id]:not([data-attachment-size]):not([data-size])',
);
for (const element of elementsNeedingSize.toArray()) {
const $el = $(element);
@@ -570,7 +627,14 @@ export class ImportAttachmentService {
if (processedEntry) {
try {
const stat = await fs.stat(processedEntry.abs);
$el.attr('data-attachment-size', stat.size.toString());
const sizeStr = stat.size.toString();
const tagName = $el.prop('tagName')?.toLowerCase();
// audio and pdf nodes use data-size, attachment nodes use data-attachment-size
if (tagName === 'audio' || $el.attr('data-type') === 'pdf') {
$el.attr('data-size', sizeStr);
} else {
$el.attr('data-attachment-size', sizeStr);
}
} catch (error) {
this.logger.debug(
`Could not get size for ${processedEntry.abs}:`,
@@ -41,6 +41,15 @@ export function resolveRelativeAttachmentPath(
'ImportUtils',
);
}
// Confluence Server uses "/download/attachments/..." in HTML but the ZIP
// stores files under "attachments/...". Strip the "download/" prefix so
// the path can match candidates from the archive.
const confluenceStripped = mainRel.replace(
/^download\/attachments\//,
'attachments/',
);
const fallback = path
.normalize(path.join(pageDir, mainRel))
.split(path.sep)
@@ -49,9 +58,13 @@ export function resolveRelativeAttachmentPath(
if (attachmentCandidates.has(mainRel)) {
return mainRel;
}
if (confluenceStripped !== mainRel && attachmentCandidates.has(confluenceStripped)) {
return confluenceStripped;
}
if (attachmentCandidates.has(fallback)) {
return fallback;
}
return null;
}
@@ -66,25 +66,25 @@ export class LocalDriver implements StorageDriver {
}
async readStream(filePath: string): Promise<Readable> {
try {
return createReadStream(this._fullPath(filePath));
} catch (err) {
throw new Error(`Failed to read file: ${(err as Error).message}`);
const fullPath = this._fullPath(filePath);
if (!(await fs.pathExists(fullPath))) {
throw new Error(`File not found: ${filePath}`);
}
return createReadStream(fullPath);
}
async readRangeStream(
filePath: string,
range: { start: number; end: number },
): Promise<Readable> {
try {
return createReadStream(this._fullPath(filePath), {
start: range.start,
end: range.end,
});
} catch (err) {
throw new Error(`Failed to read file: ${(err as Error).message}`);
const fullPath = this._fullPath(filePath);
if (!(await fs.pathExists(fullPath))) {
throw new Error(`File not found: ${filePath}`);
}
return createReadStream(fullPath, {
start: range.start,
end: range.end,
});
}
async exists(filePath: string): Promise<boolean> {
+2 -2
View File
@@ -30,6 +30,7 @@
"@joplin/turndown-plugin-gfm": "^1.0.64",
"@sindresorhus/slugify": "3.0.0",
"@tiptap/core": "3.20.4",
"@tiptap/extension-audio": "3.20.4",
"@tiptap/extension-code-block": "3.20.4",
"@tiptap/extension-collaboration": "3.20.4",
"@tiptap/extension-collaboration-caret": "3.20.4",
@@ -94,8 +95,7 @@
"packageManager": "pnpm@10.4.0",
"pnpm": {
"patchedDependencies": {
"react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch",
"@tiptap/core": "patches/@tiptap__core.patch"
"react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch"
},
"overrides": {
"prosemirror-changeset": "2.4.0",
+4
View File
@@ -11,6 +11,7 @@ export * from "./lib/media-utils";
export * from "./lib/link";
export * from "./lib/selection";
export * from "./lib/attachment";
export * from "./lib/audio";
export * from "./lib/custom-code-block";
export * from "./lib/drawio";
export * from "./lib/excalidraw";
@@ -27,3 +28,6 @@ export * from "./lib/shared-storage";
export * from "./lib/recreate-transform";
export * from "./lib/columns";
export * from "./lib/status";
export * from "./lib/pdf";
export * from "./lib/resizable-nodeview";
@@ -0,0 +1,139 @@
import { MediaUploadOptions, UploadFn } from "../media-utils";
import { IAttachment } from "../types";
import { generateNodeId } from "../utils";
import { Node } from "@tiptap/pm/model";
import { Command } from "@tiptap/core";
const findAudioNodeByPlaceholderId = (
doc: Node,
placeholderId: string,
): { node: Node; pos: number } | null => {
let result: { node: Node; pos: number } | null = null;
doc.descendants((node, pos) => {
if (result) return false;
if (
node.type.name === "audio" &&
node.attrs.placeholder?.id === placeholderId
) {
result = { node, pos };
return false;
}
return true;
});
return result;
};
const handleAudioUpload =
({ validateFn, onUpload }: MediaUploadOptions): UploadFn =>
async (file, editor, pos, pageId) => {
const validated = validateFn?.(file);
// @ts-ignore
if (!validated) return;
const objectUrl = URL.createObjectURL(file);
const placeholderId = generateNodeId();
let placeholderInserted = false;
editor.storage.shared.audioPreviews =
editor.storage.shared.audioPreviews || {};
editor.storage.shared.audioPreviews[placeholderId] = objectUrl;
const insertPlaceholder = (): Command => {
return ({ tr, state }) => {
const initialPlaceholderNode = state.schema.nodes.audio?.create({
placeholder: {
id: placeholderId,
name: file.name,
},
});
if (!initialPlaceholderNode) return false;
const { parent } = tr.doc.resolve(pos);
const isEmptyTextBlock = parent.isTextblock && !parent.childCount;
if (isEmptyTextBlock) {
tr.replaceRangeWith(pos - 1, pos + 1, initialPlaceholderNode);
} else {
tr.insert(pos, initialPlaceholderNode);
}
return true;
};
};
const replacePlaceholderWithAudio = (attachment: IAttachment): Command => {
return ({ tr }) => {
const { pos: currentPos = null } =
findAudioNodeByPlaceholderId(tr.doc, placeholderId) || {};
if (currentPos === null || !attachment) return;
tr.setNodeMarkup(currentPos, undefined, {
src: `/api/files/${attachment.id}/${attachment.fileName}`,
attachmentId: attachment.id,
size: attachment.fileSize,
});
return true;
};
};
const removePlaceholder = (): Command => {
return ({ tr }) => {
const { pos: currentPos = null } =
findAudioNodeByPlaceholderId(tr.doc, placeholderId) || {};
if (currentPos === null) return false;
tr.delete(currentPos, currentPos + 2);
return true;
};
};
const insertPlaceholderTimeout = setTimeout(() => {
editor.commands.command(insertPlaceholder());
placeholderInserted = true;
}, 250);
const disposePreviewFile = () => {
URL.revokeObjectURL(objectUrl);
if (editor.storage.shared.audioPreviews) {
delete editor.storage.shared.audioPreviews[placeholderId];
}
};
try {
const attachment: IAttachment = await onUpload(file, pageId);
clearTimeout(insertPlaceholderTimeout);
if (placeholderInserted) {
setTimeout(() => {
editor.commands.command(replacePlaceholderWithAudio(attachment));
disposePreviewFile();
}, 100);
} else {
editor
.chain()
.command(insertPlaceholder())
.command(replacePlaceholderWithAudio(attachment))
.run();
disposePreviewFile();
}
} catch (error) {
clearTimeout(insertPlaceholderTimeout);
editor.commands.command(removePlaceholder());
disposePreviewFile();
}
};
export { handleAudioUpload };
+134
View File
@@ -0,0 +1,134 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { normalizeFileUrl } from "../media-utils";
import { sanitizeUrl, isInternalFileUrl } from "../utils";
export interface AudioOptions {
view: any;
HTMLAttributes: Record<string, any>;
}
export interface AudioAttributes {
src?: string;
attachmentId?: string;
size?: number;
placeholder?: {
id: string;
name: string;
};
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
audioBlock: {
setAudio: (attributes: AudioAttributes) => ReturnType;
};
}
}
export const TiptapAudio = Node.create<AudioOptions>({
name: "audio",
group: "block",
isolating: true,
atom: true,
defining: true,
draggable: true,
addOptions() {
return {
view: null,
HTMLAttributes: {},
};
},
addAttributes() {
return {
src: {
default: "",
parseHTML: (element) => {
const src = element.getAttribute("src");
const sanitized = sanitizeUrl(src);
return isInternalFileUrl(sanitized) ? sanitized : "";
},
renderHTML: (attributes) => ({
src: isInternalFileUrl(attributes.src)
? sanitizeUrl(attributes.src)
: "",
}),
},
attachmentId: {
default: undefined,
parseHTML: (element) => element.getAttribute("data-attachment-id"),
renderHTML: (attributes: AudioAttributes) => ({
"data-attachment-id": attributes.attachmentId,
}),
},
size: {
default: null,
parseHTML: (element) => element.getAttribute("data-size"),
renderHTML: (attributes: AudioAttributes) => ({
"data-size": attributes.size,
}),
},
placeholder: {
default: null,
rendered: false,
},
};
},
parseHTML() {
return [
{
tag: "audio",
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"audio",
mergeAttributes(
{ controls: "true", preload: "metadata" },
this.options.HTMLAttributes,
HTMLAttributes,
),
["source", { src: HTMLAttributes.src }],
];
},
addCommands() {
return {
setAudio:
(attrs: AudioAttributes) =>
({ commands }) => {
return commands.insertContent({
type: "audio",
attrs: attrs,
});
},
};
},
addNodeView() {
if (this.options.view) {
this.editor.isInitialized = true;
return ReactNodeViewRenderer(this.options.view);
}
return ({ node, HTMLAttributes }) => {
const dom = document.createElement("div");
const audio = document.createElement("audio");
const src = node.attrs.src;
if (src && isInternalFileUrl(src)) {
audio.src = normalizeFileUrl(src);
}
audio.controls = true;
audio.preload = "metadata";
audio.style.width = "100%";
dom.append(audio);
return { dom };
};
},
});
@@ -0,0 +1,2 @@
export { TiptapAudio } from "./audio";
export * from "./audio-upload";
+5 -5
View File
@@ -1,5 +1,6 @@
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import type { ResizableNodeViewDirection } from "@tiptap/core";
import { Node, mergeAttributes } from "@tiptap/core";
import { ResizableNodeView } from "./resizable-nodeview";
import type { ResizableNodeViewDirection } from "./resizable-nodeview";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { normalizeFileUrl } from "./media-utils";
@@ -320,12 +321,11 @@ export const Drawio = Node.create<DrawioOptions>({
// Show skeleton background while image loads from server
dom.style.pointerEvents = "none";
dom.style.background =
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))";
el.classList.add("media-pulse");
el.onload = () => {
dom.style.pointerEvents = "";
dom.style.background = "";
el.classList.remove("media-pulse");
};
return nodeView;
+2 -2
View File
@@ -64,14 +64,14 @@ export const Embed = Node.create<EmbedOptions>({
}),
},
width: {
default: 640,
default: 800,
parseHTML: (element) => element.getAttribute("data-width"),
renderHTML: (attributes: EmbedAttributes) => ({
"data-width": attributes.width,
}),
},
height: {
default: 480,
default: 600,
parseHTML: (element) => element.getAttribute("data-height"),
renderHTML: (attributes: EmbedAttributes) => ({
"data-height": attributes.height,
+5 -5
View File
@@ -1,5 +1,6 @@
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import type { ResizableNodeViewDirection } from "@tiptap/core";
import { Node, mergeAttributes } from "@tiptap/core";
import { ResizableNodeView } from "./resizable-nodeview";
import type { ResizableNodeViewDirection } from "./resizable-nodeview";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { normalizeFileUrl } from "./media-utils";
@@ -320,12 +321,11 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
// Show skeleton background while image loads from server
dom.style.pointerEvents = "none";
dom.style.background =
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))";
el.classList.add("media-pulse");
el.onload = () => {
dom.style.pointerEvents = "";
dom.style.background = "";
el.classList.remove("media-pulse");
};
return nodeView;
+4 -5
View File
@@ -4,10 +4,10 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
import {
mergeAttributes,
Range,
ResizableNodeView,
} from "@tiptap/core";
import { ResizableNodeView } from "../resizable-nodeview";
import type { ResizableNodeViewDirection } from "../resizable-nodeview";
import { normalizeFileUrl } from "../media-utils";
import type { ResizableNodeViewDirection } from "@tiptap/core";
export type ImageResizeOptions = {
enabled: boolean;
@@ -362,12 +362,11 @@ export const TiptapImage = Image.extend<ImageOptions>({
// Show skeleton background while image loads from server
dom.style.pointerEvents = "none";
dom.style.background =
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))";
el.classList.add("media-pulse");
el.onload = () => {
dom.style.pointerEvents = "";
dom.style.background = "";
el.classList.remove("media-pulse");
};
return nodeView;
+2
View File
@@ -0,0 +1,2 @@
export { TiptapPdf } from "./pdf";
export * from "./pdf-upload";
@@ -0,0 +1,123 @@
import { MediaUploadOptions, UploadFn } from "../media-utils";
import { IAttachment } from "../types";
import { generateNodeId } from "../utils";
import { Node } from "@tiptap/pm/model";
import { Command } from "@tiptap/core";
const findPdfNodeByPlaceholderId = (
doc: Node,
placeholderId: string,
): { node: Node; pos: number } | null => {
let result: { node: Node; pos: number } | null = null;
doc.descendants((node, pos) => {
if (result) return false;
if (
node.type.name === "pdf" &&
node.attrs.placeholder?.id === placeholderId
) {
result = { node, pos };
return false;
}
return true;
});
return result;
};
const handlePdfUpload =
({ validateFn, onUpload }: MediaUploadOptions): UploadFn =>
async (file, editor, pos, pageId) => {
const validated = validateFn?.(file);
// @ts-ignore
if (!validated) return;
const placeholderId = generateNodeId();
let placeholderInserted = false;
const insertPlaceholder = (): Command => {
return ({ tr, state }) => {
const initialPlaceholderNode = state.schema.nodes.pdf?.create({
placeholder: {
id: placeholderId,
name: file.name,
},
});
if (!initialPlaceholderNode) return false;
const { parent } = tr.doc.resolve(pos);
const isEmptyTextBlock = parent.isTextblock && !parent.childCount;
if (isEmptyTextBlock) {
tr.replaceRangeWith(pos - 1, pos + 1, initialPlaceholderNode);
} else {
tr.insert(pos, initialPlaceholderNode);
}
return true;
};
};
const replacePlaceholderWithPdf = (attachment: IAttachment): Command => {
return ({ tr }) => {
const { pos: currentPos = null } =
findPdfNodeByPlaceholderId(tr.doc, placeholderId) || {};
if (currentPos === null || !attachment) return;
tr.setNodeMarkup(currentPos, undefined, {
src: `/api/files/${attachment.id}/${attachment.fileName}`,
name: attachment.fileName,
attachmentId: attachment.id,
size: attachment.fileSize,
});
return true;
};
};
const removePlaceholder = (): Command => {
return ({ tr }) => {
const { pos: currentPos = null } =
findPdfNodeByPlaceholderId(tr.doc, placeholderId) || {};
if (currentPos === null) return false;
tr.delete(currentPos, currentPos + 2);
return true;
};
};
const insertPlaceholderTimeout = setTimeout(() => {
editor.commands.command(insertPlaceholder());
placeholderInserted = true;
}, 250);
try {
const attachment: IAttachment = await onUpload(file, pageId);
clearTimeout(insertPlaceholderTimeout);
if (placeholderInserted) {
setTimeout(() => {
editor.commands.command(replacePlaceholderWithPdf(attachment));
}, 100);
} else {
editor
.chain()
.command(insertPlaceholder())
.command(replacePlaceholderWithPdf(attachment))
.run();
}
} catch (error) {
clearTimeout(insertPlaceholderTimeout);
editor.commands.command(removePlaceholder());
}
};
export { handlePdfUpload };
+156
View File
@@ -0,0 +1,156 @@
import { ReactNodeViewRenderer } from "@tiptap/react";
import { Node, mergeAttributes } from "@tiptap/core";
import { sanitizeUrl, isInternalFileUrl } from "../utils";
export type PdfOptions = {
view: any;
HTMLAttributes: Record<string, any>;
};
export type PdfAttributes = {
src?: string;
name?: string;
attachmentId?: string;
size?: number;
width?: number;
height?: number;
placeholder?: {
id: string;
name: string;
};
};
declare module "@tiptap/core" {
interface Commands<ReturnType> {
pdfBlock: {
setPdf: (attributes: PdfAttributes) => ReturnType;
};
}
}
export const TiptapPdf = Node.create<PdfOptions>({
name: "pdf",
group: "block",
isolating: true,
atom: true,
defining: true,
draggable: true,
addOptions() {
return {
view: null,
HTMLAttributes: {},
};
},
addAttributes() {
return {
src: {
default: "",
parseHTML: (element) => {
const src = element.getAttribute("src");
const sanitized = sanitizeUrl(src);
return isInternalFileUrl(sanitized) ? sanitized : "";
},
renderHTML: (attributes) => ({
src: isInternalFileUrl(attributes.src) ? sanitizeUrl(attributes.src) : "",
}),
},
name: {
default: undefined,
parseHTML: (element) => element.getAttribute("data-name"),
renderHTML: (attributes: PdfAttributes) => ({
"data-name": attributes.name,
}),
},
attachmentId: {
default: undefined,
parseHTML: (element) => element.getAttribute("data-attachment-id"),
renderHTML: (attributes: PdfAttributes) => ({
"data-attachment-id": attributes.attachmentId,
}),
},
size: {
default: null,
parseHTML: (element) => element.getAttribute("data-size"),
renderHTML: (attributes: PdfAttributes) => ({
"data-size": attributes.size,
}),
},
width: {
default: 800,
parseHTML: (element) => {
const raw = element.getAttribute("width");
if (!raw) return null;
const num = parseFloat(raw);
return isNaN(num) ? null : num;
},
renderHTML: (attributes: PdfAttributes) => ({
width: attributes.width,
}),
},
height: {
default: 600,
parseHTML: (element) => {
const raw = element.getAttribute("height");
if (!raw) return null;
const num = parseFloat(raw);
return isNaN(num) ? null : num;
},
renderHTML: (attributes: PdfAttributes) => ({
height: attributes.height,
}),
},
placeholder: {
default: null,
rendered: false,
},
};
},
parseHTML() {
return [
{
tag: `div[data-type="${this.name}"]`,
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(
{ "data-type": this.name },
this.options.HTMLAttributes,
HTMLAttributes,
),
[
"iframe",
{
src: isInternalFileUrl(HTMLAttributes.src) ? sanitizeUrl(HTMLAttributes.src) : "",
width: HTMLAttributes.width || 800,
height: HTMLAttributes.height || 600,
},
],
];
},
addCommands() {
return {
setPdf:
(attrs: PdfAttributes) =>
({ commands }) => {
return commands.insertContent({
type: "pdf",
attrs,
});
},
};
},
addNodeView() {
this.editor.isInitialized = true;
return ReactNodeViewRenderer(this.options.view);
},
});
File diff suppressed because it is too large Load Diff
+6
View File
@@ -382,6 +382,12 @@ export function sanitizeUrl(url: string | undefined): string {
return sanitized === "about:blank" ? "" : sanitized;
}
export function isInternalFileUrl(url: string | undefined): boolean {
if (!url) return false;
const normalized = url.trim();
return normalized.startsWith("/api/files/") || normalized.startsWith("/files/");
}
const alphabet = "abcdefghijklmnopqrstuvwxyz";
export const generateNodeId = customAlphabet(alphabet, 12);
+5 -5
View File
@@ -1,7 +1,8 @@
import { ReactNodeViewRenderer } from "@tiptap/react";
import { Range, Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import { Range, Node, mergeAttributes } from "@tiptap/core";
import { ResizableNodeView } from "../resizable-nodeview";
import type { ResizableNodeViewDirection } from "../resizable-nodeview";
import { normalizeFileUrl } from "../media-utils";
import type { ResizableNodeViewDirection } from "@tiptap/core";
export type VideoResizeOptions = {
enabled: boolean;
@@ -328,12 +329,11 @@ export const TiptapVideo = Node.create<VideoOptions>({
// Show skeleton background while video loads from server
dom.style.pointerEvents = "none";
dom.style.background =
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))";
el.classList.add("media-pulse");
el.onloadedmetadata = () => {
dom.style.pointerEvents = "";
dom.style.background = "";
el.classList.remove("media-pulse");
};
return nodeView;
+150 -141
View File
@@ -34,9 +34,6 @@ overrides:
yaml@>=2.0.0 <2.8.3: 2.8.3
patchedDependencies:
'@tiptap/core':
hash: efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00
path: patches/@tiptap__core.patch
react-arborist@3.4.0:
hash: 419b3b02e24afe928cc006a006f6e906666aff19aa6fd7daaa788ccc2202678a
path: patches/react-arborist@3.4.0.patch
@@ -65,7 +62,7 @@ importers:
version: 3.4.4(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)
'@hocuspocus/transformer':
specifier: 3.4.4
version: 3.4.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)
version: 3.4.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)
'@joplin/turndown':
specifier: ^4.0.82
version: 4.0.82
@@ -77,85 +74,88 @@ importers:
version: 3.0.0
'@tiptap/core':
specifier: 3.20.4
version: 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
version: 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-audio':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-code-block':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-collaboration':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)
'@tiptap/extension-collaboration-caret':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))
'@tiptap/extension-color':
specifier: 3.20.4
version: 3.20.4(@tiptap/extension-text-style@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)))
version: 3.20.4(@tiptap/extension-text-style@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)))
'@tiptap/extension-document':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-heading':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-highlight':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-history':
specifier: 3.20.4
version: 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-image':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-link':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-placeholder':
specifier: 3.20.4
version: 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-subscript':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-superscript':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-table':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-text':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-text-align':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-text-style':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-typography':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-unique-id':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-youtube':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/html':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(happy-dom@20.8.4)
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(happy-dom@20.8.4)
'@tiptap/pm':
specifier: 3.20.4
version: 3.20.4
'@tiptap/react':
specifier: 3.20.4
version: 3.20.4(@floating-ui/dom@1.7.3)(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 3.20.4(@floating-ui/dom@1.7.3)(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tiptap/starter-kit':
specifier: 3.20.4
version: 3.20.4
'@tiptap/suggestion':
specifier: 3.20.4
version: 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
version: 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/y-tiptap':
specifier: 3.0.2
version: 3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)
@@ -4662,6 +4662,11 @@ packages:
peerDependencies:
'@tiptap/pm': ^3.20.4
'@tiptap/extension-audio@3.20.4':
resolution: {integrity: sha512-zX90pxpEYpV5jSrwtQw8Nmh2uK4WC+xwSG5MXVh4VLG8SnSE/vg/vCCqFiSHjXNfw68dctd6HJ0MJigwnuS0lw==}
peerDependencies:
'@tiptap/core': ^3.20.4
'@tiptap/extension-blockquote@3.20.4':
resolution: {integrity: sha512-9sskyyhYj2oKat//lyZVXCp9YrPt4oJAZnGHYWXS0xlskjsLElrfKKlM4vpbhGss3VrhQRoEGqWLnIaJYPF1zw==}
peerDependencies:
@@ -12707,9 +12712,9 @@ snapshots:
- bufferutil
- utf-8-validate
'@hocuspocus/transformer@3.4.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)':
'@hocuspocus/transformer@3.4.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/starter-kit': 3.20.4
y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)
@@ -15281,191 +15286,195 @@ snapshots:
'@tanstack/query-core': 5.90.17
react: 18.3.1
'@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)':
'@tiptap/core@3.20.4(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/pm': 3.20.4
'@tiptap/extension-blockquote@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-audio@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-bold@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-blockquote@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-bubble-menu@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extension-bold@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-bubble-menu@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@floating-ui/dom': 1.7.4
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
optional: true
'@tiptap/extension-bullet-list@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
'@tiptap/extension-bullet-list@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-code-block@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extension-code-block@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/extension-code@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-code@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-collaboration-caret@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))':
'@tiptap/extension-collaboration-caret@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/y-tiptap': 3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)
'@tiptap/extension-collaboration@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)':
'@tiptap/extension-collaboration@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/y-tiptap': 3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)
yjs: 13.6.30
'@tiptap/extension-color@3.20.4(@tiptap/extension-text-style@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)))':
'@tiptap/extension-color@3.20.4(@tiptap/extension-text-style@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4)))':
dependencies:
'@tiptap/extension-text-style': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-text-style': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-document@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-document@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-dropcursor@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
'@tiptap/extension-dropcursor@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-floating-menu@3.20.4(@floating-ui/dom@1.7.3)(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extension-floating-menu@3.20.4(@floating-ui/dom@1.7.3)(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@floating-ui/dom': 1.7.3
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
optional: true
'@tiptap/extension-gapcursor@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
'@tiptap/extension-gapcursor@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-hard-break@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-hard-break@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-heading@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-heading@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-highlight@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-highlight@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-history@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
'@tiptap/extension-history@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-horizontal-rule@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extension-horizontal-rule@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/extension-image@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-image@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-italic@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-italic@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-link@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extension-link@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
linkifyjs: 4.3.2
'@tiptap/extension-list-item@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
'@tiptap/extension-list-item@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list-keymap@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
'@tiptap/extension-list-keymap@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/extension-ordered-list@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
'@tiptap/extension-ordered-list@3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-paragraph@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-paragraph@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-placeholder@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
'@tiptap/extension-placeholder@3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-strike@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-strike@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-subscript@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extension-subscript@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/extension-superscript@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extension-superscript@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/extension-table@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extension-table@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/extension-text-align@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-text-align@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-text-style@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-text-style@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-text@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-text@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-typography@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-typography@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-underline@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-underline@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-unique-id@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extension-unique-id@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
uuid: 10.0.0
'@tiptap/extension-youtube@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))':
'@tiptap/extension-youtube@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/html@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(happy-dom@20.8.4)':
'@tiptap/html@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(happy-dom@20.8.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
happy-dom: 20.8.4
@@ -15490,9 +15499,9 @@ snapshots:
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
'@tiptap/react@3.20.4(@floating-ui/dom@1.7.3)(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@tiptap/react@3.20.4(@floating-ui/dom@1.7.3)(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
@@ -15502,41 +15511,41 @@ snapshots:
react-dom: 18.3.1(react@18.3.1)
use-sync-external-store: 1.6.0(react@18.3.1)
optionalDependencies:
'@tiptap/extension-bubble-menu': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-floating-menu': 3.20.4(@floating-ui/dom@1.7.3)(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-bubble-menu': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-floating-menu': 3.20.4(@floating-ui/dom@1.7.3)(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
transitivePeerDependencies:
- '@floating-ui/dom'
'@tiptap/starter-kit@3.20.4':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/extension-blockquote': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-bold': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-bullet-list': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-code': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-code-block': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-document': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-dropcursor': 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-gapcursor': 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-hard-break': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-heading': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-horizontal-rule': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-italic': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-link': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list-item': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-list-keymap': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-ordered-list': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-paragraph': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-strike': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-text': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extension-underline': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))
'@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/extension-blockquote': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-bold': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-bullet-list': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-code': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-code-block': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-document': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-dropcursor': 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-gapcursor': 3.20.4(@tiptap/extensions@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-hard-break': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-heading': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-horizontal-rule': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-italic': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-link': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/extension-list-item': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-list-keymap': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-ordered-list': 3.20.4(@tiptap/extension-list@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4))
'@tiptap/extension-paragraph': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-strike': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-text': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extension-underline': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))
'@tiptap/extensions': 3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/suggestion@3.20.4(@tiptap/core@3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
'@tiptap/suggestion@3.20.4(@tiptap/core@3.20.4(@tiptap/pm@3.20.4))(@tiptap/pm@3.20.4)':
dependencies:
'@tiptap/core': 3.20.4(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.20.4)
'@tiptap/core': 3.20.4(@tiptap/pm@3.20.4)
'@tiptap/pm': 3.20.4
'@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)':