From ef87210b3d6c6989cabec98216c266dcb631ea85 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:22:37 +0000 Subject: [PATCH] feat: editor UI refresh and enhancements (#1968) * feat: new image menu * switch to resizable side handles * use pixels * refactor excalidraw and drawio menu * support image resize undo * video resize * callout menu refresh * refresh table menus * fix color scheme * fix: patch @tiptap/core ResizableNodeView to prevent resize sticking after mouseup * feat: columns * notes callout * focus on first column * capture tab key in column * fix print * hide columns menu when some nodes are focused * fix print * fix columns * selective placeholder * fix blockquote * quote * fix callout in columns --- .../public/locales/en-US/translation.json | 10 + .../src/components/icons/icon-columns-4.tsx | 27 ++ .../src/components/icons/icon-columns-5.tsx | 28 ++ .../components/callout/callout-menu.tsx | 55 ++- .../components/callout/callout-view.tsx | 8 +- .../components/callout/callout.module.css | 20 +- .../components/columns/columns-menu.tsx | 267 ++++++++++++++ .../components/common/node-resize-handles.ts | 35 ++ .../components/common/node-resize.module.css | 65 ++++ .../components/common/toolbar-menu.module.css | 29 ++ .../editor/components/drawio/drawio-menu.tsx | 290 +++++++++++++-- .../editor/components/drawio/drawio-view.tsx | 106 ++---- .../components/excalidraw/excalidraw-menu.tsx | 343 +++++++++++++++--- .../components/excalidraw/excalidraw-view.tsx | 106 ++---- .../editor/components/image/image-menu.tsx | 108 +++++- .../components/image/image-resize-handles.ts | 7 + .../components/image/image-resize.module.css | 64 ++++ .../components/slash-menu/menu-items.ts | 56 +++ .../table/table-background-color.tsx | 2 +- .../components/table/table-cell-menu.tsx | 21 +- .../editor/components/table/table-menu.tsx | 33 +- .../components/table/table-text-alignment.tsx | 2 +- .../editor/components/video/video-menu.tsx | 83 +++-- .../features/editor/extensions/extensions.ts | 66 +++- .../src/features/editor/page-editor.tsx | 2 + .../src/features/editor/styles/columns.css | 116 ++++++ .../src/features/editor/styles/core.css | 25 +- .../src/features/editor/styles/index.css | 1 + apps/client/src/main.tsx | 6 +- .../src/collaboration/collaboration.util.ts | 4 + package.json | 3 +- packages/editor-ext/src/index.ts | 1 + packages/editor-ext/src/lib/callout/utils.ts | 19 +- packages/editor-ext/src/lib/columns/column.ts | 127 +++++++ .../editor-ext/src/lib/columns/columns.ts | 196 ++++++++++ packages/editor-ext/src/lib/columns/index.ts | 4 + packages/editor-ext/src/lib/drawio.ts | 223 +++++++++++- packages/editor-ext/src/lib/excalidraw.ts | 224 +++++++++++- packages/editor-ext/src/lib/image/image.ts | 241 +++++++++++- packages/editor-ext/src/lib/video/video.ts | 208 ++++++++++- patches/@tiptap__core.patch | 105 ++++++ pnpm-lock.yaml | 279 +++++++------- 42 files changed, 3082 insertions(+), 533 deletions(-) create mode 100644 apps/client/src/components/icons/icon-columns-4.tsx create mode 100644 apps/client/src/components/icons/icon-columns-5.tsx create mode 100644 apps/client/src/features/editor/components/columns/columns-menu.tsx create mode 100644 apps/client/src/features/editor/components/common/node-resize-handles.ts create mode 100644 apps/client/src/features/editor/components/common/node-resize.module.css create mode 100644 apps/client/src/features/editor/components/common/toolbar-menu.module.css create mode 100644 apps/client/src/features/editor/components/image/image-resize-handles.ts create mode 100644 apps/client/src/features/editor/components/image/image-resize.module.css create mode 100644 apps/client/src/features/editor/styles/columns.css create mode 100644 packages/editor-ext/src/lib/columns/column.ts create mode 100644 packages/editor-ext/src/lib/columns/columns.ts create mode 100644 packages/editor-ext/src/lib/columns/index.ts create mode 100644 patches/@tiptap__core.patch diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index e46dd2c8..25848e5d 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -274,6 +274,7 @@ "Add row below": "Add row below", "Delete table": "Delete table", "Info": "Info", + "Note": "Note", "Success": "Success", "Warning": "Warning", "Danger": "Danger", @@ -363,6 +364,15 @@ "Heading {{level}}": "Heading {{level}}", "Toggle title": "Toggle title", "Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands", + "Write...": "Write...", + "Column count": "Column count", + "{{count}} Columns": "{{count}} Columns", + "Equal columns": "Equal columns", + "Left sidebar": "Left sidebar", + "Right sidebar": "Right sidebar", + "Wide center": "Wide center", + "Left wide": "Left wide", + "Right wide": "Right wide", "Names do not match": "Names do not match", "Today, {{time}}": "Today, {{time}}", "Yesterday, {{time}}": "Yesterday, {{time}}", diff --git a/apps/client/src/components/icons/icon-columns-4.tsx b/apps/client/src/components/icons/icon-columns-4.tsx new file mode 100644 index 00000000..d2b4541b --- /dev/null +++ b/apps/client/src/components/icons/icon-columns-4.tsx @@ -0,0 +1,27 @@ +import { rem } from "@mantine/core"; + +type Props = { + size?: number | string; + stroke?: number; +}; + +export function IconColumns4({ size = 24, stroke = 2 }: Props) { + return ( + + + + + + + ); +} diff --git a/apps/client/src/components/icons/icon-columns-5.tsx b/apps/client/src/components/icons/icon-columns-5.tsx new file mode 100644 index 00000000..afa4773c --- /dev/null +++ b/apps/client/src/components/icons/icon-columns-5.tsx @@ -0,0 +1,28 @@ +import { rem } from "@mantine/core"; + +type Props = { + size?: number | string; + stroke?: number; +}; + +export function IconColumns5({ size = 24, stroke = 2 }: Props) { + return ( + + + + + + + + ); +} diff --git a/apps/client/src/features/editor/components/callout/callout-menu.tsx b/apps/client/src/features/editor/components/callout/callout-menu.tsx index e7ee2138..bdc71993 100644 --- a/apps/client/src/features/editor/components/callout/callout-menu.tsx +++ b/apps/client/src/features/editor/components/callout/callout-menu.tsx @@ -7,16 +7,19 @@ import { ShouldShowProps, } from "@/features/editor/components/table/types/types.ts"; import { ActionIcon, Tooltip } from "@mantine/core"; +import clsx from "clsx"; import { IconAlertTriangleFilled, IconCircleCheckFilled, IconCircleXFilled, IconInfoCircleFilled, IconMoodSmile, + IconNotes, } from "@tabler/icons-react"; -import { CalloutType } from "@docmost/editor-ext"; +import { CalloutType, isTextSelected } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; import EmojiPicker from "@/components/ui/emoji-picker.tsx"; +import classes from "../common/toolbar-menu.module.css"; export function CalloutMenu({ editor }: EditorMenuProps) { const { t } = useTranslation(); @@ -26,6 +29,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) { if (!state) { return false; } + if (isTextSelected(editor)) return false; return editor.isActive("callout"); }, @@ -42,6 +46,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) { return { isCallout: ctx.editor.isActive("callout"), isInfo: ctx.editor.isActive("callout", { type: "info" }), + isNote: ctx.editor.isActive("callout", { type: "note" }), isSuccess: ctx.editor.isActive("callout", { type: "success" }), isWarning: ctx.editor.isActive("callout", { type: "warning" }), isDanger: ctx.editor.isActive("callout", { type: "danger" }), @@ -126,15 +131,31 @@ export function CalloutMenu({ editor }: EditorMenuProps) { }} shouldShow={shouldShow} > - +
setCalloutType("info")} size="lg" aria-label={t("Info")} - variant={editorState?.isInfo ? "light" : "default"} + variant="subtle" + className={clsx({ [classes.active]: editorState?.isInfo })} > - + + + + + + setCalloutType("note")} + size="lg" + aria-label={t("Note")} + variant="subtle" + className={clsx({ [classes.active]: editorState?.isNote })} + > + @@ -143,9 +164,13 @@ export function CalloutMenu({ editor }: EditorMenuProps) { onClick={() => setCalloutType("success")} size="lg" aria-label={t("Success")} - variant={editorState?.isSuccess ? "light" : "default"} + variant="subtle" + className={clsx({ [classes.active]: editorState?.isSuccess })} > - + @@ -154,9 +179,13 @@ export function CalloutMenu({ editor }: EditorMenuProps) { onClick={() => setCalloutType("warning")} size="lg" aria-label={t("Warning")} - variant={editorState?.isWarning ? "light" : "default"} + variant="subtle" + className={clsx({ [classes.active]: editorState?.isWarning })} > - + @@ -165,9 +194,10 @@ export function CalloutMenu({ editor }: EditorMenuProps) { onClick={() => setCalloutType("danger")} size="lg" aria-label={t("Danger")} - variant={editorState?.isDanger ? "light" : "default"} + variant="subtle" + className={clsx({ [classes.active]: editorState?.isDanger })} > - + @@ -178,11 +208,10 @@ export function CalloutMenu({ editor }: EditorMenuProps) { icon={currentIcon || } actionIconProps={{ size: "lg", - variant: "default", - c: undefined, + variant: "subtle", }} /> - +
); } diff --git a/apps/client/src/features/editor/components/callout/callout-view.tsx b/apps/client/src/features/editor/components/callout/callout-view.tsx index 5583bd87..3cf5bb57 100644 --- a/apps/client/src/features/editor/components/callout/callout-view.tsx +++ b/apps/client/src/features/editor/components/callout/callout-view.tsx @@ -4,6 +4,7 @@ import { IconCircleCheckFilled, IconCircleXFilled, IconInfoCircleFilled, + IconNotes, } from "@tabler/icons-react"; import { Alert } from "@mantine/core"; import classes from "./callout.module.css"; @@ -22,6 +23,7 @@ export default function CalloutView(props: NodeViewProps) { icon={getCalloutIcon(type, icon)} p="xs" classNames={{ + root: classes.root, message: classes.message, icon: classes.icon, }} @@ -34,12 +36,14 @@ export default function CalloutView(props: NodeViewProps) { function getCalloutIcon(type: CalloutType, customIcon?: string) { if (customIcon && customIcon.trim() !== "") { - return {customIcon}; + return {customIcon}; } switch (type) { case "info": return ; + case "note": + return ; case "success": return ; case "warning": @@ -55,6 +59,8 @@ function getCalloutColor(type: CalloutType) { switch (type) { case "info": return "blue"; + case "note": + return "grape"; case "success": return "green"; case "warning": diff --git a/apps/client/src/features/editor/components/callout/callout.module.css b/apps/client/src/features/editor/components/callout/callout.module.css index 2839b426..8289f1a7 100644 --- a/apps/client/src/features/editor/components/callout/callout.module.css +++ b/apps/client/src/features/editor/components/callout/callout.module.css @@ -1,9 +1,13 @@ +.root { + overflow: visible; +} + .icon { font-size: 24px; line-height: 1; width: 20px; height: 20px; - margin-inline-end: var(--mantine-spacing-md); + margin-inline-end: var(--mantine-spacing-xs); margin-top: 4px; cursor: pointer; } @@ -11,18 +15,8 @@ .message { font-size: var(--mantine-font-size-md); color: var(--mantine-color-default-color); - - white-space: nowrap; + overflow: visible; + text-overflow: unset; word-break: break-word; overflow-wrap: break-word; } - -/* - @mixin where-light { - color: var(--mantine-color-default-color); - } - - @mixin where-dark { - color: var(--mantine-color-default-color); - } -*/ diff --git a/apps/client/src/features/editor/components/columns/columns-menu.tsx b/apps/client/src/features/editor/components/columns/columns-menu.tsx new file mode 100644 index 00000000..5c9e7607 --- /dev/null +++ b/apps/client/src/features/editor/components/columns/columns-menu.tsx @@ -0,0 +1,267 @@ +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; +import React, { useCallback, useState } from "react"; +import { Node as PMNode } from "prosemirror-model"; +import { + EditorMenuProps, + ShouldShowProps, +} from "@/features/editor/components/table/types/types.ts"; +import { ActionIcon, Tooltip, Popover, Button } from "@mantine/core"; +import clsx from "clsx"; +import { + IconChevronDown, + IconCheck, + IconColumns2, + IconColumns3, + IconLayoutSidebar, + IconLayoutSidebarRight, + IconLayoutAlignCenter, +} from "@tabler/icons-react"; +import { isTextSelected } from "@docmost/editor-ext"; +import type { WidthMode, ColumnsLayout } from "@docmost/editor-ext"; +import { useTranslation } from "react-i18next"; +import classes from "../common/toolbar-menu.module.css"; + +type LayoutPreset = { + layout: ColumnsLayout; + label: string; + icon: React.ElementType; +}; + +const twoColumnPresets: LayoutPreset[] = [ + { layout: "two_equal", label: "Equal columns", icon: IconColumns2 }, + { + layout: "two_left_sidebar", + label: "Left sidebar", + icon: IconLayoutSidebar, + }, + { + layout: "two_right_sidebar", + label: "Right sidebar", + icon: IconLayoutSidebarRight, + }, +]; + +const threeColumnPresets: LayoutPreset[] = [ + { layout: "three_equal", label: "Equal columns", icon: IconColumns3 }, + { + layout: "three_with_sidebars", + label: "Wide center", + icon: IconLayoutAlignCenter, + }, + { + layout: "three_left_wide", + label: "Left wide", + icon: IconLayoutSidebarRight, + }, + { layout: "three_right_wide", label: "Right wide", icon: IconLayoutSidebar + }, +]; + +function getPresetsForCount(count: number): LayoutPreset[] { + if (count === 2) return twoColumnPresets; + if (count === 3) return threeColumnPresets; + return []; +} + +export function ColumnsMenu({ editor }: EditorMenuProps) { + const { t } = useTranslation(); + const [isCountOpen, setIsCountOpen] = useState(false); + + const nodesWithMenus = [ + "callout", + "image", + "video", + "drawio", + "excalidraw", + "table", + ]; + + const shouldShow = useCallback( + ({ state }: ShouldShowProps) => { + if (!state) return false; + if (!editor.isActive("columns")) return false; + if (isTextSelected(editor)) return false; + if (nodesWithMenus.some((name) => editor.isActive(name))) return false; + + const parent = findParentNode( + (node: PMNode) => node.type.name === "columns", + )(state.selection); + if (!parent) return false; + + const dom = editor.view.nodeDOM(parent.pos) as HTMLElement; + if (!dom) return false; + + const rect = dom.getBoundingClientRect(); + return rect.bottom > 0 && rect.top < window.innerHeight; + }, + [editor], + ); + + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) return null; + + const { selection } = ctx.editor.state; + const parent = findParentNode( + (node: PMNode) => node.type.name === "columns", + )(selection); + + return { + columnCount: parent?.node.childCount || 2, + layout: (parent?.node.attrs.layout as ColumnsLayout) || "two_equal", + isNormal: ctx.editor.isActive("columns", { widthMode: "normal" }), + isWide: ctx.editor.isActive("columns", { widthMode: "wide" }), + }; + }, + }); + + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; + const { selection } = editor.state; + const predicate = (node: PMNode) => node.type.name === "columns"; + const parent = findParentNode(predicate)(selection); + + if (parent) { + const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; + const domRect = dom.getBoundingClientRect(); + + // Columns entirely out of viewport — return real rect so menu goes off-screen + if (domRect.bottom <= 0 || domRect.top >= window.innerHeight) { + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; + } + + // Clamp bottom so menu stays within viewport when columns extend below it + // 55px = 15px offset + ~40px menu height + const maxBottom = window.innerHeight - 55; + if (domRect.bottom > maxBottom) { + const clamped = new DOMRect( + domRect.x, + domRect.y, + domRect.width, + maxBottom - domRect.y, + ); + return { + getBoundingClientRect: () => clamped, + getClientRects: () => [clamped], + }; + } + + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; + } + + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; + }, [editor]); + + const setColumnCount = useCallback( + (count: number) => { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .setColumnCount(count) + .run(); + setIsCountOpen(false); + }, + [editor], + ); + + const setLayout = useCallback( + (layout: ColumnsLayout) => { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .setColumnsLayout(layout) + .run(); + }, + [editor], + ); + + const columnCount = editorState?.columnCount || 2; + const currentLayout = editorState?.layout || "two_equal"; + const presets = getPresetsForCount(columnCount); + + return ( + +
+ + + + + + + {[2, 3, 4, 5].map((n) => ( + + ))} + + + + + {presets.length > 0 &&
} + + {presets.map((preset) => ( + + setLayout(preset.layout)} + size="lg" + aria-label={t(preset.label)} + variant="subtle" + className={clsx({ + [classes.active]: currentLayout === preset.layout, + })} + > + + + + ))} +
+ + ); +} + +export default ColumnsMenu; diff --git a/apps/client/src/features/editor/components/common/node-resize-handles.ts b/apps/client/src/features/editor/components/common/node-resize-handles.ts new file mode 100644 index 00000000..0785845d --- /dev/null +++ b/apps/client/src/features/editor/components/common/node-resize-handles.ts @@ -0,0 +1,35 @@ +import type { ResizableNodeViewDirection } from "@tiptap/core"; +import classes from "./node-resize.module.css"; + +export function createResizeHandle( + direction: ResizableNodeViewDirection, +): HTMLElement { + const handle = document.createElement("div"); + handle.dataset.resizeHandle = direction; + handle.style.position = "absolute"; + handle.className = classes.handle; + + if (direction === "left") { + handle.style.left = "-8px"; + handle.style.top = "0"; + handle.style.bottom = "0"; + } else if (direction === "right") { + handle.style.right = "-8px"; + handle.style.top = "0"; + handle.style.bottom = "0"; + } + + const bar = document.createElement("div"); + bar.className = classes.handleBar; + handle.appendChild(bar); + + return handle; +} + +export function buildResizeClasses(nodeClass: string) { + return { + container: `${classes.container} ${nodeClass}`, + wrapper: classes.wrapper, + resizing: classes.resizing, + }; +} diff --git a/apps/client/src/features/editor/components/common/node-resize.module.css b/apps/client/src/features/editor/components/common/node-resize.module.css new file mode 100644 index 00000000..7010e324 --- /dev/null +++ b/apps/client/src/features/editor/components/common/node-resize.module.css @@ -0,0 +1,65 @@ +.container { + display: flex; +} + +.wrapper { + position: relative; + border-radius: 8px; + overflow: visible; + max-width: 100%; +} + +.wrapper img, +.wrapper video { + height: auto !important; +} + +.resizing { + user-select: none; +} + +.handle { + position: absolute; + top: 0; + bottom: 0; + width: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: ew-resize; + opacity: 0; + transition: opacity 0.2s ease; + z-index: 2; +} + +.handle[data-resize-handle="left"] { + left: -8px; +} + +.handle[data-resize-handle="right"] { + right: -8px; +} + +.wrapper:hover .handle { + opacity: 1; +} + +.resizing .handle { + opacity: 1; +} + +.handleBar { + width: 4px; + height: 48px; + border-radius: 4px; + transition: background-color 0.15s ease; + background-color: light-dark(var(--mantine-color-blue-4), var(--mantine-color-blue-5)); +} + +.handle:hover .handleBar { + background-color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4)); +} + +.resizing .handleBar { + background-color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4)); +} diff --git a/apps/client/src/features/editor/components/common/toolbar-menu.module.css b/apps/client/src/features/editor/components/common/toolbar-menu.module.css new file mode 100644 index 00000000..7fd91f56 --- /dev/null +++ b/apps/client/src/features/editor/components/common/toolbar-menu.module.css @@ -0,0 +1,29 @@ +.toolbar { + display: flex; + align-items: center; + gap: 2px; + padding: 3px; + border-radius: 8px; + border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); + background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); + box-shadow: 0 2px 12px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.35)); +} + +.toolbar :global(.mantine-ActionIcon-root) { + --ai-color: light-dark(var(--mantine-color-dark-7), var(--mantine-color-gray-4)) !important; + --ai-hover: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5)) !important; +} + +.toolbar .active { + --ai-color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-3)) !important; + --ai-hover: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-5)) !important; + background-color: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-5)); +} + +.divider { + width: 1px; + height: 16px; + align-self: center; + margin: 0 2px; + background-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-3)); +} diff --git a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx index 937b8e7d..547ea375 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx @@ -1,24 +1,41 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; -import { useCallback } from "react"; +import { useCallback, useRef, useState } from "react"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, ShouldShowProps, } from "@/features/editor/components/table/types/types.ts"; -import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx"; +import { ActionIcon, Modal, Tooltip, useComputedColorScheme } from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import clsx from "clsx"; +import { + IconLayoutAlignCenter, + IconLayoutAlignLeft, + IconLayoutAlignRight, + IconDownload, + IconEdit, + IconTrash, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { getDrawioUrl, getFileUrl } from "@/lib/config.ts"; +import { uploadFile } from "@/features/page/services/page-service.ts"; +import { + DrawIoEmbed, + DrawIoEmbedRef, + EventExit, + EventSave, +} from "react-drawio"; +import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils"; +import { IAttachment } from "@/features/attachments/types/attachment.types"; +import classes from "../common/toolbar-menu.module.css"; export function DrawioMenu({ editor }: EditorMenuProps) { - const shouldShow = useCallback( - ({ state }: ShouldShowProps) => { - if (!state) { - return false; - } - - return editor.isActive("drawio") && editor.getAttributes("drawio")?.src; - }, - [editor], - ); + const { t } = useTranslation(); + const [opened, { open, close }] = useDisclosure(false); + const [initialXML, setInitialXML] = useState(""); + const drawioRef = useRef(null); + const computedColorScheme = useComputedColorScheme(); const editorState = useEditorState({ editor, @@ -30,11 +47,26 @@ export function DrawioMenu({ editor }: EditorMenuProps) { const drawioAttr = ctx.editor.getAttributes("drawio"); return { isDrawio: ctx.editor.isActive("drawio"), - width: drawioAttr?.width ? parseInt(drawioAttr.width) : null, + isAlignLeft: ctx.editor.isActive("drawio", { align: "left" }), + isAlignCenter: ctx.editor.isActive("drawio", { align: "center" }), + isAlignRight: ctx.editor.isActive("drawio", { align: "right" }), + src: drawioAttr?.src || null, + attachmentId: drawioAttr?.attachmentId || null, }; }, }); + const shouldShow = useCallback( + ({ state }: ShouldShowProps) => { + if (!state) { + return false; + } + + return editor.isActive("drawio") && editor.getAttributes("drawio")?.src; + }, + [editor], + ); + const getReferencedVirtualElement = useCallback(() => { if (!editor) return; const { selection } = editor.state; @@ -57,38 +89,218 @@ export function DrawioMenu({ editor }: EditorMenuProps) { }; }, [editor]); - const onWidthChange = useCallback( - (value: number) => { - editor.commands.updateAttributes("drawio", { width: `${value}%` }); + const alignLeft = useCallback(() => { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .setDrawioAlign("left") + .run(); + }, [editor]); + + const alignCenter = useCallback(() => { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .setDrawioAlign("center") + .run(); + }, [editor]); + + const alignRight = useCallback(() => { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .setDrawioAlign("right") + .run(); + }, [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]); + + const handleOpen = useCallback(async () => { + if (!editorState?.src) return; + + try { + const url = getFileUrl(editorState.src); + const request = await fetch(url, { + credentials: "include", + cache: "no-store", + }); + const blob = await request.blob(); + + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => { + const base64data = (reader.result || "") as string; + setInitialXML(base64data); + }; + } catch (err) { + console.error(err); + } finally { + open(); + } + }, [editorState?.src, open]); + + const handleSave = useCallback( + async (data: EventSave) => { + const svgString = decodeBase64ToSvgString(data.xml); + const fileName = "diagram.drawio.svg"; + const drawioSVGFile = await svgStringToFile(svgString, fileName); + + // @ts-ignore + const pageId = editor.storage?.pageId; + const attachmentId = editorState?.attachmentId; + + let attachment: IAttachment = null; + if (attachmentId) { + attachment = await uploadFile(drawioSVGFile, pageId, attachmentId); + } else { + attachment = await uploadFile(drawioSVGFile, pageId); + } + + editor.commands.updateAttributes("drawio", { + src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`, + title: attachment.fileName, + size: attachment.fileSize, + attachmentId: attachment.id, + }); + + close(); }, - [editor], + [editor, editorState?.attachmentId, close], ); return ( - -
+ - {editorState?.width && ( - - )} -
-
+
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ { + if (data.parentEvent !== "save") { + return; + } + handleSave(data); + }} + onClose={(data: EventExit) => { + if (data.parentEvent) { + return; + } + close(); + }} + /> +
+
+
+
+ ); } diff --git a/apps/client/src/features/editor/components/drawio/drawio-view.tsx b/apps/client/src/features/editor/components/drawio/drawio-view.tsx index b51e8936..0b1580ec 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-view.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-view.tsx @@ -2,7 +2,6 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { ActionIcon, Card, - Image, Modal, Text, useComputedColorScheme, @@ -10,7 +9,7 @@ import { import { useRef, useState } from "react"; import { uploadFile } from "@/features/page/services/page-service.ts"; import { useDisclosure } from "@mantine/hooks"; -import { getDrawioUrl, getFileUrl } from "@/lib/config.ts"; +import { getDrawioUrl } from "@/lib/config.ts"; import { DrawIoEmbed, DrawIoEmbedRef, @@ -26,7 +25,7 @@ import { useTranslation } from "react-i18next"; export default function DrawioView(props: NodeViewProps) { const { t } = useTranslation(); const { node, updateAttributes, editor, selected } = props; - const { src, title, width, attachmentId } = node.attrs; + const { attachmentId } = node.attrs; const drawioRef = useRef(null); const [initialXML, setInitialXML] = useState(""); const [opened, { open, close }] = useDisclosure(false); @@ -36,33 +35,11 @@ export default function DrawioView(props: NodeViewProps) { if (!editor.isEditable) { return; } - - try { - if (src) { - const url = getFileUrl(src); - const request = await fetch(url, { - credentials: "include", - cache: "no-store", - }); - const blob = await request.blob(); - - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = () => { - const base64data = (reader.result || "") as string; - setInitialXML(base64data); - }; - } - } catch (err) { - console.error(err); - } finally { - open(); - } + open(); }; const handleSave = async (data: EventSave) => { const svgString = decodeBase64ToSvgString(data.xml); - const fileName = "diagram.drawio.svg"; const drawioSVGFile = await svgStringToFile(svgString, fileName); @@ -70,7 +47,6 @@ export default function DrawioView(props: NodeViewProps) { const pageId = editor.storage?.pageId; let attachment: IAttachment = null; - if (attachmentId) { attachment = await uploadFile(drawioSVGFile, pageId, attachmentId); } else { @@ -106,14 +82,12 @@ export default function DrawioView(props: NodeViewProps) { noSaveBtn: true, }} onSave={(data: EventSave) => { - // If the save is triggered by another event, then do nothing if (data.parentEvent !== "save") { return; } handleSave(data); }} onClose={(data: EventExit) => { - // If the exit is triggered by another event, then do nothing if (data.parentEvent) { return; } @@ -125,62 +99,28 @@ export default function DrawioView(props: NodeViewProps) { - {src ? ( -
- e.detail === 2 && handleOpen()} - radius="md" - fit="contain" - w={width} - src={getFileUrl(src)} - alt={title} - className={clsx( - selected ? "ProseMirror-selectednode" : "", - "alignCenter", - )} - /> + e.detail === 2 && handleOpen()} + p="xs" + style={{ + display: "flex", + justifyContent: "center", + alignItems: "center", + }} + withBorder + className={clsx(selected ? "ProseMirror-selectednode" : "")} + > +
+ + + - {selected && editor.isEditable && ( - - - - )} + + {t("Double-click to edit Draw.io diagram")} +
- ) : ( - e.detail === 2 && handleOpen()} - p="xs" - style={{ - display: "flex", - justifyContent: "center", - alignItems: "center", - }} - withBorder - className={clsx(selected ? "ProseMirror-selectednode" : "")} - > -
- - - - - - {t("Double-click to edit Draw.io diagram")} - -
-
- )} +
); } diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx index 06e79515..766a357c 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx @@ -1,26 +1,57 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; -import { useCallback } from "react"; +import { lazy, Suspense, useCallback, useState } from "react"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, ShouldShowProps, } from "@/features/editor/components/table/types/types.ts"; -import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx"; +import { + ActionIcon, + Button, + Group, + Tooltip, + useComputedColorScheme, +} from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import clsx from "clsx"; +import { + IconLayoutAlignCenter, + IconLayoutAlignLeft, + IconLayoutAlignRight, + IconDownload, + IconEdit, + IconTrash, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { getFileUrl } from "@/lib/config.ts"; +import { uploadFile } from "@/features/page/services/page-service.ts"; +import { svgStringToFile } from "@/lib"; +import "@excalidraw/excalidraw/index.css"; +import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types"; +import { IAttachment } from "@/features/attachments/types/attachment.types"; +import ReactClearModal from "react-clear-modal"; +import { useHandleLibrary } from "@excalidraw/excalidraw"; +import { localStorageLibraryAdapter } from "@/features/editor/components/excalidraw/excalidraw-utils.ts"; +import classes from "../common/toolbar-menu.module.css"; + +const ExcalidrawComponent = lazy(() => + import("@excalidraw/excalidraw").then((module) => ({ + default: module.Excalidraw, + })), +); export function ExcalidrawMenu({ editor }: EditorMenuProps) { - const shouldShow = useCallback( - ({ state }: ShouldShowProps) => { - if (!state) { - return false; - } - - return ( - editor.isActive("excalidraw") && editor.getAttributes("excalidraw")?.src - ); - }, - [editor], - ); + const { t } = useTranslation(); + const [opened, { open, close }] = useDisclosure(false); + const [excalidrawAPI, setExcalidrawAPI] = + useState(null); + useHandleLibrary({ + excalidrawAPI, + adapter: localStorageLibraryAdapter, + }); + const [excalidrawData, setExcalidrawData] = useState(null); + const computedColorScheme = useComputedColorScheme(); const editorState = useEditorState({ editor, @@ -32,11 +63,29 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { const excalidrawAttr = ctx.editor.getAttributes("excalidraw"); return { isExcalidraw: ctx.editor.isActive("excalidraw"), - width: excalidrawAttr?.width ? parseInt(excalidrawAttr.width) : null, + isAlignLeft: ctx.editor.isActive("excalidraw", { align: "left" }), + isAlignCenter: ctx.editor.isActive("excalidraw", { align: "center" }), + isAlignRight: ctx.editor.isActive("excalidraw", { align: "right" }), + src: excalidrawAttr?.src || null, + attachmentId: excalidrawAttr?.attachmentId || null, }; }, }); + const shouldShow = useCallback( + ({ state }: ShouldShowProps) => { + if (!state) { + return false; + } + + return ( + editor.isActive("excalidraw") && + editor.getAttributes("excalidraw")?.src + ); + }, + [editor], + ); + const getReferencedVirtualElement = useCallback(() => { if (!editor) return; const { selection } = editor.state; @@ -59,38 +108,248 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { }; }, [editor]); - const onWidthChange = useCallback( - (value: number) => { - editor.commands.updateAttributes("excalidraw", { width: `${value}%` }); - }, - [editor], - ); + const alignLeft = useCallback(() => { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .setExcalidrawAlign("left") + .run(); + }, [editor]); + + const alignCenter = useCallback(() => { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .setExcalidrawAlign("center") + .run(); + }, [editor]); + + const alignRight = useCallback(() => { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .setExcalidrawAlign("right") + .run(); + }, [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]); + + const handleOpen = useCallback(async () => { + if (!editorState?.src) return; + + try { + const url = getFileUrl(editorState.src); + const request = await fetch(url, { + credentials: "include", + cache: "no-store", + }); + + const { loadFromBlob } = await import("@excalidraw/excalidraw"); + const data = await loadFromBlob(await request.blob(), null, null); + setExcalidrawData(data); + } catch (err) { + console.error(err); + } finally { + open(); + } + }, [editorState?.src, open]); + + const handleSave = useCallback(async () => { + if (!excalidrawAPI) { + return; + } + + const { exportToSvg } = await import("@excalidraw/excalidraw"); + + const svg = await exportToSvg({ + elements: excalidrawAPI?.getSceneElements(), + appState: { + exportEmbedScene: true, + exportWithDarkMode: false, + }, + files: excalidrawAPI?.getFiles(), + }); + + const serializer = new XMLSerializer(); + let svgString = serializer.serializeToString(svg); + + svgString = svgString.replace( + /https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g, + "https://unpkg.com/@excalidraw/excalidraw@latest", + ); + + const fileName = "diagram.excalidraw.svg"; + const excalidrawSvgFile = await svgStringToFile(svgString, fileName); + + // @ts-ignore + const pageId = editor.storage?.pageId; + const attachmentId = editorState?.attachmentId; + + let attachment: IAttachment = null; + if (attachmentId) { + attachment = await uploadFile(excalidrawSvgFile, pageId, attachmentId); + } else { + attachment = await uploadFile(excalidrawSvgFile, pageId); + } + + editor.commands.updateAttributes("excalidraw", { + src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`, + title: attachment.fileName, + size: attachment.fileSize, + attachmentId: attachment.id, + }); + + close(); + }, [editor, excalidrawAPI, editorState?.attachmentId, close]); return ( - -
+ +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + - {editorState?.width && ( - - )} -
-
+ + + + +
+ + setExcalidrawAPI(api)} + initialData={{ + ...excalidrawData, + scrollToContent: true, + }} + theme={computedColorScheme} + /> + +
+ + ); } diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx index 86c9665e..51ff5b06 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx @@ -4,28 +4,24 @@ import { Button, Card, Group, - Image, Text, useComputedColorScheme, } from "@mantine/core"; -import { useState } from "react"; +import { lazy, Suspense, useState } from "react"; import { uploadFile } from "@/features/page/services/page-service.ts"; import { svgStringToFile } from "@/lib"; import { useDisclosure } from "@mantine/hooks"; -import { getFileUrl } from "@/lib/config.ts"; import "@excalidraw/excalidraw/index.css"; import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types"; import { IAttachment } from "@/features/attachments/types/attachment.types"; import ReactClearModal from "react-clear-modal"; import clsx from "clsx"; import { IconEdit } from "@tabler/icons-react"; -import { lazy } from "react"; -import { Suspense } from "react"; import { useTranslation } from "react-i18next"; import { useHandleLibrary } from "@excalidraw/excalidraw"; import { localStorageLibraryAdapter } from "@/features/editor/components/excalidraw/excalidraw-utils.ts"; -const Excalidraw = lazy(() => +const ExcalidrawComponent = lazy(() => import("@excalidraw/excalidraw").then((module) => ({ default: module.Excalidraw, })), @@ -34,7 +30,7 @@ const Excalidraw = lazy(() => export default function ExcalidrawView(props: NodeViewProps) { const { t } = useTranslation(); const { node, updateAttributes, editor, selected } = props; - const { src, title, width, attachmentId } = node.attrs; + const { attachmentId } = node.attrs; const [excalidrawAPI, setExcalidrawAPI] = useState(null); @@ -50,25 +46,7 @@ export default function ExcalidrawView(props: NodeViewProps) { if (!editor.isEditable) { return; } - - try { - if (src) { - const url = getFileUrl(src); - const request = await fetch(url, { - credentials: "include", - cache: "no-store", - }); - - const { loadFromBlob } = await import("@excalidraw/excalidraw"); - - const data = await loadFromBlob(await request.blob(), null, null); - setExcalidrawData(data); - } - } catch (err) { - console.error(err); - } finally { - open(); - } + open(); }; const handleSave = async () => { @@ -151,7 +129,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
- setExcalidrawAPI(api)} initialData={{ ...excalidrawData, @@ -163,62 +141,28 @@ export default function ExcalidrawView(props: NodeViewProps) {
- {src ? ( -
- e.detail === 2 && handleOpen()} - radius="md" - fit="contain" - w={width} - src={getFileUrl(src)} - alt={title} - className={clsx( - selected ? "ProseMirror-selectednode" : "", - "alignCenter", - )} - /> + e.detail === 2 && handleOpen()} + p="xs" + style={{ + display: "flex", + justifyContent: "center", + alignItems: "center", + }} + withBorder + className={clsx(selected ? "ProseMirror-selectednode" : "")} + > +
+ + + - {selected && editor.isEditable && ( - - - - )} + + {t("Double-click to edit Excalidraw diagram")} +
- ) : ( - e.detail === 2 && handleOpen()} - p="xs" - style={{ - display: "flex", - justifyContent: "center", - alignItems: "center", - }} - withBorder - className={clsx(selected ? "ProseMirror-selectednode" : "")} - > -
- - - - - - {t("Double-click to edit Excalidraw diagram")} - -
-
- )} +
); } diff --git a/apps/client/src/features/editor/components/image/image-menu.tsx b/apps/client/src/features/editor/components/image/image-menu.tsx index a1699f93..abef7603 100644 --- a/apps/client/src/features/editor/components/image/image-menu.tsx +++ b/apps/client/src/features/editor/components/image/image-menu.tsx @@ -1,22 +1,29 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; -import React, { useCallback } from "react"; +import React, { useCallback, useRef } from "react"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, ShouldShowProps, } from "@/features/editor/components/table/types/types.ts"; import { ActionIcon, Tooltip } from "@mantine/core"; +import clsx from "clsx"; import { IconLayoutAlignCenter, IconLayoutAlignLeft, IconLayoutAlignRight, + IconDownload, + IconRefresh, + IconTrash, } from "@tabler/icons-react"; -import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx"; import { useTranslation } from "react-i18next"; +import { getFileUrl } from "@/lib/config.ts"; +import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx"; +import classes from "../common/toolbar-menu.module.css"; export function ImageMenu({ editor }: EditorMenuProps) { const { t } = useTranslation(); + const fileInputRef = useRef(null); const editorState = useEditorState({ editor, @@ -32,7 +39,7 @@ export function ImageMenu({ editor }: EditorMenuProps) { isAlignLeft: ctx.editor.isActive("image", { align: "left" }), isAlignCenter: ctx.editor.isActive("image", { align: "center" }), isAlignRight: ctx.editor.isActive("image", { align: "right" }), - width: imageAttrs?.width ? parseInt(imageAttrs.width) : null, + src: imageAttrs?.src || null, }; }, }); @@ -94,17 +101,40 @@ export function ImageMenu({ editor }: EditorMenuProps) { .run(); }, [editor]); - const onWidthChange = useCallback( - (value: number) => { - editor - .chain() - .focus(undefined, { scrollIntoView: false }) - .setImageWidth(value) - .run(); + 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 handleReplace = useCallback(() => { + fileInputRef.current?.click(); + }, []); + + const handleFileChange = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // @ts-ignore + const pageId = editor.storage?.pageId; + if (pageId) { + const pos = editor.state.selection.from; + uploadImageAction(file, editor, pos, pageId); + } + // Reset so the same file can be selected again + e.target.value = ""; }, [editor], ); + const handleDelete = useCallback(() => { + editor.commands.deleteSelection(); + }, [editor]); + return ( - +
@@ -135,7 +166,8 @@ export function ImageMenu({ editor }: EditorMenuProps) { onClick={alignImageCenter} size="lg" aria-label={t("Align center")} - variant={editorState?.isAlignCenter ? "light" : "default"} + variant="subtle" + className={clsx({ [classes.active]: editorState?.isAlignCenter })} > @@ -146,16 +178,56 @@ export function ImageMenu({ editor }: EditorMenuProps) { onClick={alignImageRight} size="lg" aria-label={t("Align right")} - variant={editorState?.isAlignRight ? "light" : "default"} + variant="subtle" + className={clsx({ [classes.active]: editorState?.isAlignRight })} > - - {editorState?.width && ( - - )} +
+ + + + + + + + + + + + + + + + + + +
+ + ); } diff --git a/apps/client/src/features/editor/components/image/image-resize-handles.ts b/apps/client/src/features/editor/components/image/image-resize-handles.ts new file mode 100644 index 00000000..ec941497 --- /dev/null +++ b/apps/client/src/features/editor/components/image/image-resize-handles.ts @@ -0,0 +1,7 @@ +import { + createResizeHandle, + buildResizeClasses, +} from "../common/node-resize-handles"; + +export const createImageHandle = createResizeHandle; +export const imageResizeClasses = buildResizeClasses("node-image"); diff --git a/apps/client/src/features/editor/components/image/image-resize.module.css b/apps/client/src/features/editor/components/image/image-resize.module.css new file mode 100644 index 00000000..24414171 --- /dev/null +++ b/apps/client/src/features/editor/components/image/image-resize.module.css @@ -0,0 +1,64 @@ +.container { + display: flex; +} + +.wrapper { + position: relative; + border-radius: 8px; + overflow: visible; + max-width: 100%; +} + +.wrapper img { + height: auto !important; +} + +.resizing { + user-select: none; +} + +.handle { + position: absolute; + top: 0; + bottom: 0; + width: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: ew-resize; + opacity: 0; + transition: opacity 0.2s ease; + z-index: 2; +} + +.handle[data-resize-handle="left"] { + left: -8px; +} + +.handle[data-resize-handle="right"] { + right: -8px; +} + +.wrapper:hover .handle { + opacity: 1; +} + +.resizing .handle { + opacity: 1; +} + +.handleBar { + width: 4px; + height: 48px; + border-radius: 4px; + transition: background-color 0.15s ease; + background-color: light-dark(var(--mantine-color-blue-4), var(--mantine-color-blue-5)); +} + +.handle:hover .handleBar { + background-color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4)); +} + +.resizing .handleBar { + background-color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4)); +} diff --git a/apps/client/src/features/editor/components/slash-menu/menu-items.ts b/apps/client/src/features/editor/components/slash-menu/menu-items.ts index 27793f62..03ba7b80 100644 --- a/apps/client/src/features/editor/components/slash-menu/menu-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/menu-items.ts @@ -20,6 +20,8 @@ import { IconCalendar, IconAppWindow, IconSitemap, + IconColumns3, + IconColumns2, } from "@tabler/icons-react"; import { CommandProps, @@ -31,6 +33,8 @@ import { uploadAttachmentAction } from "@/features/editor/components/attachment/ import IconExcalidraw from "@/components/icons/icon-excalidraw"; import IconMermaid from "@/components/icons/icon-mermaid"; import IconDrawio from "@/components/icons/icon-drawio"; +import { IconColumns4 } from "@/components/icons/icon-columns-4"; +import { IconColumns5 } from "@/components/icons/icon-columns-5"; import { AirtableIcon, FigmaIcon, @@ -390,6 +394,58 @@ const CommandGroups: SlashMenuGroupedItemsType = { editor.chain().focus().deleteRange(range).insertSubpages().run(); }, }, + { + title: "2 Columns", + description: "Split content into two columns.", + searchTerms: ["columns", "layout", "split", "side"], + icon: IconColumns2, + command: ({ editor, range }: CommandProps) => + editor + .chain() + .focus() + .deleteRange(range) + .insertColumns({ layout: "two_equal" }) + .run(), + }, + { + title: "3 Columns", + description: "Split content into three columns.", + searchTerms: ["columns", "layout", "split", "triple"], + icon: IconColumns3, + command: ({ editor, range }: CommandProps) => + editor + .chain() + .focus() + .deleteRange(range) + .insertColumns({ layout: "three_equal" }) + .run(), + }, + { + title: "4 Columns", + description: "Split content into four columns.", + searchTerms: ["columns", "layout", "split"], + icon: IconColumns4, + command: ({ editor, range }: CommandProps) => + editor + .chain() + .focus() + .deleteRange(range) + .insertColumns({ layout: "four_equal" }) + .run(), + }, + { + title: "5 Columns", + description: "Split content into five columns.", + searchTerms: ["columns", "layout", "split"], + icon: IconColumns5, + command: ({ editor, range }: CommandProps) => + editor + .chain() + .focus() + .deleteRange(range) + .insertColumns({ layout: "five_equal" }) + .run(), + }, { title: "Iframe embed", description: "Embed any Iframe", diff --git a/apps/client/src/features/editor/components/table/table-background-color.tsx b/apps/client/src/features/editor/components/table/table-background-color.tsx index 7508d4fe..3e4ce616 100644 --- a/apps/client/src/features/editor/components/table/table-background-color.tsx +++ b/apps/client/src/features/editor/components/table/table-background-color.tsx @@ -95,7 +95,7 @@ export const TableBackgroundColor: FC = ({ setOpened(!opened)} diff --git a/apps/client/src/features/editor/components/table/table-cell-menu.tsx b/apps/client/src/features/editor/components/table/table-cell-menu.tsx index 8af896b3..34ac6156 100644 --- a/apps/client/src/features/editor/components/table/table-cell-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-cell-menu.tsx @@ -16,6 +16,7 @@ import { useTranslation } from "react-i18next"; import { TableBackgroundColor } from "./table-background-color"; import { TableTextAlignment } from "./table-text-alignment"; import { BubbleMenu } from "@tiptap/react/menus"; +import classes from "../common/toolbar-menu.module.css"; export const TableCellMenu = React.memo( ({ editor, appendTo }: EditorMenuProps): JSX.Element => { @@ -69,14 +70,16 @@ export const TableCellMenu = React.memo( }} shouldShow={shouldShow} > - +
+
+ @@ -87,7 +90,7 @@ export const TableCellMenu = React.memo( @@ -95,10 +98,12 @@ export const TableCellMenu = React.memo( +
+ @@ -109,7 +114,7 @@ export const TableCellMenu = React.memo( @@ -117,17 +122,19 @@ export const TableCellMenu = React.memo( +
+ - +
); } diff --git a/apps/client/src/features/editor/components/table/table-menu.tsx b/apps/client/src/features/editor/components/table/table-menu.tsx index e54a06af..66fe4d7b 100644 --- a/apps/client/src/features/editor/components/table/table-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-menu.tsx @@ -18,8 +18,9 @@ import { IconTrashX, } from "@tabler/icons-react"; import { BubbleMenu } from "@tiptap/react/menus"; -import { isCellSelection } from "@docmost/editor-ext"; +import { isCellSelection, isTextSelected } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; +import classes from "../common/toolbar-menu.module.css"; export const TableMenu = React.memo( ({ editor }: EditorMenuProps): JSX.Element => { @@ -30,6 +31,7 @@ export const TableMenu = React.memo( return false; } + if (isTextSelected(editor)) return false; return editor.isActive("table") && !isCellSelection(state.selection); }, [editor] @@ -118,11 +120,11 @@ export const TableMenu = React.memo( }} shouldShow={shouldShow} > - +
@@ -133,7 +135,7 @@ export const TableMenu = React.memo( @@ -144,7 +146,7 @@ export const TableMenu = React.memo( @@ -152,10 +154,12 @@ export const TableMenu = React.memo( +
+ @@ -166,7 +170,7 @@ export const TableMenu = React.memo( @@ -177,7 +181,7 @@ export const TableMenu = React.memo( @@ -185,10 +189,12 @@ export const TableMenu = React.memo( +
+ @@ -199,7 +205,7 @@ export const TableMenu = React.memo( @@ -207,18 +213,19 @@ export const TableMenu = React.memo( +
+ - +
); } diff --git a/apps/client/src/features/editor/components/table/table-text-alignment.tsx b/apps/client/src/features/editor/components/table/table-text-alignment.tsx index 8b966290..4d4646cf 100644 --- a/apps/client/src/features/editor/components/table/table-text-alignment.tsx +++ b/apps/client/src/features/editor/components/table/table-text-alignment.tsx @@ -88,7 +88,7 @@ export const TableTextAlignment: FC = ({ editor }) => { setOpened(!opened)} diff --git a/apps/client/src/features/editor/components/video/video-menu.tsx b/apps/client/src/features/editor/components/video/video-menu.tsx index dfece398..398f758f 100644 --- a/apps/client/src/features/editor/components/video/video-menu.tsx +++ b/apps/client/src/features/editor/components/video/video-menu.tsx @@ -1,19 +1,23 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; -import React, { useCallback } from "react"; +import { useCallback } from "react"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, ShouldShowProps, } from "@/features/editor/components/table/types/types.ts"; import { ActionIcon, Tooltip } from "@mantine/core"; +import clsx from "clsx"; import { IconLayoutAlignCenter, IconLayoutAlignLeft, IconLayoutAlignRight, + IconDownload, + IconTrash, } from "@tabler/icons-react"; -import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx"; import { useTranslation } from "react-i18next"; +import { getFileUrl } from "@/lib/config.ts"; +import classes from "../common/toolbar-menu.module.css"; export function VideoMenu({ editor }: EditorMenuProps) { const { t } = useTranslation(); @@ -32,7 +36,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { isAlignLeft: ctx.editor.isActive("video", { align: "left" }), isAlignCenter: ctx.editor.isActive("video", { align: "center" }), isAlignRight: ctx.editor.isActive("video", { align: "right" }), - width: videoAttrs?.width ? parseInt(videoAttrs.width) : null, + src: videoAttrs?.src || null, }; }, }); @@ -70,7 +74,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { }; }, [editor]); - const alignVideoLeft = useCallback(() => { + const alignLeft = useCallback(() => { editor .chain() .focus(undefined, { scrollIntoView: false }) @@ -78,7 +82,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { .run(); }, [editor]); - const alignVideoCenter = useCallback(() => { + const alignCenter = useCallback(() => { editor .chain() .focus(undefined, { scrollIntoView: false }) @@ -86,7 +90,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { .run(); }, [editor]); - const alignVideoRight = useCallback(() => { + const alignRight = useCallback(() => { editor .chain() .focus(undefined, { scrollIntoView: false }) @@ -94,16 +98,18 @@ export function VideoMenu({ editor }: EditorMenuProps) { .run(); }, [editor]); - const onWidthChange = useCallback( - (value: number) => { - editor - .chain() - .focus(undefined, { scrollIntoView: false }) - .setVideoWidth(value) - .run(); - }, - [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 ( - +
@@ -132,10 +139,11 @@ export function VideoMenu({ editor }: EditorMenuProps) { @@ -143,19 +151,40 @@ export function VideoMenu({ editor }: EditorMenuProps) { - - {editorState?.width && ( - - )} +
+ + + + + + + + + + + + +
); } diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index ef03108b..687e76f9 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -43,6 +43,8 @@ import { Highlight, UniqueID, SharedStorage, + Columns, + Column, } from "@docmost/editor-ext"; import { randomElement, @@ -52,6 +54,14 @@ import { IUser } from "@/features/user/types/user.types.ts"; import MathInlineView from "@/features/editor/components/math/math-inline.tsx"; import MathBlockView from "@/features/editor/components/math/math-block.tsx"; import ImageView from "@/features/editor/components/image/image-view.tsx"; +import { + createImageHandle, + imageResizeClasses, +} from "@/features/editor/components/image/image-resize-handles.ts"; +import { + createResizeHandle, + buildResizeClasses, +} from "@/features/editor/components/common/node-resize-handles.ts"; import CalloutView from "@/features/editor/components/callout/callout-view.tsx"; import VideoView from "@/features/editor/components/video/video-view.tsx"; import AttachmentView from "@/features/editor/components/attachment/attachment-view.tsx"; @@ -91,6 +101,7 @@ lowlight.register("fortran", fortran); lowlight.register("haskell", haskell); lowlight.register("scala", scala); +// @ts-ignore export const mainExtensions = [ StarterKit.configure({ heading: false, @@ -115,7 +126,7 @@ export const mainExtensions = [ filterTransaction: (transaction) => !isChangeOrigin(transaction), }), Placeholder.configure({ - placeholder: ({ node }) => { + placeholder: ({ editor, node, pos }) => { if (node.type.name === "heading") { return i18n.t("Heading {{level}}", { level: node.attrs.level }); } @@ -123,6 +134,17 @@ export const mainExtensions = [ return i18n.t("Toggle title"); } if (node.type.name === "paragraph") { + const $pos = editor.state.doc.resolve(pos); + const parentName = $pos.parent.type.name; + if ( + parentName === "column" || + parentName === "tableCell" || + parentName === "tableHeader" || + parentName === "callout" || + parentName === "blockquote" + ) { + return i18n.t("Write..."); + } return i18n.t('Write anything. Enter "/" for commands'); } }, @@ -200,9 +222,29 @@ export const mainExtensions = [ TiptapImage.configure({ view: ImageView, allowBase64: false, + resize: { + enabled: true, + directions: ["left", "right"], + minWidth: 80, + minHeight: 40, + alwaysPreserveAspectRatio: true, + //@ts-ignore + createCustomHandle: createImageHandle, + className: imageResizeClasses, + }, }), TiptapVideo.configure({ view: VideoView, + resize: { + enabled: true, + directions: ["left", "right"], + minWidth: 80, + minHeight: 40, + alwaysPreserveAspectRatio: true, + //@ts-ignore + createCustomHandle: createResizeHandle, + className: buildResizeClasses("node-video"), + }, }), Callout.configure({ view: CalloutView, @@ -221,9 +263,29 @@ export const mainExtensions = [ }), Drawio.configure({ view: DrawioView, + resize: { + enabled: true, + directions: ["left", "right"], + minWidth: 80, + minHeight: 40, + alwaysPreserveAspectRatio: true, + //@ts-ignore + createCustomHandle: createResizeHandle, + className: buildResizeClasses("node-drawio"), + }, }), Excalidraw.configure({ view: ExcalidrawView, + resize: { + enabled: true, + directions: ["left", "right"], + minWidth: 80, + minHeight: 40, + alwaysPreserveAspectRatio: true, + //@ts-ignore + createCustomHandle: createResizeHandle, + className: buildResizeClasses("node-excalidraw"), + }, }), Embed.configure({ view: EmbedView, @@ -253,6 +315,8 @@ export const mainExtensions = [ }; }, }).configure(), + Columns, + Column, ] as any; type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[]; diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index ed7ccecd..d0d1de03 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -67,6 +67,7 @@ import { jwtDecode } from "jwt-decode"; import { searchSpotlight } from "@/features/search/constants.ts"; import { useEditorScroll } from "./hooks/use-editor-scroll"; import { EditorAiMenu } from "@/ee/ai/components/editor/ai-menu/ai-menu"; +import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx"; interface PageEditorProps { pageId: string; @@ -416,6 +417,7 @@ export default function PageEditor({ +
)} diff --git a/apps/client/src/features/editor/styles/columns.css b/apps/client/src/features/editor/styles/columns.css new file mode 100644 index 00000000..fac034f6 --- /dev/null +++ b/apps/client/src/features/editor/styles/columns.css @@ -0,0 +1,116 @@ +div[data-type="columns"] { + display: flex; + margin: 0.75rem 0; + padding: 0.5em; +} + +div[data-type="columns"] > div[data-type="column"] { + flex: 1; + min-width: 0; +} + +div[data-type="columns"] > div[data-type="column"] + div[data-type="column"] { + border-left: 1px solid transparent; + padding-left: 1rem; + transition: border 0.3s; +} + +div[data-type="columns"]:hover + > div[data-type="column"] + + div[data-type="column"] { + border-left: 1px solid + light-dark(var(--mantine-color-gray-3), var(--mantine-color-gray-7)); +} + +/* Confluence layout types */ +div[data-type="columns"][data-layout="two_left_sidebar"] + > div[data-type="column"]:first-child { + flex: 1; +} + +div[data-type="columns"][data-layout="two_left_sidebar"] + > div[data-type="column"]:last-child { + flex: 2; +} + +div[data-type="columns"][data-layout="two_right_sidebar"] + > div[data-type="column"]:first-child { + flex: 2; +} + +div[data-type="columns"][data-layout="two_right_sidebar"] + > div[data-type="column"]:last-child { + flex: 1; +} + +div[data-type="columns"][data-layout="three_left_wide"] + > div[data-type="column"]:first-child { + flex: 2; +} + +div[data-type="columns"][data-layout="three_right_wide"] + > div[data-type="column"]:last-child { + flex: 2; +} + +div[data-type="columns"][data-layout="three_with_sidebars"] + > div[data-type="column"]:first-child, +div[data-type="columns"][data-layout="three_with_sidebars"] + > div[data-type="column"]:last-child { + flex: 1; +} + +div[data-type="columns"][data-layout="three_with_sidebars"] + > div[data-type="column"]:nth-child(2) { + flex: 2; +} + +/* Stack columns vertically on small viewports */ +@media (max-width: 820px) { + div[data-type="columns"] { + flex-direction: column; + } + + div[data-type="columns"] > div[data-type="column"] + div[data-type="column"] { + border-left: none; + padding-left: 0; + } + + div[data-type="columns"]:hover + > div[data-type="column"] + + div[data-type="column"] { + border-left: none; + } +} + +/* Wide width mode — extends columns to full container width */ +div[data-type="columns"][data-width-mode="wide"] { + margin-left: -3rem; + margin-right: -3rem; + width: calc(100% + 6rem); +} + +@media (max-width: $mantine-breakpoint-sm) { + div[data-type="columns"][data-width-mode="wide"] { + margin-left: -1rem; + margin-right: -1rem; + width: calc(100% + 2rem); + } +} + +@media print { + div[data-type="columns"] { + flex-direction: row !important; + } + + div[data-type="columns"] > div[data-type="column"] + div[data-type="column"] { + border-left: none; + padding-left: 1rem; + } + + div[data-type="columns"][data-width-mode="wide"] { + margin-left: 0; + margin-right: 0; + width: 100%; + } +} diff --git a/apps/client/src/features/editor/styles/core.css b/apps/client/src/features/editor/styles/core.css index 0aed878e..c1f67554 100644 --- a/apps/client/src/features/editor/styles/core.css +++ b/apps/client/src/features/editor/styles/core.css @@ -82,13 +82,9 @@ } blockquote { - padding-left: 25px; - padding-right: 25px; - border-left: 2px solid var(--mantine-color-gray-6); - background-color: light-dark( - var(--mantine-color-gray-0), - var(--mantine-color-dark-8) - ); + padding-left: 1rem; + border-left: 3px solid + light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4)); margin: 0; } @@ -126,13 +122,14 @@ margin-bottom: 0; } - &.node-callout { - div[style*="white-space: inherit;"] { - > :first-child { - margin: 0; - } - } - } + } + + .react-renderer.node-callout div[style*="white-space: inherit;"] > :first-child { + margin-top: 0; + } + + .react-renderer.node-callout + .react-renderer.node-callout { + margin-top: 0.75em; } .selection { diff --git a/apps/client/src/features/editor/styles/index.css b/apps/client/src/features/editor/styles/index.css index e32a606f..120c2a10 100644 --- a/apps/client/src/features/editor/styles/index.css +++ b/apps/client/src/features/editor/styles/index.css @@ -13,3 +13,4 @@ @import "./mention.css"; @import "./ordered-list.css"; @import "./highlight.css"; +@import "./columns.css"; diff --git a/apps/client/src/main.tsx b/apps/client/src/main.tsx index 63a775de..0e4c3314 100644 --- a/apps/client/src/main.tsx +++ b/apps/client/src/main.tsx @@ -42,9 +42,9 @@ if (isCloud() && isPostHogEnabled) { }); } -const root = ReactDOM.createRoot( - document.getElementById("root") as HTMLElement, -); + +const container = document.getElementById("root") as HTMLElement; +const root = (container as any).__reactRoot ??= ReactDOM.createRoot(container); root.render( diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index a29bb22a..9f173d44 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -35,6 +35,8 @@ import { UniqueID, addUniqueIdsToDoc, htmlToMarkdown, + Columns, + Column, } from '@docmost/editor-ext'; import { generateText, getSchema, JSONContent } from '@tiptap/core'; import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html'; @@ -91,6 +93,8 @@ export const tiptapExtensions = [ Embed, Mention, Subpages, + Columns, + Column, ] as any; export function jsonToHtml(tiptapJson: any) { diff --git a/package.json b/package.json index 7106d562..b9bf1043 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ "packageManager": "pnpm@10.4.0", "pnpm": { "patchedDependencies": { - "react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch" + "react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch", + "@tiptap/core": "patches/@tiptap__core.patch" }, "overrides": { "jsdom": "25.0.1", diff --git a/packages/editor-ext/src/index.ts b/packages/editor-ext/src/index.ts index 102cc4b1..feb7e488 100644 --- a/packages/editor-ext/src/index.ts +++ b/packages/editor-ext/src/index.ts @@ -25,3 +25,4 @@ export * from "./lib/heading/heading"; export * from "./lib/unique-id"; export * from "./lib/shared-storage"; export * from "./lib/recreate-transform"; +export * from "./lib/columns"; diff --git a/packages/editor-ext/src/lib/callout/utils.ts b/packages/editor-ext/src/lib/callout/utils.ts index 6484aa3b..26ff3856 100644 --- a/packages/editor-ext/src/lib/callout/utils.ts +++ b/packages/editor-ext/src/lib/callout/utils.ts @@ -1,8 +1,21 @@ -export type CalloutType = "default" | "info" | "success" | "warning" | "danger"; -const validCalloutTypes = ["default", "info", "success", "warning", "danger"]; +export type CalloutType = + | 'default' + | 'info' + | 'note' + | 'success' + | 'warning' + | 'danger'; +const validCalloutTypes = [ + 'default', + 'info', + 'note', + 'success', + 'warning', + 'danger', +]; export function getValidCalloutType(value: string): string { if (value) { - return validCalloutTypes.includes(value) ? value : "info"; + return validCalloutTypes.includes(value) ? value : 'info'; } } diff --git a/packages/editor-ext/src/lib/columns/column.ts b/packages/editor-ext/src/lib/columns/column.ts new file mode 100644 index 00000000..ea41acfc --- /dev/null +++ b/packages/editor-ext/src/lib/columns/column.ts @@ -0,0 +1,127 @@ +import { Node, mergeAttributes, findParentNode } from "@tiptap/core"; +import { TextSelection } from "prosemirror-state"; + +export interface ColumnOptions { + HTMLAttributes: Record; +} + +export interface ColumnAttributes { + width?: number | null; +} + +declare module "@tiptap/core" { + interface Commands { + column: { + setColumnWidth: (width: number | null) => ReturnType; + }; + } +} + +export const Column = Node.create({ + name: "column", + group: "block", + content: "block+", + defining: true, + isolating: true, + selectable: false, + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + addAttributes() { + return { + width: { + default: null, + parseHTML: (element) => { + const value = element.getAttribute("data-width"); + return value ? parseFloat(value) : null; + }, + renderHTML: (attributes: ColumnAttributes) => { + if (!attributes.width) return {}; + return { + "data-width": attributes.width, + style: `flex: ${attributes.width}`, + }; + }, + }, + }; + }, + + parseHTML() { + return [ + { + tag: `div[data-type="${this.name}"]`, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes( + { "data-type": this.name }, + this.options.HTMLAttributes, + HTMLAttributes, + ), + 0, + ]; + }, + + addKeyboardShortcuts() { + const jumpToColumn = (direction: 1 | -1) => () => { + const { state, dispatch } = this.editor.view; + + const columns = findParentNode( + (node) => node.type.name === "columns", + )(state.selection); + if (!columns) return false; + + const column = findParentNode( + (node) => node.type.name === "column", + )(state.selection); + if (!column) return false; + + let currentIndex = -1; + columns.node.forEach((_child, offset, index) => { + if (columns.pos + 1 + offset === column.pos) { + currentIndex = index; + } + }); + + const targetIndex = currentIndex + direction; + if (targetIndex < 0 || targetIndex >= columns.node.childCount) { + return true; + } + + let offset = 0; + for (let j = 0; j < targetIndex; j++) { + offset += columns.node.child(j).nodeSize; + } + + const targetPos = columns.pos + 1 + offset + 1 + 1; + if (dispatch) { + dispatch( + state.tr.setSelection(TextSelection.create(state.doc, targetPos)), + ); + } + return true; + }; + + return { + Tab: jumpToColumn(1), + "Shift-Tab": jumpToColumn(-1), + }; + }, + + addCommands() { + return { + setColumnWidth: + (width) => + ({ commands }) => + commands.updateAttributes("column", { width }), + }; + }, +}); diff --git a/packages/editor-ext/src/lib/columns/columns.ts b/packages/editor-ext/src/lib/columns/columns.ts new file mode 100644 index 00000000..f2682a73 --- /dev/null +++ b/packages/editor-ext/src/lib/columns/columns.ts @@ -0,0 +1,196 @@ +import { Node, mergeAttributes, findParentNode } from "@tiptap/core"; +import { Fragment, Node as PMNode } from "prosemirror-model"; +import { TextSelection } from "prosemirror-state"; + +export type ColumnsLayout = + | "two_equal" + | "two_left_sidebar" + | "two_right_sidebar" + | "three_equal" + | "three_left_wide" + | "three_right_wide" + | "three_with_sidebars" + | "four_equal" + | "five_equal"; + +export interface ColumnsOptions { + HTMLAttributes: Record; +} + +export type WidthMode = "normal" | "wide"; + +export interface ColumnsAttributes { + layout?: ColumnsLayout; + widthMode?: WidthMode; +} + +declare module "@tiptap/core" { + interface Commands { + columns: { + insertColumns: (attributes?: ColumnsAttributes) => ReturnType; + setColumnsWidthMode: (widthMode: WidthMode) => ReturnType; + setColumnCount: (count: number) => ReturnType; + setColumnsLayout: (layout: ColumnsLayout) => ReturnType; + }; + } +} + +function columnCountFromLayout(layout: string): number { + if (layout.startsWith("five")) return 5; + if (layout.startsWith("four")) return 4; + if (layout.startsWith("three")) return 3; + return 2; +} + +function defaultLayoutForCount(count: number): ColumnsLayout { + if (count === 3) return "three_equal"; + if (count === 4) return "four_equal"; + if (count === 5) return "five_equal"; + return "two_equal"; +} + +export const Columns = Node.create({ + name: "columns", + group: "block", + content: "column+", + defining: true, + isolating: true, + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + addAttributes() { + return { + layout: { + default: "two_equal", + parseHTML: (element) => element.getAttribute("data-layout"), + renderHTML: (attributes: ColumnsAttributes) => ({ + "data-layout": attributes.layout, + }), + }, + widthMode: { + default: "normal", + parseHTML: (element) => + element.getAttribute("data-width-mode") || "normal", + renderHTML: (attributes: ColumnsAttributes) => { + if (!attributes.widthMode || attributes.widthMode === "normal") + return {}; + return { "data-width-mode": attributes.widthMode }; + }, + }, + }; + }, + + parseHTML() { + return [ + { + tag: `div[data-type="${this.name}"]`, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes( + { "data-type": this.name }, + this.options.HTMLAttributes, + HTMLAttributes, + ), + 0, + ]; + }, + + addCommands() { + return { + insertColumns: + (attributes) => + ({ tr, state, dispatch }) => { + const layout = attributes?.layout || "two_equal"; + const count = columnCountFromLayout(layout); + + const columnType = state.schema.nodes.column; + const paraType = state.schema.nodes.paragraph; + const children = Array.from({ length: count }, () => + columnType.create(null, paraType.create()), + ); + const columnsNode = this.type.create( + attributes, + Fragment.from(children), + ); + + const stepsBefore = tr.steps.length; + tr.replaceSelectionWith(columnsNode); + + if (tr.steps.length > stepsBefore) { + const stepMap = tr.steps[tr.steps.length - 1].getMap(); + let insertStart = 0; + stepMap.forEach((_from, _to, newFrom) => { + insertStart = newFrom; + }); + tr.setSelection( + TextSelection.near(tr.doc.resolve(insertStart + 1), 1), + ); + } + + if (dispatch) dispatch(tr); + return true; + }, + + setColumnsWidthMode: + (widthMode) => + ({ commands }) => + commands.updateAttributes("columns", { widthMode }), + + setColumnCount: + (count: number) => + ({ tr, state }) => { + const predicate = (node: PMNode) => node.type.name === "columns"; + const parent = findParentNode(predicate)(state.selection); + if (!parent) return false; + + const { node: columnsNode, pos: parentPos } = parent; + const currentCount = columnsNode.childCount; + if (count === currentCount || count < 2 || count > 5) return false; + + const columnType = state.schema.nodes.column; + const paraType = state.schema.nodes.paragraph; + const newChildren: PMNode[] = []; + + if (count > currentCount) { + for (let i = 0; i < currentCount; i++) { + newChildren.push(columnsNode.child(i)); + } + for (let i = currentCount; i < count; i++) { + newChildren.push(columnType.create(null, paraType.create())); + } + } else { + for (let i = 0; i < count - 1; i++) { + newChildren.push(columnsNode.child(i)); + } + let mergedContent = columnsNode.child(count - 1).content; + for (let j = count; j < currentCount; j++) { + mergedContent = mergedContent.append(columnsNode.child(j).content); + } + newChildren.push(columnType.create(null, mergedContent)); + } + + const newLayout = defaultLayoutForCount(count); + const newNode = columnsNode.type.create( + { ...columnsNode.attrs, layout: newLayout }, + Fragment.from(newChildren), + ); + tr.replaceWith(parentPos, parentPos + columnsNode.nodeSize, newNode); + return true; + }, + + setColumnsLayout: + (layout) => + ({ commands }) => + commands.updateAttributes("columns", { layout }), + }; + }, +}); diff --git a/packages/editor-ext/src/lib/columns/index.ts b/packages/editor-ext/src/lib/columns/index.ts new file mode 100644 index 00000000..e7af35b6 --- /dev/null +++ b/packages/editor-ext/src/lib/columns/index.ts @@ -0,0 +1,4 @@ +export { Columns } from "./columns"; +export type { ColumnsOptions, ColumnsAttributes, ColumnsLayout, WidthMode } from "./columns"; +export { Column } from "./column"; +export type { ColumnOptions, ColumnAttributes } from "./column"; diff --git a/packages/editor-ext/src/lib/drawio.ts b/packages/editor-ext/src/lib/drawio.ts index 3cc041a2..bd42b0e4 100644 --- a/packages/editor-ext/src/lib/drawio.ts +++ b/packages/editor-ext/src/lib/drawio.ts @@ -1,15 +1,35 @@ -import { Node, mergeAttributes } from "@tiptap/core"; +import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core"; +import type { ResizableNodeViewDirection } from "@tiptap/core"; import { ReactNodeViewRenderer } from "@tiptap/react"; +export type DrawioResizeOptions = { + enabled: boolean; + directions?: ResizableNodeViewDirection[]; + minWidth?: number; + minHeight?: number; + alwaysPreserveAspectRatio?: boolean; + createCustomHandle?: (direction: ResizableNodeViewDirection) => HTMLElement; + className?: { + container?: string; + wrapper?: string; + handle?: string; + resizing?: string; + }; +}; + export interface DrawioOptions { HTMLAttributes: Record; view: any; + resize: DrawioResizeOptions | false; } + export interface DrawioAttributes { src?: string; title?: string; size?: number; - width?: string; + width?: number | string; + height?: number; + aspectRatio?: number; align?: string; attachmentId?: string; } @@ -18,6 +38,8 @@ declare module "@tiptap/core" { interface Commands { drawio: { setDrawio: (attributes?: DrawioAttributes) => ReturnType; + setDrawioAlign: (align: "left" | "center" | "right") => ReturnType; + setDrawioSize: (width: number, height: number) => ReturnType; }; } } @@ -35,6 +57,7 @@ export const Drawio = Node.create({ return { HTMLAttributes: {}, view: null, + resize: false, }; }, @@ -55,12 +78,30 @@ export const Drawio = Node.create({ }), }, width: { - default: "100%", - parseHTML: (element) => element.getAttribute("data-width"), + default: null, + parseHTML: (element) => { + const raw = element.getAttribute("data-width"); + if (!raw) return null; + if (raw.endsWith("%")) return raw; + const num = parseFloat(raw); + return isNaN(num) ? null : num; + }, renderHTML: (attributes: DrawioAttributes) => ({ "data-width": attributes.width, }), }, + height: { + default: null, + parseHTML: (element) => { + const raw = element.getAttribute("data-height"); + if (!raw) return null; + const num = parseFloat(raw); + return isNaN(num) ? null : num; + }, + renderHTML: (attributes: DrawioAttributes) => ({ + "data-height": attributes.height, + }), + }, size: { default: null, parseHTML: (element) => element.getAttribute("data-size"), @@ -68,6 +109,13 @@ export const Drawio = Node.create({ "data-size": attributes.size, }), }, + aspectRatio: { + default: null, + parseHTML: (element) => element.getAttribute("data-aspect-ratio"), + renderHTML: (attributes: DrawioAttributes) => ({ + "data-aspect-ratio": attributes.aspectRatio, + }), + }, align: { default: "center", parseHTML: (element) => element.getAttribute("data-align"), @@ -99,7 +147,7 @@ export const Drawio = Node.create({ mergeAttributes( { "data-type": this.name }, this.options.HTMLAttributes, - HTMLAttributes + HTMLAttributes, ), [ "img", @@ -122,13 +170,172 @@ export const Drawio = Node.create({ attrs: attrs, }); }, + + setDrawioAlign: + (align) => + ({ commands }) => + commands.updateAttributes("drawio", { align }), + + setDrawioSize: + (width, height) => + ({ commands }) => + commands.updateAttributes("drawio", { width, height }), }; }, addNodeView() { - // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) - this.editor.isInitialized = true; + const resize = this.options.resize; - return ReactNodeViewRenderer(this.options.view); + if (!resize || !resize.enabled) { + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); + } + + const { + directions, + minWidth, + minHeight, + alwaysPreserveAspectRatio, + createCustomHandle, + className, + } = resize; + + return (props) => { + const { node, getPos, HTMLAttributes, editor } = props; + + if (!node.attrs.src) { + editor.isInitialized = true; + const reactView = ReactNodeViewRenderer(this.options.view); + const view = reactView(props); + + const originalUpdate = view.update?.bind(view); + view.update = (updatedNode, decorations, innerDecorations) => { + if (updatedNode.attrs.src && !node.attrs.src) { + return false; + } + if (originalUpdate) { + return originalUpdate(updatedNode, decorations, innerDecorations); + } + return true; + }; + + return view; + } + + const el = document.createElement("img"); + el.src = node.attrs.src; + el.alt = node.attrs.title || ""; + el.style.display = "block"; + el.style.maxWidth = "100%"; + el.style.borderRadius = "8px"; + + let currentNode = node; + + const nodeView = new ResizableNodeView({ + element: el, + editor, + node, + getPos, + onResize: (w, h) => { + el.style.width = `${w}px`; + el.style.height = `${h}px`; + }, + onCommit: () => { + const pos = getPos(); + if (pos === undefined) return; + + this.editor + .chain() + .setNodeSelection(pos) + .updateAttributes(this.name, { + width: Math.round(el.offsetWidth), + height: Math.round(el.offsetHeight), + }) + .run(); + }, + onUpdate: (updatedNode, _decorations, _innerDecorations) => { + if (updatedNode.type !== currentNode.type) { + return false; + } + + if (updatedNode.attrs.src !== currentNode.attrs.src) { + el.src = updatedNode.attrs.src || ""; + } + + const w = updatedNode.attrs.width; + const h = updatedNode.attrs.height; + if (w != null) { + el.style.width = `${w}px`; + } + if (h != null) { + el.style.height = `${h}px`; + } + + const align = updatedNode.attrs.align || "center"; + const container = nodeView.dom as HTMLElement; + applyAlignment(container, align); + + currentNode = updatedNode; + return true; + }, + options: { + directions, + min: { + width: minWidth, + height: minHeight, + }, + preserveAspectRatio: alwaysPreserveAspectRatio === true, + createCustomHandle, + className, + }, + }); + + const dom = nodeView.dom as HTMLElement; + + applyAlignment(dom, node.attrs.align || "center"); + + // Handle percentage width backward compat + const widthAttr = node.attrs.width; + if (typeof widthAttr === "string" && widthAttr.endsWith("%")) { + requestAnimationFrame(() => { + const parentEl = dom.parentElement; + if (parentEl) { + const containerWidth = parentEl.clientWidth; + const pctValue = parseInt(widthAttr, 10); + if (!isNaN(pctValue) && containerWidth > 0) { + const pxWidth = Math.round( + containerWidth * (pctValue / 100), + ); + el.style.width = `${pxWidth}px`; + if (node.attrs.aspectRatio) { + el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`; + } + } + } + dom.style.visibility = ""; + dom.style.pointerEvents = ""; + }); + } + + // Hide until image loads + dom.style.visibility = "hidden"; + dom.style.pointerEvents = "none"; + el.onload = () => { + dom.style.visibility = ""; + dom.style.pointerEvents = ""; + }; + + return nodeView; + }; }, }); + +function applyAlignment(container: HTMLElement, align: string) { + if (align === "left") { + container.style.justifyContent = "flex-start"; + } else if (align === "right") { + container.style.justifyContent = "flex-end"; + } else { + container.style.justifyContent = "center"; + } +} diff --git a/packages/editor-ext/src/lib/excalidraw.ts b/packages/editor-ext/src/lib/excalidraw.ts index 28b064e4..f132d37a 100644 --- a/packages/editor-ext/src/lib/excalidraw.ts +++ b/packages/editor-ext/src/lib/excalidraw.ts @@ -1,15 +1,35 @@ -import { Node, mergeAttributes } from "@tiptap/core"; +import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core"; +import type { ResizableNodeViewDirection } from "@tiptap/core"; import { ReactNodeViewRenderer } from "@tiptap/react"; +export type ExcalidrawResizeOptions = { + enabled: boolean; + directions?: ResizableNodeViewDirection[]; + minWidth?: number; + minHeight?: number; + alwaysPreserveAspectRatio?: boolean; + createCustomHandle?: (direction: ResizableNodeViewDirection) => HTMLElement; + className?: { + container?: string; + wrapper?: string; + handle?: string; + resizing?: string; + }; +}; + export interface ExcalidrawOptions { HTMLAttributes: Record; view: any; + resize: ExcalidrawResizeOptions | false; } + export interface ExcalidrawAttributes { src?: string; title?: string; size?: number; - width?: string; + width?: number | string; + height?: number; + aspectRatio?: number; align?: string; attachmentId?: string; } @@ -18,6 +38,8 @@ declare module "@tiptap/core" { interface Commands { excalidraw: { setExcalidraw: (attributes?: ExcalidrawAttributes) => ReturnType; + setExcalidrawAlign: (align: "left" | "center" | "right") => ReturnType; + setExcalidrawSize: (width: number, height: number) => ReturnType; }; } } @@ -35,8 +57,10 @@ export const Excalidraw = Node.create({ return { HTMLAttributes: {}, view: null, + resize: false, }; }, + addAttributes() { return { src: { @@ -54,12 +78,30 @@ export const Excalidraw = Node.create({ }), }, width: { - default: "100%", - parseHTML: (element) => element.getAttribute("data-width"), + default: null, + parseHTML: (element) => { + const raw = element.getAttribute("data-width"); + if (!raw) return null; + if (raw.endsWith("%")) return raw; + const num = parseFloat(raw); + return isNaN(num) ? null : num; + }, renderHTML: (attributes: ExcalidrawAttributes) => ({ "data-width": attributes.width, }), }, + height: { + default: null, + parseHTML: (element) => { + const raw = element.getAttribute("data-height"); + if (!raw) return null; + const num = parseFloat(raw); + return isNaN(num) ? null : num; + }, + renderHTML: (attributes: ExcalidrawAttributes) => ({ + "data-height": attributes.height, + }), + }, size: { default: null, parseHTML: (element) => element.getAttribute("data-size"), @@ -67,6 +109,13 @@ export const Excalidraw = Node.create({ "data-size": attributes.size, }), }, + aspectRatio: { + default: null, + parseHTML: (element) => element.getAttribute("data-aspect-ratio"), + renderHTML: (attributes: ExcalidrawAttributes) => ({ + "data-aspect-ratio": attributes.aspectRatio, + }), + }, align: { default: "center", parseHTML: (element) => element.getAttribute("data-align"), @@ -98,7 +147,7 @@ export const Excalidraw = Node.create({ mergeAttributes( { "data-type": this.name }, this.options.HTMLAttributes, - HTMLAttributes + HTMLAttributes, ), [ "img", @@ -121,13 +170,172 @@ export const Excalidraw = Node.create({ attrs: attrs, }); }, + + setExcalidrawAlign: + (align) => + ({ commands }) => + commands.updateAttributes("excalidraw", { align }), + + setExcalidrawSize: + (width, height) => + ({ commands }) => + commands.updateAttributes("excalidraw", { width, height }), }; }, addNodeView() { - // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) - this.editor.isInitialized = true; + const resize = this.options.resize; - return ReactNodeViewRenderer(this.options.view); + if (!resize || !resize.enabled) { + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); + } + + const { + directions, + minWidth, + minHeight, + alwaysPreserveAspectRatio, + createCustomHandle, + className, + } = resize; + + return (props) => { + const { node, getPos, HTMLAttributes, editor } = props; + + if (!node.attrs.src) { + editor.isInitialized = true; + const reactView = ReactNodeViewRenderer(this.options.view); + const view = reactView(props); + + const originalUpdate = view.update?.bind(view); + view.update = (updatedNode, decorations, innerDecorations) => { + if (updatedNode.attrs.src && !node.attrs.src) { + return false; + } + if (originalUpdate) { + return originalUpdate(updatedNode, decorations, innerDecorations); + } + return true; + }; + + return view; + } + + const el = document.createElement("img"); + el.src = node.attrs.src; + el.alt = node.attrs.title || ""; + el.style.display = "block"; + el.style.maxWidth = "100%"; + el.style.borderRadius = "8px"; + + let currentNode = node; + + const nodeView = new ResizableNodeView({ + element: el, + editor, + node, + getPos, + onResize: (w, h) => { + el.style.width = `${w}px`; + el.style.height = `${h}px`; + }, + onCommit: () => { + const pos = getPos(); + if (pos === undefined) return; + + this.editor + .chain() + .setNodeSelection(pos) + .updateAttributes(this.name, { + width: Math.round(el.offsetWidth), + height: Math.round(el.offsetHeight), + }) + .run(); + }, + onUpdate: (updatedNode, _decorations, _innerDecorations) => { + if (updatedNode.type !== currentNode.type) { + return false; + } + + if (updatedNode.attrs.src !== currentNode.attrs.src) { + el.src = updatedNode.attrs.src || ""; + } + + const w = updatedNode.attrs.width; + const h = updatedNode.attrs.height; + if (w != null) { + el.style.width = `${w}px`; + } + if (h != null) { + el.style.height = `${h}px`; + } + + const align = updatedNode.attrs.align || "center"; + const container = nodeView.dom as HTMLElement; + applyAlignment(container, align); + + currentNode = updatedNode; + return true; + }, + options: { + directions, + min: { + width: minWidth, + height: minHeight, + }, + preserveAspectRatio: alwaysPreserveAspectRatio === true, + createCustomHandle, + className, + }, + }); + + const dom = nodeView.dom as HTMLElement; + + applyAlignment(dom, node.attrs.align || "center"); + + // Handle percentage width backward compat + const widthAttr = node.attrs.width; + if (typeof widthAttr === "string" && widthAttr.endsWith("%")) { + requestAnimationFrame(() => { + const parentEl = dom.parentElement; + if (parentEl) { + const containerWidth = parentEl.clientWidth; + const pctValue = parseInt(widthAttr, 10); + if (!isNaN(pctValue) && containerWidth > 0) { + const pxWidth = Math.round( + containerWidth * (pctValue / 100), + ); + el.style.width = `${pxWidth}px`; + if (node.attrs.aspectRatio) { + el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`; + } + } + } + dom.style.visibility = ""; + dom.style.pointerEvents = ""; + }); + } + + // Hide until image loads + dom.style.visibility = "hidden"; + dom.style.pointerEvents = "none"; + el.onload = () => { + dom.style.visibility = ""; + dom.style.pointerEvents = ""; + }; + + return nodeView; + }; }, }); + +function applyAlignment(container: HTMLElement, align: string) { + if (align === "left") { + container.style.justifyContent = "flex-start"; + } else if (align === "right") { + container.style.justifyContent = "flex-end"; + } else { + container.style.justifyContent = "center"; + } +} diff --git a/packages/editor-ext/src/lib/image/image.ts b/packages/editor-ext/src/lib/image/image.ts index e0f5053d..8798a9bb 100644 --- a/packages/editor-ext/src/lib/image/image.ts +++ b/packages/editor-ext/src/lib/image/image.ts @@ -1,18 +1,41 @@ import Image from "@tiptap/extension-image"; import { ImageOptions as DefaultImageOptions } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; -import { mergeAttributes, Range } from "@tiptap/core"; +import { + mergeAttributes, + Range, + ResizableNodeView, +} from "@tiptap/core"; +import type { ResizableNodeViewDirection } from "@tiptap/core"; + +export type ImageResizeOptions = { + enabled: boolean; + directions?: ResizableNodeViewDirection[]; + minWidth?: number; + minHeight?: number; + alwaysPreserveAspectRatio?: boolean; + createCustomHandle?: (direction: ResizableNodeViewDirection) => HTMLElement; + className?: { + container?: string; + wrapper?: string; + handle?: string; + resizing?: string; + }; +}; export interface ImageOptions extends DefaultImageOptions { view: any; + resize: ImageResizeOptions | false; } + export interface ImageAttributes { src?: string; alt?: string; align?: string; attachmentId?: string; size?: number; - width?: number; + width?: number | string; + height?: number; aspectRatio?: number; placeholder?: { id: string; @@ -25,10 +48,11 @@ declare module "@tiptap/core" { imageBlock: { setImage: (attributes: ImageAttributes) => ReturnType; setImageAt: ( - attributes: ImageAttributes & { pos: number | Range } + attributes: ImageAttributes & { pos: number | Range }, ) => ReturnType; setImageAlign: (align: "left" | "center" | "right") => ReturnType; setImageWidth: (width: number) => ReturnType; + setImageSize: (width: number, height: number) => ReturnType; }; } } @@ -46,6 +70,7 @@ export const TiptapImage = Image.extend({ return { ...this.parent?.(), view: null, + resize: false, }; }, @@ -59,12 +84,30 @@ export const TiptapImage = Image.extend({ }), }, width: { - default: "100%", - parseHTML: (element) => element.getAttribute("width"), + default: null, + parseHTML: (element) => { + const raw = element.getAttribute("width"); + if (!raw) return null; + if (raw.endsWith("%")) return raw; + const num = parseFloat(raw); + return isNaN(num) ? null : num; + }, renderHTML: (attributes: ImageAttributes) => ({ width: attributes.width, }), }, + height: { + default: null, + parseHTML: (element) => { + const raw = element.getAttribute("height"); + if (!raw) return null; + const num = parseFloat(raw); + return isNaN(num) ? null : num; + }, + renderHTML: (attributes: ImageAttributes) => ({ + height: attributes.height, + }), + }, align: { default: "center", parseHTML: (element) => element.getAttribute("data-align"), @@ -142,16 +185,192 @@ export const TiptapImage = Image.extend({ setImageWidth: (width) => ({ commands }) => - commands.updateAttributes("image", { - width: `${Math.max(0, Math.min(100, width))}%`, - }), + commands.updateAttributes("image", { width }), + + setImageSize: + (width, height) => + ({ commands }) => + commands.updateAttributes("image", { width, height }), }; }, addNodeView() { - // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) - this.editor.isInitialized = true; + const resize = this.options.resize; - return ReactNodeViewRenderer(this.options.view); + if (!resize || !resize.enabled) { + // Fallback to React node view (existing behavior) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); + } + + const { + directions, + minWidth, + minHeight, + alwaysPreserveAspectRatio, + createCustomHandle, + className, + } = resize; + + return (props) => { + const { node, getPos, HTMLAttributes, editor } = props; + + // If no src yet (placeholder/uploading), use React view for loading UI + if (!HTMLAttributes.src) { + editor.isInitialized = true; + const reactView = ReactNodeViewRenderer(this.options.view); + const view = reactView(props); + + // When the node gets a src, return false from update to force rebuild + const originalUpdate = view.update?.bind(view); + view.update = (updatedNode, decorations, innerDecorations) => { + if (updatedNode.attrs.src && !node.attrs.src) { + return false; + } + if (originalUpdate) { + return originalUpdate(updatedNode, decorations, innerDecorations); + } + return true; + }; + + return view; + } + + // Has src — use ResizableNodeView + const el = document.createElement("img"); + + Object.entries(HTMLAttributes).forEach(([key, value]) => { + if (value != null) { + switch (key) { + case "width": + case "height": + break; + default: + el.setAttribute(key, String(value)); + break; + } + } + }); + + el.src = HTMLAttributes.src; + el.style.display = "block"; + el.style.maxWidth = "100%"; + el.style.borderRadius = "8px"; + + let currentNode = node; + + const nodeView = new ResizableNodeView({ + element: el, + editor, + node, + getPos, + onResize: (w, h) => { + el.style.width = `${w}px`; + el.style.height = `${h}px`; + }, + onCommit: () => { + const pos = getPos(); + if (pos === undefined) return; + + this.editor + .chain() + .setNodeSelection(pos) + .updateAttributes(this.name, { + width: Math.round(el.offsetWidth), + height: Math.round(el.offsetHeight), + }) + .run(); + }, + onUpdate: (updatedNode, _decorations, _innerDecorations) => { + if (updatedNode.type !== currentNode.type) { + return false; + } + + if (updatedNode.attrs.src !== currentNode.attrs.src) { + el.src = updatedNode.attrs.src || ""; + } + + if (updatedNode.attrs.alt !== currentNode.attrs.alt) { + el.alt = updatedNode.attrs.alt || ""; + } + + const w = updatedNode.attrs.width; + const h = updatedNode.attrs.height; + if (w != null) { + el.style.width = `${w}px`; + } + if (h != null) { + el.style.height = `${h}px`; + } + + // Update alignment on container + const align = updatedNode.attrs.align || "center"; + const container = nodeView.dom as HTMLElement; + applyAlignment(container, align); + + currentNode = updatedNode; + return true; + }, + options: { + directions, + min: { + width: minWidth, + height: minHeight, + }, + preserveAspectRatio: alwaysPreserveAspectRatio === true, + createCustomHandle, + className, + }, + }); + + const dom = nodeView.dom as HTMLElement; + + // Apply initial alignment + applyAlignment(dom, node.attrs.align || "center"); + + // Handle percentage width backward compat + const widthAttr = node.attrs.width; + if (typeof widthAttr === "string" && widthAttr.endsWith("%")) { + // Defer conversion until we can measure the container + requestAnimationFrame(() => { + const parentEl = dom.parentElement; + if (parentEl) { + const containerWidth = parentEl.clientWidth; + const pctValue = parseInt(widthAttr, 10); + if (!isNaN(pctValue) && containerWidth > 0) { + const pxWidth = Math.round( + containerWidth * (pctValue / 100), + ); + el.style.width = `${pxWidth}px`; + if (node.attrs.aspectRatio) { + el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`; + } + } + } + dom.style.visibility = ""; + dom.style.pointerEvents = ""; + }); + } + + // Hide until image loads (official TipTap pattern) + dom.style.visibility = "hidden"; + dom.style.pointerEvents = "none"; + el.onload = () => { + dom.style.visibility = ""; + dom.style.pointerEvents = ""; + }; + + return nodeView; + }; }, }); + +function applyAlignment(container: HTMLElement, align: string) { + if (align === "left") { + container.style.justifyContent = "flex-start"; + } else if (align === "right") { + container.style.justifyContent = "flex-end"; + } else { + container.style.justifyContent = "center"; + } +} diff --git a/packages/editor-ext/src/lib/video/video.ts b/packages/editor-ext/src/lib/video/video.ts index c3c6ab3e..a296d13e 100644 --- a/packages/editor-ext/src/lib/video/video.ts +++ b/packages/editor-ext/src/lib/video/video.ts @@ -1,16 +1,35 @@ import { ReactNodeViewRenderer } from "@tiptap/react"; -import { Range, Node } from "@tiptap/core"; +import { Range, Node, mergeAttributes, ResizableNodeView } from "@tiptap/core"; +import type { ResizableNodeViewDirection } from "@tiptap/core"; + +export type VideoResizeOptions = { + enabled: boolean; + directions?: ResizableNodeViewDirection[]; + minWidth?: number; + minHeight?: number; + alwaysPreserveAspectRatio?: boolean; + createCustomHandle?: (direction: ResizableNodeViewDirection) => HTMLElement; + className?: { + container?: string; + wrapper?: string; + handle?: string; + resizing?: string; + }; +}; export interface VideoOptions { view: any; HTMLAttributes: Record; + resize: VideoResizeOptions | false; } + export interface VideoAttributes { src?: string; align?: string; attachmentId?: string; size?: number; - width?: number; + width?: number | string; + height?: number; aspectRatio?: number; placeholder?: { id: string; @@ -27,6 +46,7 @@ declare module "@tiptap/core" { ) => ReturnType; setVideoAlign: (align: "left" | "center" | "right") => ReturnType; setVideoWidth: (width: number) => ReturnType; + setVideoSize: (width: number, height: number) => ReturnType; }; } } @@ -44,6 +64,7 @@ export const TiptapVideo = Node.create({ return { view: null, HTMLAttributes: {}, + resize: false, }; }, @@ -64,12 +85,30 @@ export const TiptapVideo = Node.create({ }), }, width: { - default: "100%", - parseHTML: (element) => element.getAttribute("width"), + default: null, + parseHTML: (element) => { + const raw = element.getAttribute("width"); + if (!raw) return null; + if (raw.endsWith("%")) return raw; + const num = parseFloat(raw); + return isNaN(num) ? null : num; + }, renderHTML: (attributes: VideoAttributes) => ({ width: attributes.width, }), }, + height: { + default: null, + parseHTML: (element) => { + const raw = element.getAttribute("height"); + if (!raw) return null; + const num = parseFloat(raw); + return isNaN(num) ? null : num; + }, + renderHTML: (attributes: VideoAttributes) => ({ + height: attributes.height, + }), + }, size: { default: null, parseHTML: (element) => element.getAttribute("data-size"), @@ -136,13 +175,168 @@ export const TiptapVideo = Node.create({ commands.updateAttributes("video", { width: `${Math.max(0, Math.min(100, width))}%`, }), + + setVideoSize: + (width, height) => + ({ commands }) => + commands.updateAttributes("video", { width, height }), }; }, addNodeView() { - // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) - this.editor.isInitialized = true; + const resize = this.options.resize; - return ReactNodeViewRenderer(this.options.view); + if (!resize || !resize.enabled) { + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); + } + + const { + directions, + minWidth, + minHeight, + alwaysPreserveAspectRatio, + createCustomHandle, + className, + } = resize; + + return (props) => { + const { node, getPos, HTMLAttributes, editor } = props; + + if (!node.attrs.src) { + editor.isInitialized = true; + const reactView = ReactNodeViewRenderer(this.options.view); + const view = reactView(props); + + const originalUpdate = view.update?.bind(view); + view.update = (updatedNode, decorations, innerDecorations) => { + if (updatedNode.attrs.src && !node.attrs.src) { + return false; + } + if (originalUpdate) { + return originalUpdate(updatedNode, decorations, innerDecorations); + } + return true; + }; + + return view; + } + + const el = document.createElement("video"); + el.src = node.attrs.src; + el.controls = true; + el.preload = "metadata"; + el.style.display = "block"; + el.style.maxWidth = "100%"; + el.style.borderRadius = "8px"; + + let currentNode = node; + + const nodeView = new ResizableNodeView({ + element: el, + editor, + node, + getPos, + onResize: (w, h) => { + el.style.width = `${w}px`; + el.style.height = `${h}px`; + }, + onCommit: () => { + const pos = getPos(); + if (pos === undefined) return; + + this.editor + .chain() + .setNodeSelection(pos) + .updateAttributes(this.name, { + width: Math.round(el.offsetWidth), + height: Math.round(el.offsetHeight), + }) + .run(); + }, + onUpdate: (updatedNode, _decorations, _innerDecorations) => { + if (updatedNode.type !== currentNode.type) { + return false; + } + + if (updatedNode.attrs.src !== currentNode.attrs.src) { + el.src = updatedNode.attrs.src || ""; + } + + const w = updatedNode.attrs.width; + const h = updatedNode.attrs.height; + if (w != null) { + el.style.width = `${w}px`; + } + if (h != null) { + el.style.height = `${h}px`; + } + + const align = updatedNode.attrs.align || "center"; + const container = nodeView.dom as HTMLElement; + applyAlignment(container, align); + + currentNode = updatedNode; + return true; + }, + options: { + directions, + min: { + width: minWidth, + height: minHeight, + }, + preserveAspectRatio: alwaysPreserveAspectRatio === true, + createCustomHandle, + className, + }, + }); + + const dom = nodeView.dom as HTMLElement; + + applyAlignment(dom, node.attrs.align || "center"); + + // Handle percentage width backward compat + const widthAttr = node.attrs.width; + if (typeof widthAttr === "string" && widthAttr.endsWith("%")) { + requestAnimationFrame(() => { + const parentEl = dom.parentElement; + if (parentEl) { + const containerWidth = parentEl.clientWidth; + const pctValue = parseInt(widthAttr, 10); + if (!isNaN(pctValue) && containerWidth > 0) { + const pxWidth = Math.round( + containerWidth * (pctValue / 100), + ); + el.style.width = `${pxWidth}px`; + if (node.attrs.aspectRatio) { + el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`; + } + } + } + dom.style.visibility = ""; + dom.style.pointerEvents = ""; + }); + } + + // Hide until video metadata loads + dom.style.visibility = "hidden"; + dom.style.pointerEvents = "none"; + el.onloadedmetadata = () => { + dom.style.visibility = ""; + dom.style.pointerEvents = ""; + }; + + return nodeView; + }; }, }); + +function applyAlignment(container: HTMLElement, align: string) { + if (align === "left") { + container.style.justifyContent = "flex-start"; + } else if (align === "right") { + container.style.justifyContent = "flex-end"; + } else { + container.style.justifyContent = "center"; + } +} diff --git a/patches/@tiptap__core.patch b/patches/@tiptap__core.patch new file mode 100644 index 00000000..58f580c8 --- /dev/null +++ b/patches/@tiptap__core.patch @@ -0,0 +1,105 @@ +diff --git a/dist/index.cjs b/dist/index.cjs +index 01d6999642c5ae990083798a1bf0ef87068e4192..891b13c6901f28a6ab413c6dbae0ea726a76a196 100644 +--- a/dist/index.cjs ++++ b/dist/index.cjs +@@ -5463,7 +5463,10 @@ var ResizableNodeView = class { + this.container.classList.remove(this.classNames.resizing); + } + document.removeEventListener("mousemove", this.handleMouseMove); ++ document.removeEventListener("touchmove", this.handleTouchMove); + document.removeEventListener("mouseup", this.handleMouseUp); ++ document.removeEventListener("touchend", this.handleMouseUp); ++ window.removeEventListener("blur", this.handleMouseUp); + document.removeEventListener("keydown", this.handleKeyDown); + document.removeEventListener("keyup", this.handleKeyUp); + }; +@@ -5593,7 +5596,10 @@ var ResizableNodeView = class { + this.container.classList.remove(this.classNames.resizing); + } + document.removeEventListener("mousemove", this.handleMouseMove); ++ document.removeEventListener("touchmove", this.handleTouchMove); + document.removeEventListener("mouseup", this.handleMouseUp); ++ document.removeEventListener("touchend", this.handleMouseUp); ++ window.removeEventListener("blur", this.handleMouseUp); + document.removeEventListener("keydown", this.handleKeyDown); + document.removeEventListener("keyup", this.handleKeyUp); + this.isResizing = false; +@@ -5796,6 +5802,8 @@ var ResizableNodeView = class { + document.addEventListener("mousemove", this.handleMouseMove); + document.addEventListener("touchmove", this.handleTouchMove); + document.addEventListener("mouseup", this.handleMouseUp); ++ document.addEventListener("touchend", this.handleMouseUp); ++ window.addEventListener("blur", this.handleMouseUp); + document.addEventListener("keydown", this.handleKeyDown); + document.addEventListener("keyup", this.handleKeyUp); + } +diff --git a/dist/index.js b/dist/index.js +index 6f357a03b038abeb5ed86967b7fc7c3e5eb1d2d6..2d2742532860821984e1ba82625821504538ebbe 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -5330,7 +5330,10 @@ var ResizableNodeView = class { + this.container.classList.remove(this.classNames.resizing); + } + document.removeEventListener("mousemove", this.handleMouseMove); ++ document.removeEventListener("touchmove", this.handleTouchMove); + document.removeEventListener("mouseup", this.handleMouseUp); ++ document.removeEventListener("touchend", this.handleMouseUp); ++ window.removeEventListener("blur", this.handleMouseUp); + document.removeEventListener("keydown", this.handleKeyDown); + document.removeEventListener("keyup", this.handleKeyUp); + }; +@@ -5460,7 +5463,10 @@ var ResizableNodeView = class { + this.container.classList.remove(this.classNames.resizing); + } + document.removeEventListener("mousemove", this.handleMouseMove); ++ document.removeEventListener("touchmove", this.handleTouchMove); + document.removeEventListener("mouseup", this.handleMouseUp); ++ document.removeEventListener("touchend", this.handleMouseUp); ++ window.removeEventListener("blur", this.handleMouseUp); + document.removeEventListener("keydown", this.handleKeyDown); + document.removeEventListener("keyup", this.handleKeyUp); + this.isResizing = false; +@@ -5663,6 +5669,8 @@ var ResizableNodeView = class { + document.addEventListener("mousemove", this.handleMouseMove); + document.addEventListener("touchmove", this.handleTouchMove); + document.addEventListener("mouseup", this.handleMouseUp); ++ document.addEventListener("touchend", this.handleMouseUp); ++ window.addEventListener("blur", this.handleMouseUp); + document.addEventListener("keydown", this.handleKeyDown); + document.addEventListener("keyup", this.handleKeyUp); + } +diff --git a/src/lib/ResizableNodeView.ts b/src/lib/ResizableNodeView.ts +index f13e210b0aa46aefe7c31105deee3d2aa8a26cd5..9bac138dbf17c6ae6c3c129cbedb3a81bd39b60c 100644 +--- a/src/lib/ResizableNodeView.ts ++++ b/src/lib/ResizableNodeView.ts +@@ -523,7 +523,10 @@ export class ResizableNodeView { + } + + document.removeEventListener('mousemove', this.handleMouseMove) ++ document.removeEventListener('touchmove', this.handleTouchMove) + document.removeEventListener('mouseup', this.handleMouseUp) ++ document.removeEventListener('touchend', this.handleMouseUp) ++ window.removeEventListener('blur', this.handleMouseUp) + document.removeEventListener('keydown', this.handleKeyDown) + document.removeEventListener('keyup', this.handleKeyUp) + this.isResizing = false +@@ -774,6 +777,8 @@ export class ResizableNodeView { + document.addEventListener('mousemove', this.handleMouseMove) + document.addEventListener('touchmove', this.handleTouchMove) + document.addEventListener('mouseup', this.handleMouseUp) ++ document.addEventListener('touchend', this.handleMouseUp) ++ window.addEventListener('blur', this.handleMouseUp) + document.addEventListener('keydown', this.handleKeyDown) + document.addEventListener('keyup', this.handleKeyUp) + } +@@ -859,7 +864,10 @@ export class ResizableNodeView { + + // Clean up document-level listeners + document.removeEventListener('mousemove', this.handleMouseMove) ++ document.removeEventListener('touchmove', this.handleTouchMove) + document.removeEventListener('mouseup', this.handleMouseUp) ++ document.removeEventListener('touchend', this.handleMouseUp) ++ window.removeEventListener('blur', this.handleMouseUp) + document.removeEventListener('keydown', this.handleKeyDown) + document.removeEventListener('keyup', this.handleKeyUp) + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 161aa6f1..73141a72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ overrides: '@tiptap/extension-code': 3.17.1 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 @@ -57,7 +60,7 @@ importers: version: 3.4.4(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) '@hocuspocus/transformer': specifier: 3.4.4 - version: 3.4.4(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(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.29))(yjs@13.6.29))(yjs@13.6.29) + version: 3.4.4(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(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.29))(yjs@13.6.29))(yjs@13.6.29) '@joplin/turndown': specifier: ^4.0.74 version: 4.0.74 @@ -69,85 +72,85 @@ importers: version: 1.1.0 '@tiptap/core': specifier: 3.17.1 - version: 3.17.1(@tiptap/pm@3.17.1) + version: 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/extension-code-block': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-collaboration': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.29))(yjs@13.6.29))(yjs@13.6.29) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.29))(yjs@13.6.29))(yjs@13.6.29) '@tiptap/extension-collaboration-caret': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.29))(yjs@13.6.29)) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.29))(yjs@13.6.29)) '@tiptap/extension-color': specifier: 3.17.1 - version: 3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))) + version: 3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))) '@tiptap/extension-document': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) '@tiptap/extension-heading': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) '@tiptap/extension-highlight': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) '@tiptap/extension-history': specifier: 3.17.1 - version: 3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) '@tiptap/extension-image': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) '@tiptap/extension-link': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-list': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-placeholder': specifier: 3.17.1 - version: 3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) '@tiptap/extension-subscript': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-superscript': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-table': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-text': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) '@tiptap/extension-text-align': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) '@tiptap/extension-text-style': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) '@tiptap/extension-typography': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) '@tiptap/extension-unique-id': specifier: ^3.17.1 - version: 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + version: 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-youtube': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) '@tiptap/html': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0) '@tiptap/pm': specifier: 3.17.1 version: 3.17.1 '@tiptap/react': specifier: 3.17.1 - version: 3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.17.1 version: 3.17.1 '@tiptap/suggestion': specifier: 3.17.1 - version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@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.29))(yjs@13.6.29) @@ -12332,9 +12335,9 @@ snapshots: - bufferutil - utf-8-validate - '@hocuspocus/transformer@3.4.4(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(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.29))(yjs@13.6.29))(yjs@13.6.29)': + '@hocuspocus/transformer@3.4.4(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(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.29))(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 '@tiptap/starter-kit': 3.17.1 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.29))(yjs@13.6.29) @@ -14895,191 +14898,191 @@ snapshots: '@tanstack/query-core': 5.90.17 react: 18.3.1 - '@tiptap/core@3.17.1(@tiptap/pm@3.17.1)': + '@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)': dependencies: '@tiptap/pm': 3.17.1 - '@tiptap/extension-blockquote@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-blockquote@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-bold@3.17.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-bold@3.17.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-bubble-menu@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extension-bubble-menu@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: '@floating-ui/dom': 1.7.4 - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 optional: true - '@tiptap/extension-bullet-list@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': + '@tiptap/extension-bullet-list@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-code-block@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extension-code-block@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 - '@tiptap/extension-code@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-code@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-collaboration-caret@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.29))(yjs@13.6.29))': + '@tiptap/extension-collaboration-caret@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.29))(yjs@13.6.29))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 '@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.29))(yjs@13.6.29) - '@tiptap/extension-collaboration@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.29))(yjs@13.6.29))(yjs@13.6.29)': + '@tiptap/extension-collaboration@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.29))(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 '@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.29))(yjs@13.6.29) yjs: 13.6.29 - '@tiptap/extension-color@3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)))': + '@tiptap/extension-color@3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)))': dependencies: - '@tiptap/extension-text-style': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-text-style': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) - '@tiptap/extension-document@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-document@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-dropcursor@3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': + '@tiptap/extension-dropcursor@3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-floating-menu@3.19.0(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extension-floating-menu@3.19.0(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: '@floating-ui/dom': 1.7.3 - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 optional: true - '@tiptap/extension-gapcursor@3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': + '@tiptap/extension-gapcursor@3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-hard-break@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-hard-break@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-heading@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-heading@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-highlight@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-highlight@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-history@3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': + '@tiptap/extension-history@3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-horizontal-rule@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extension-horizontal-rule@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 - '@tiptap/extension-image@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-image@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-italic@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-italic@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-link@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extension-link@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 linkifyjs: 4.3.2 - '@tiptap/extension-list-item@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': + '@tiptap/extension-list-item@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-list-keymap@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': + '@tiptap/extension-list-keymap@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 - '@tiptap/extension-ordered-list@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': + '@tiptap/extension-ordered-list@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-paragraph@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-paragraph@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-placeholder@3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': + '@tiptap/extension-placeholder@3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-strike@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-strike@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-subscript@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extension-subscript@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 - '@tiptap/extension-superscript@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extension-superscript@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 - '@tiptap/extension-table@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extension-table@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 - '@tiptap/extension-text-align@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-text-align@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-text@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-text@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-typography@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-typography@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-underline@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-underline@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extension-unique-id@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extension-unique-id@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 uuid: 10.0.0 - '@tiptap/extension-youtube@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': + '@tiptap/extension-youtube@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) - '@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 - '@tiptap/html@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0)': + '@tiptap/html@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 happy-dom: 20.1.0 @@ -15104,9 +15107,9 @@ snapshots: prosemirror-transform: 1.10.4 prosemirror-view: 1.40.0 - '@tiptap/react@3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@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.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 '@types/react': 18.3.12 '@types/react-dom': 18.3.1 @@ -15116,41 +15119,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.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-floating-menu': 3.19.0(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-bubble-menu': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-floating-menu': 3.19.0(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) transitivePeerDependencies: - '@floating-ui/dom' '@tiptap/starter-kit@3.17.1': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-blockquote': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-bold': 3.17.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-bullet-list': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) - '@tiptap/extension-code': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-code-block': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-document': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-dropcursor': 3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) - '@tiptap/extension-gapcursor': 3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) - '@tiptap/extension-hard-break': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-heading': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-horizontal-rule': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-italic': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-link': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-list-item': 3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) - '@tiptap/extension-list-keymap': 3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) - '@tiptap/extension-ordered-list': 3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) - '@tiptap/extension-paragraph': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-strike': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-text': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-underline': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) + '@tiptap/extension-blockquote': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extension-bold': 3.17.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extension-bullet-list': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-code': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extension-code-block': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-document': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extension-dropcursor': 3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-gapcursor': 3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-hard-break': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extension-heading': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extension-horizontal-rule': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-italic': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extension-link': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list-item': 3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-list-keymap': 3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-ordered-list': 3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-paragraph': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extension-strike': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extension-text': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extension-underline': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)) + '@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 - '@tiptap/suggestion@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': + '@tiptap/suggestion@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1) '@tiptap/pm': 3.17.1 '@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.29))(yjs@13.6.29)':