diff --git a/.vscode/settings.json b/.vscode/settings.json index 8e9e171..c5e3393 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -64,8 +64,10 @@ "hyoban", "lucide", "ofetch", + "openapi", "sonner", "tanstack", + "tiptap", "tsup" ], "typescript.preferences.autoImportFileExcludePatterns": ["react-day-picker"], diff --git a/apps/admin/package.json b/apps/admin/package.json index c0b62de..8c954ca 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -31,9 +31,11 @@ "@hookform/resolvers": "^3.9.1", "@radix-ui/react-icons": "^1.3.2", "@repo/pro-table": "workspace:*", + "@repo/tiptap": "workspace:*", "@repo/ui": "workspace:*", "@tanstack/react-query": "^5.59.17", "@tanstack/react-table": "^8.20.5", + "@tiptap/react": "^2.10.3", "clsx": "^2.1.1", "date-fns": "^2.30.0", "dotenv": "^16.4.5", diff --git a/apps/admin/src/main.tsx b/apps/admin/src/main.tsx index a97fccb..993943f 100644 --- a/apps/admin/src/main.tsx +++ b/apps/admin/src/main.tsx @@ -1,4 +1,5 @@ import "@repo/ui/globals.css" +import "@repo/tiptap/tiptap.css" import "./i18n" import { env } from "@env" diff --git a/apps/admin/src/pages/(external)/playground/tiptap.tsx b/apps/admin/src/pages/(external)/playground/tiptap.tsx new file mode 100644 index 0000000..d71eb11 --- /dev/null +++ b/apps/admin/src/pages/(external)/playground/tiptap.tsx @@ -0,0 +1,21 @@ +import { MinimalTiptapEditor } from "@repo/tiptap/minimal-tiptap" +import type { Content } from "@tiptap/react" +import { useState } from "react" + +export function Component() { + const [value, setValue] = useState("") + + return ( + + ) +} diff --git a/packages/tiptap/README.md b/packages/tiptap/README.md new file mode 100644 index 0000000..a7ebc83 --- /dev/null +++ b/packages/tiptap/README.md @@ -0,0 +1,7 @@ +# shadcn-minimal-tiptap + +This package is a secondary development based on [shadcn-minimal-tiptap](https://github.com/Aslam97/shadcn-minimal-tiptap). It extends and enhances the functionality of the original project while maintaining its core features. + +## About Original Project + +The original [shadcn-minimal-tiptap](https://github.com/Aslam97/shadcn-minimal-tiptap) is a minimal implementation of Tiptap editor with shadcn/ui components. diff --git a/packages/tiptap/package.json b/packages/tiptap/package.json new file mode 100644 index 0000000..22a392f --- /dev/null +++ b/packages/tiptap/package.json @@ -0,0 +1,39 @@ +{ + "name": "@repo/tiptap", + "version": "1.0.0", + "description": "", + "author": "", + "license": "ISC", + "keywords": [], + "exports": { + "./minimal-tiptap": "./src/minimal-tiptap.tsx", + "./minimal-tiptap-one": "./src/minimal-tiptap-one.tsx", + "./minimal-tiptap-three": "./src/minimal-tiptap-three.tsx", + "./tiptap.css": "./src/styles/index.css" + }, + "dependencies": { + "@radix-ui/react-icons": "^1.3.2", + "@repo/ui": "workspace:*", + "@tiptap/extension-code-block-lowlight": "^2.10.3", + "@tiptap/extension-color": "^2.10.3", + "@tiptap/extension-heading": "^2.10.3", + "@tiptap/extension-horizontal-rule": "^2.10.3", + "@tiptap/extension-image": "^2.10.3", + "@tiptap/extension-link": "^2.10.3", + "@tiptap/extension-placeholder": "^2.10.3", + "@tiptap/extension-text-style": "^2.10.3", + "@tiptap/extension-typography": "^2.10.3", + "@tiptap/extension-underline": "^2.10.3", + "@tiptap/pm": "^2.10.3", + "@tiptap/react": "^2.10.3", + "@tiptap/starter-kit": "^2.10.3", + "@types/react": "^18.3.12", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lowlight": "^3.2.0", + "react": "^18.3.1", + "react-medium-image-zoom": "^5.2.11", + "sonner": "^1.6.1", + "tailwind-merge": "^2.5.4" + } +} diff --git a/packages/tiptap/src/components/bubble-menu/link-bubble-menu.tsx b/packages/tiptap/src/components/bubble-menu/link-bubble-menu.tsx new file mode 100644 index 0000000..8c9295d --- /dev/null +++ b/packages/tiptap/src/components/bubble-menu/link-bubble-menu.tsx @@ -0,0 +1,107 @@ +import type { Editor } from "@tiptap/react" +import { BubbleMenu } from "@tiptap/react" +import * as React from "react" + +import type { ShouldShowProps } from "../../types" +import { LinkEditBlock } from "../link/link-edit-block" +import { LinkPopoverBlock } from "../link/link-popover-block" + +interface LinkBubbleMenuProps { + editor: Editor +} + +interface LinkAttributes { + href: string + target: string +} + +export const LinkBubbleMenu: React.FC = ({ editor }) => { + const [showEdit, setShowEdit] = React.useState(false) + const [linkAttrs, setLinkAttrs] = React.useState({ href: "", target: "" }) + const [selectedText, setSelectedText] = React.useState("") + + const updateLinkState = React.useCallback(() => { + const { from, to } = editor.state.selection + const { href, target } = editor.getAttributes("link") + const text = editor.state.doc.textBetween(from, to, " ") + + setLinkAttrs({ href, target }) + setSelectedText(text) + }, [editor]) + + const shouldShow = React.useCallback( + ({ editor, from, to }: ShouldShowProps) => { + if (from === to) { + return false + } + const { href } = editor.getAttributes("link") + + if (href) { + updateLinkState() + return true + } + return false + }, + [updateLinkState], + ) + + const handleEdit = React.useCallback(() => { + setShowEdit(true) + }, []) + + const onSetLink = React.useCallback( + (url: string, text?: string, openInNewTab?: boolean) => { + editor + .chain() + .focus() + .extendMarkRange("link") + .insertContent({ + type: "text", + text: text || url, + marks: [ + { + type: "link", + attrs: { + href: url, + target: openInNewTab ? "_blank" : "", + }, + }, + ], + }) + .setLink({ href: url, target: openInNewTab ? "_blank" : "" }) + .run() + setShowEdit(false) + updateLinkState() + }, + [editor, updateLinkState], + ) + + const onUnsetLink = React.useCallback(() => { + editor.chain().focus().extendMarkRange("link").unsetLink().run() + setShowEdit(false) + updateLinkState() + }, [editor, updateLinkState]) + + return ( + setShowEdit(false), + }} + > + {showEdit ? ( + + ) : ( + + )} + + ) +} diff --git a/packages/tiptap/src/components/image/image-edit-block.tsx b/packages/tiptap/src/components/image/image-edit-block.tsx new file mode 100644 index 0000000..6643a14 --- /dev/null +++ b/packages/tiptap/src/components/image/image-edit-block.tsx @@ -0,0 +1,83 @@ +import type { Editor } from "@tiptap/react" +import * as React from "react" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" + +interface ImageEditBlockProps { + editor: Editor + close: () => void +} + +export const ImageEditBlock: React.FC = ({ editor, close }) => { + const fileInputRef = React.useRef(null) + const [link, setLink] = React.useState("") + + const handleClick = React.useCallback(() => { + fileInputRef.current?.click() + }, []) + + const handleFile = React.useCallback( + async (e: React.ChangeEvent) => { + const { files } = e.target + if (!files?.length) return + + const insertImages = async () => { + const contentBucket = [] + const filesArray = Array.from(files) + + for (const file of filesArray) { + contentBucket.push({ src: file }) + } + + editor.commands.setImages(contentBucket) + } + + await insertImages() + close() + }, + [editor, close], + ) + + const handleSubmit = React.useCallback( + (e: React.FormEvent) => { + e.preventDefault() + e.stopPropagation() + + if (link) { + editor.commands.setImages([{ src: link }]) + close() + } + }, + [editor, link, close], + ) + + return ( +
+
+ +
+ ) => setLink(e.target.value)} + /> + +
+
+ + +
+ ) +} + +export default ImageEditBlock diff --git a/packages/tiptap/src/components/image/image-edit-dialog.tsx b/packages/tiptap/src/components/image/image-edit-dialog.tsx new file mode 100644 index 0000000..518c8b7 --- /dev/null +++ b/packages/tiptap/src/components/image/image-edit-dialog.tsx @@ -0,0 +1,50 @@ +import { ImageIcon } from "@radix-ui/react-icons" +import type { Editor } from "@tiptap/react" +import type { VariantProps } from "class-variance-authority" +import { useState } from "react" + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import type { toggleVariants } from "@/components/ui/toggle" + +import { ToolbarButton } from "../toolbar-button" +import { ImageEditBlock } from "./image-edit-block" + +interface ImageEditDialogProps extends VariantProps { + editor: Editor +} + +const ImageEditDialog = ({ editor, size, variant }: ImageEditDialogProps) => { + const [open, setOpen] = useState(false) + + return ( + + + + + + + + + Select image + Upload an image from your computer + + setOpen(false)} /> + + + ) +} + +export { ImageEditDialog } diff --git a/packages/tiptap/src/components/link/link-edit-block.tsx b/packages/tiptap/src/components/link/link-edit-block.tsx new file mode 100644 index 0000000..dde3881 --- /dev/null +++ b/packages/tiptap/src/components/link/link-edit-block.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { cn } from "@/lib/utils" + +export interface LinkEditorProps extends React.HTMLAttributes { + defaultUrl?: string + defaultText?: string + defaultIsNewTab?: boolean + onSave: (url: string, text?: string, isNewTab?: boolean) => void +} + +export const LinkEditBlock = React.forwardRef( + ({ onSave, defaultIsNewTab, defaultUrl, defaultText, className }, ref) => { + const formRef = React.useRef(null) + const [url, setUrl] = React.useState(defaultUrl || "") + const [text, setText] = React.useState(defaultText || "") + const [isNewTab, setIsNewTab] = React.useState(defaultIsNewTab || false) + + const handleSave = React.useCallback( + (e: React.FormEvent) => { + e.preventDefault() + if (formRef.current) { + const isValid = Array.from(formRef.current.querySelectorAll("input")).every((input) => input.checkValidity()) + + if (isValid) { + onSave(url, text, isNewTab) + } else { + formRef.current.querySelectorAll("input").forEach((input) => { + if (!input.checkValidity()) { + input.reportValidity() + } + }) + } + } + }, + [onSave, url, text, isNewTab], + ) + + React.useImperativeHandle(ref, () => formRef.current as HTMLDivElement) + + return ( +
+
+
+ + setUrl(e.target.value)} /> +
+ +
+ + setText(e.target.value)} /> +
+ +
+ + +
+ +
+ +
+
+
+ ) + }, +) + +LinkEditBlock.displayName = "LinkEditBlock" + +export default LinkEditBlock diff --git a/packages/tiptap/src/components/link/link-edit-popover.tsx b/packages/tiptap/src/components/link/link-edit-popover.tsx new file mode 100644 index 0000000..b1bfe92 --- /dev/null +++ b/packages/tiptap/src/components/link/link-edit-popover.tsx @@ -0,0 +1,70 @@ +import { Link2Icon } from "@radix-ui/react-icons" +import type { Editor } from "@tiptap/react" +import type { VariantProps } from "class-variance-authority" +import * as React from "react" + +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import type { toggleVariants } from "@/components/ui/toggle" + +import { ToolbarButton } from "../toolbar-button" +import { LinkEditBlock } from "./link-edit-block" + +interface LinkEditPopoverProps extends VariantProps { + editor: Editor +} + +const LinkEditPopover = ({ editor, size, variant }: LinkEditPopoverProps) => { + const [open, setOpen] = React.useState(false) + + const { from, to } = editor.state.selection + const text = editor.state.doc.textBetween(from, to, " ") + + const onSetLink = React.useCallback( + (url: string, text?: string, openInNewTab?: boolean) => { + editor + .chain() + .focus() + .extendMarkRange("link") + .insertContent({ + type: "text", + text: text || url, + marks: [ + { + type: "link", + attrs: { + href: url, + target: openInNewTab ? "_blank" : "", + }, + }, + ], + }) + .setLink({ href: url }) + .run() + + editor.commands.enter() + }, + [editor], + ) + + return ( + + + + + + + + + + + ) +} + +export { LinkEditPopover } diff --git a/packages/tiptap/src/components/link/link-popover-block.tsx b/packages/tiptap/src/components/link/link-popover-block.tsx new file mode 100644 index 0000000..1970638 --- /dev/null +++ b/packages/tiptap/src/components/link/link-popover-block.tsx @@ -0,0 +1,64 @@ +import { CopyIcon, ExternalLinkIcon, LinkBreak2Icon } from "@radix-ui/react-icons" +import * as React from "react" + +import { Separator } from "@/components/ui/separator" + +import { ToolbarButton } from "../toolbar-button" + +interface LinkPopoverBlockProps { + url: string + onClear: () => void + onEdit: (e: React.MouseEvent) => void +} + +export const LinkPopoverBlock: React.FC = ({ url, onClear, onEdit }) => { + const [copyTitle, setCopyTitle] = React.useState("Copy") + + const handleCopy = React.useCallback( + (e: React.MouseEvent) => { + e.preventDefault() + navigator.clipboard + .writeText(url) + .then(() => { + setCopyTitle("Copied!") + setTimeout(() => setCopyTitle("Copy"), 1000) + }) + .catch(console.error) + }, + [url], + ) + + const handleOpenLink = React.useCallback(() => { + window.open(url, "_blank", "noopener,noreferrer") + }, [url]) + + return ( +
+
+ + Edit link + + + + + + + + + + + { + if (e.target === e.currentTarget) e.preventDefault() + }, + }} + > + + +
+
+ ) +} diff --git a/packages/tiptap/src/components/measured-container.tsx b/packages/tiptap/src/components/measured-container.tsx new file mode 100644 index 0000000..a6affbb --- /dev/null +++ b/packages/tiptap/src/components/measured-container.tsx @@ -0,0 +1,34 @@ +import * as React from "react" + +import { useContainerSize } from "../hooks/use-container-size" + +interface MeasuredContainerProps { + as: T + name: string + children?: React.ReactNode +} + +export const MeasuredContainer = React.forwardRef( + ( + { as: Component, name, children, style = {}, ...props }: MeasuredContainerProps & React.ComponentProps, + ref: React.Ref, + ) => { + const innerRef = React.useRef(null) + const rect = useContainerSize(innerRef.current) + + React.useImperativeHandle(ref, () => innerRef.current as HTMLElement) + + const customStyle = { + [`--${name}-width`]: `${rect.width}px`, + [`--${name}-height`]: `${rect.height}px`, + } + + return ( + + {children} + + ) + }, +) + +MeasuredContainer.displayName = "MeasuredContainer" diff --git a/packages/tiptap/src/components/section/five.tsx b/packages/tiptap/src/components/section/five.tsx new file mode 100644 index 0000000..21b7b88 --- /dev/null +++ b/packages/tiptap/src/components/section/five.tsx @@ -0,0 +1,86 @@ +import { CaretDownIcon, CodeIcon, DividerHorizontalIcon, PlusIcon, QuoteIcon } from "@radix-ui/react-icons" +import type { Editor } from "@tiptap/react" +import type { VariantProps } from "class-variance-authority" +import React from "react" + +import type { toggleVariants } from "@/components/ui/toggle" + +import type { FormatAction } from "../../types" +import { ImageEditDialog } from "../image/image-edit-dialog" +import { LinkEditPopover } from "../link/link-edit-popover" +import { ToolbarSection } from "../toolbar-section" + +type InsertElementAction = "codeBlock" | "blockquote" | "horizontalRule" +interface InsertElement extends FormatAction { + value: InsertElementAction +} + +const formatActions: InsertElement[] = [ + { + value: "codeBlock", + label: "Code block", + icon: , + action: (editor) => editor.chain().focus().toggleCodeBlock().run(), + isActive: (editor) => editor.isActive("codeBlock"), + canExecute: (editor) => editor.can().chain().focus().toggleCodeBlock().run(), + shortcuts: ["mod", "alt", "C"], + }, + { + value: "blockquote", + label: "Blockquote", + icon: , + action: (editor) => editor.chain().focus().toggleBlockquote().run(), + isActive: (editor) => editor.isActive("blockquote"), + canExecute: (editor) => editor.can().chain().focus().toggleBlockquote().run(), + shortcuts: ["mod", "shift", "B"], + }, + { + value: "horizontalRule", + label: "Divider", + icon: , + action: (editor) => editor.chain().focus().setHorizontalRule().run(), + isActive: () => false, + canExecute: (editor) => editor.can().chain().focus().setHorizontalRule().run(), + shortcuts: ["mod", "alt", "-"], + }, +] + +interface SectionFiveProps extends VariantProps { + editor: Editor + activeActions?: InsertElementAction[] + mainActionCount?: number +} + +export const SectionFive = ({ + editor, + activeActions = formatActions.map((action) => action.value), + mainActionCount = 0, + size, + variant, +}: SectionFiveProps) => { + return ( + <> + + + + + + + )} + dropdownTooltip="Insert elements" + size={size} + variant={variant} + /> + + ) +} + +SectionFive.displayName = "SectionFive" + +export default SectionFive diff --git a/packages/tiptap/src/components/section/four.tsx b/packages/tiptap/src/components/section/four.tsx new file mode 100644 index 0000000..5896d88 --- /dev/null +++ b/packages/tiptap/src/components/section/four.tsx @@ -0,0 +1,75 @@ +import { CaretDownIcon, ListBulletIcon } from "@radix-ui/react-icons" +import type { Editor } from "@tiptap/react" +import type { VariantProps } from "class-variance-authority" +import * as React from "react" + +import type { toggleVariants } from "@/components/ui/toggle" + +import type { FormatAction } from "../../types" +import { ToolbarSection } from "../toolbar-section" + +type ListItemAction = "orderedList" | "bulletList" +interface ListItem extends FormatAction { + value: ListItemAction +} + +const formatActions: ListItem[] = [ + { + value: "orderedList", + label: "Numbered list", + icon: ( + + + + ), + isActive: (editor) => editor.isActive("orderedList"), + action: (editor) => editor.chain().focus().toggleOrderedList().run(), + canExecute: (editor) => editor.can().chain().focus().toggleOrderedList().run(), + shortcuts: ["mod", "shift", "7"], + }, + { + value: "bulletList", + label: "Bullet list", + icon: , + isActive: (editor) => editor.isActive("bulletList"), + action: (editor) => editor.chain().focus().toggleBulletList().run(), + canExecute: (editor) => editor.can().chain().focus().toggleBulletList().run(), + shortcuts: ["mod", "shift", "8"], + }, +] + +interface SectionFourProps extends VariantProps { + editor: Editor + activeActions?: ListItemAction[] + mainActionCount?: number +} + +export const SectionFour: React.FC = ({ + editor, + activeActions = formatActions.map((action) => action.value), + mainActionCount = 0, + size, + variant, +}) => { + return ( + + + + + )} + dropdownTooltip="Lists" + size={size} + variant={variant} + /> + ) +} + +SectionFour.displayName = "SectionFour" + +export default SectionFour diff --git a/packages/tiptap/src/components/section/one.tsx b/packages/tiptap/src/components/section/one.tsx new file mode 100644 index 0000000..236bb24 --- /dev/null +++ b/packages/tiptap/src/components/section/one.tsx @@ -0,0 +1,139 @@ +import { CaretDownIcon, LetterCaseCapitalizeIcon } from "@radix-ui/react-icons" +import type { Level } from "@tiptap/extension-heading" +import type { Editor } from "@tiptap/react" +import type { VariantProps } from "class-variance-authority" +import * as React from "react" + +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import type { toggleVariants } from "@/components/ui/toggle" +import { cn } from "@/lib/utils" + +import type { FormatAction } from "../../types" +import { ShortcutKey } from "../shortcut-key" +import { ToolbarButton } from "../toolbar-button" + +interface TextStyle extends Omit { + element: keyof JSX.IntrinsicElements + level?: Level + className: string +} + +const formatActions: TextStyle[] = [ + { + label: "Normal Text", + element: "span", + className: "grow", + shortcuts: ["mod", "alt", "0"], + }, + { + label: "Heading 1", + element: "h1", + level: 1, + className: "m-0 grow text-3xl font-extrabold", + shortcuts: ["mod", "alt", "1"], + }, + { + label: "Heading 2", + element: "h2", + level: 2, + className: "m-0 grow text-xl font-bold", + shortcuts: ["mod", "alt", "2"], + }, + { + label: "Heading 3", + element: "h3", + level: 3, + className: "m-0 grow text-lg font-semibold", + shortcuts: ["mod", "alt", "3"], + }, + { + label: "Heading 4", + element: "h4", + level: 4, + className: "m-0 grow text-base font-semibold", + shortcuts: ["mod", "alt", "4"], + }, + { + label: "Heading 5", + element: "h5", + level: 5, + className: "m-0 grow text-sm font-normal", + shortcuts: ["mod", "alt", "5"], + }, + { + label: "Heading 6", + element: "h6", + level: 6, + className: "m-0 grow text-sm font-normal", + shortcuts: ["mod", "alt", "6"], + }, +] + +interface SectionOneProps extends VariantProps { + editor: Editor + activeLevels?: Level[] +} + +export const SectionOne: React.FC = React.memo( + ({ editor, activeLevels = [1, 2, 3, 4, 5, 6], size, variant }) => { + const filteredActions = React.useMemo( + () => formatActions.filter((action) => !action.level || activeLevels.includes(action.level)), + [activeLevels], + ) + + const handleStyleChange = React.useCallback( + (level?: Level) => { + if (level) { + editor.chain().focus().toggleHeading({ level }).run() + } else { + editor.chain().focus().setParagraph().run() + } + }, + [editor], + ) + + const renderMenuItem = React.useCallback( + ({ label, element: Element, level, className, shortcuts }: TextStyle) => ( + handleStyleChange(level)} + className={cn("flex flex-row items-center justify-between gap-4", { + "bg-accent": level ? editor.isActive("heading", { level }) : editor.isActive("paragraph"), + })} + aria-label={label} + > + {label} + + + ), + [editor, handleStyleChange], + ) + + return ( + + + + + + + + + {filteredActions.map((element) => renderMenuItem(element))} + + + ) + }, +) + +SectionOne.displayName = "SectionOne" + +export default SectionOne diff --git a/packages/tiptap/src/components/section/three.tsx b/packages/tiptap/src/components/section/three.tsx new file mode 100644 index 0000000..abac7ae --- /dev/null +++ b/packages/tiptap/src/components/section/three.tsx @@ -0,0 +1,194 @@ +import { CaretDownIcon, CheckIcon } from "@radix-ui/react-icons" +import type { Editor } from "@tiptap/react" +import type { VariantProps } from "class-variance-authority" +import * as React from "react" + +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import type { toggleVariants } from "@/components/ui/toggle" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" + +import { useTheme } from "../../hooks/use-theme" +import { ToolbarButton } from "../toolbar-button" + +interface ColorItem { + cssVar: string + label: string + darkLabel?: string +} + +interface ColorPalette { + label: string + colors: ColorItem[] + inverse: string +} + +const COLORS: ColorPalette[] = [ + { + label: "Palette 1", + inverse: "hsl(var(--background))", + colors: [ + { cssVar: "hsl(var(--foreground))", label: "Default" }, + { cssVar: "var(--mt-accent-bold-blue)", label: "Bold blue" }, + { cssVar: "var(--mt-accent-bold-teal)", label: "Bold teal" }, + { cssVar: "var(--mt-accent-bold-green)", label: "Bold green" }, + { cssVar: "var(--mt-accent-bold-orange)", label: "Bold orange" }, + { cssVar: "var(--mt-accent-bold-red)", label: "Bold red" }, + { cssVar: "var(--mt-accent-bold-purple)", label: "Bold purple" }, + ], + }, + { + label: "Palette 2", + inverse: "hsl(var(--background))", + colors: [ + { cssVar: "var(--mt-accent-gray)", label: "Gray" }, + { cssVar: "var(--mt-accent-blue)", label: "Blue" }, + { cssVar: "var(--mt-accent-teal)", label: "Teal" }, + { cssVar: "var(--mt-accent-green)", label: "Green" }, + { cssVar: "var(--mt-accent-orange)", label: "Orange" }, + { cssVar: "var(--mt-accent-red)", label: "Red" }, + { cssVar: "var(--mt-accent-purple)", label: "Purple" }, + ], + }, + { + label: "Palette 3", + inverse: "hsl(var(--foreground))", + colors: [ + { cssVar: "hsl(var(--background))", label: "White", darkLabel: "Black" }, + { cssVar: "var(--mt-accent-blue-subtler)", label: "Blue subtle" }, + { cssVar: "var(--mt-accent-teal-subtler)", label: "Teal subtle" }, + { cssVar: "var(--mt-accent-green-subtler)", label: "Green subtle" }, + { cssVar: "var(--mt-accent-yellow-subtler)", label: "Yellow subtle" }, + { cssVar: "var(--mt-accent-red-subtler)", label: "Red subtle" }, + { cssVar: "var(--mt-accent-purple-subtler)", label: "Purple subtle" }, + ], + }, +] + +const MemoizedColorButton = React.memo<{ + color: ColorItem + isSelected: boolean + inverse: string + onClick: (value: string) => void +}>(({ color, isSelected, inverse, onClick }) => { + const isDarkMode = useTheme() + const label = isDarkMode && color.darkLabel ? color.darkLabel : color.label + + return ( + + + ) => { + e.preventDefault() + onClick(color.cssVar) + }} + > + {isSelected && } + + + +

{label}

+
+
+ ) + }) + +MemoizedColorButton.displayName = "MemoizedColorButton" + +const MemoizedColorPicker = React.memo<{ + palette: ColorPalette + selectedColor: string + inverse: string + onColorChange: (value: string) => void +}>(({ palette, selectedColor, inverse, onColorChange }) => ( + { + if (value) onColorChange(value) + }} + className="gap-1.5" + > + {palette.colors.map((color, index) => ( + + ))} + + )) + +MemoizedColorPicker.displayName = "MemoizedColorPicker" + +interface SectionThreeProps extends VariantProps { + editor: Editor +} + +export const SectionThree: React.FC = ({ editor, size, variant }) => { + const color = editor.getAttributes("textStyle")?.color || "hsl(var(--foreground))" + const [selectedColor, setSelectedColor] = React.useState(color) + + const handleColorChange = React.useCallback( + (value: string) => { + setSelectedColor(value) + editor.chain().setColor(value).run() + }, + [editor], + ) + + React.useEffect(() => { + setSelectedColor(color) + }, [color]) + + return ( + + + + + + + + + + + + +
+ {COLORS.map((palette, index) => ( + + ))} +
+
+
+ ) +} + +SectionThree.displayName = "SectionThree" + +export default SectionThree diff --git a/packages/tiptap/src/components/section/two.tsx b/packages/tiptap/src/components/section/two.tsx new file mode 100644 index 0000000..8a9292e --- /dev/null +++ b/packages/tiptap/src/components/section/two.tsx @@ -0,0 +1,112 @@ +import { + CodeIcon, + DotsHorizontalIcon, + FontBoldIcon, + FontItalicIcon, + StrikethroughIcon, + TextNoneIcon, + UnderlineIcon, +} from "@radix-ui/react-icons" +import type { Editor } from "@tiptap/react" +import type { VariantProps } from "class-variance-authority" +import * as React from "react" + +import type { toggleVariants } from "@/components/ui/toggle" + +import type { FormatAction } from "../../types" +import { ToolbarSection } from "../toolbar-section" + +type TextStyleAction = "bold" | "italic" | "underline" | "strikethrough" | "code" | "clearFormatting" + +interface TextStyle extends FormatAction { + value: TextStyleAction +} + +const formatActions: TextStyle[] = [ + { + value: "bold", + label: "Bold", + icon: , + action: (editor) => editor.chain().focus().toggleBold().run(), + isActive: (editor) => editor.isActive("bold"), + canExecute: (editor) => editor.can().chain().focus().toggleBold().run() && !editor.isActive("codeBlock"), + shortcuts: ["mod", "B"], + }, + { + value: "italic", + label: "Italic", + icon: , + action: (editor) => editor.chain().focus().toggleItalic().run(), + isActive: (editor) => editor.isActive("italic"), + canExecute: (editor) => editor.can().chain().focus().toggleItalic().run() && !editor.isActive("codeBlock"), + shortcuts: ["mod", "I"], + }, + { + value: "underline", + label: "Underline", + icon: , + action: (editor) => editor.chain().focus().toggleUnderline().run(), + isActive: (editor) => editor.isActive("underline"), + canExecute: (editor) => editor.can().chain().focus().toggleUnderline().run() && !editor.isActive("codeBlock"), + shortcuts: ["mod", "U"], + }, + { + value: "strikethrough", + label: "Strikethrough", + icon: , + action: (editor) => editor.chain().focus().toggleStrike().run(), + isActive: (editor) => editor.isActive("strike"), + canExecute: (editor) => editor.can().chain().focus().toggleStrike().run() && !editor.isActive("codeBlock"), + shortcuts: ["mod", "shift", "S"], + }, + { + value: "code", + label: "Code", + icon: , + action: (editor) => editor.chain().focus().toggleCode().run(), + isActive: (editor) => editor.isActive("code"), + canExecute: (editor) => editor.can().chain().focus().toggleCode().run() && !editor.isActive("codeBlock"), + shortcuts: ["mod", "E"], + }, + { + value: "clearFormatting", + label: "Clear formatting", + icon: , + action: (editor) => editor.chain().focus().unsetAllMarks().run(), + isActive: () => false, + canExecute: (editor) => editor.can().chain().focus().unsetAllMarks().run() && !editor.isActive("codeBlock"), + shortcuts: ["mod", "\\"], + }, +] + +interface SectionTwoProps extends VariantProps { + editor: Editor + activeActions?: TextStyleAction[] + mainActionCount?: number +} + +export const SectionTwo: React.FC = ({ + editor, + activeActions = formatActions.map((action) => action.value), + mainActionCount = 2, + size, + variant, +}) => { + return ( + } + dropdownTooltip="More formatting" + dropdownClassName="w-8" + size={size} + variant={variant} + /> + ) +} + +SectionTwo.displayName = "SectionTwo" + +export default SectionTwo diff --git a/packages/tiptap/src/components/shortcut-key.tsx b/packages/tiptap/src/components/shortcut-key.tsx new file mode 100644 index 0000000..19f3ba5 --- /dev/null +++ b/packages/tiptap/src/components/shortcut-key.tsx @@ -0,0 +1,35 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +import { getShortcutKey } from "../utils" + +export interface ShortcutKeyProps extends React.HTMLAttributes { + keys: string[] +} + +export const ShortcutKey = React.forwardRef(({ className, keys, ...props }, ref) => { + const modifiedKeys = keys.map((key) => getShortcutKey(key)) + const ariaLabel = modifiedKeys.map((shortcut) => shortcut.readable).join(" + ") + + return ( + + {modifiedKeys.map((shortcut) => ( + + {shortcut.symbol} + + ))} + + ) +}) + +ShortcutKey.displayName = "ShortcutKey" diff --git a/packages/tiptap/src/components/spinner.tsx b/packages/tiptap/src/components/spinner.tsx new file mode 100644 index 0000000..bdf1741 --- /dev/null +++ b/packages/tiptap/src/components/spinner.tsx @@ -0,0 +1,29 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +interface SpinnerProps extends React.SVGProps {} + +const SpinnerComponent = React.forwardRef(function Spinner({ className, ...props }, ref) { + return ( + + + + + ) +}) + +SpinnerComponent.displayName = "Spinner" + +export const Spinner = React.memo(SpinnerComponent) diff --git a/packages/tiptap/src/components/toolbar-button.tsx b/packages/tiptap/src/components/toolbar-button.tsx new file mode 100644 index 0000000..d06c711 --- /dev/null +++ b/packages/tiptap/src/components/toolbar-button.tsx @@ -0,0 +1,39 @@ +import type { TooltipContentProps } from "@radix-ui/react-tooltip" +import * as React from "react" + +import { Toggle } from "@/components/ui/toggle" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { cn } from "@/lib/utils" + +interface ToolbarButtonProps extends React.ComponentPropsWithoutRef { + isActive?: boolean + tooltip?: string + tooltipOptions?: TooltipContentProps +} + +export const ToolbarButton = React.forwardRef( + ({ isActive, children, tooltip, className, tooltipOptions, ...props }, ref) => { + const toggleButton = ( + + {children} + + ) + + if (!tooltip) { + return toggleButton + } + + return ( + + {toggleButton} + +
{tooltip}
+
+
+ ) + }, +) + +ToolbarButton.displayName = "ToolbarButton" + +export default ToolbarButton diff --git a/packages/tiptap/src/components/toolbar-section.tsx b/packages/tiptap/src/components/toolbar-section.tsx new file mode 100644 index 0000000..86875d5 --- /dev/null +++ b/packages/tiptap/src/components/toolbar-section.tsx @@ -0,0 +1,114 @@ +import { CaretDownIcon } from "@radix-ui/react-icons" +import type { Editor } from "@tiptap/react" +import type { VariantProps } from "class-variance-authority" +import * as React from "react" + +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import type { toggleVariants } from "@/components/ui/toggle" +import { cn } from "@/lib/utils" + +import type { FormatAction } from "../types" +import { getShortcutKey } from "../utils" +import { ShortcutKey } from "./shortcut-key" +import { ToolbarButton } from "./toolbar-button" + +interface ToolbarSectionProps extends VariantProps { + editor: Editor + actions: FormatAction[] + activeActions?: string[] + mainActionCount?: number + dropdownIcon?: React.ReactNode + dropdownTooltip?: string + dropdownClassName?: string +} + +export const ToolbarSection: React.FC = ({ + editor, + actions, + activeActions = actions.map((action) => action.value), + mainActionCount = 0, + dropdownIcon, + dropdownTooltip = "More options", + dropdownClassName = "w-12", + size, + variant, +}) => { + const { mainActions, dropdownActions } = React.useMemo(() => { + const sortedActions = actions + .filter((action) => activeActions.includes(action.value)) + .sort((a, b) => activeActions.indexOf(a.value) - activeActions.indexOf(b.value)) + + return { + mainActions: sortedActions.slice(0, mainActionCount), + dropdownActions: sortedActions.slice(mainActionCount), + } + }, [actions, activeActions, mainActionCount]) + + const renderToolbarButton = React.useCallback( + (action: FormatAction) => ( + action.action(editor)} + disabled={!action.canExecute(editor)} + isActive={action.isActive(editor)} + tooltip={`${action.label} ${action.shortcuts.map((s) => getShortcutKey(s).symbol).join(" ")}`} + aria-label={action.label} + size={size} + variant={variant} + > + {action.icon} + + ), + [editor, size, variant], + ) + + const renderDropdownMenuItem = React.useCallback( + (action: FormatAction) => ( + action.action(editor)} + disabled={!action.canExecute(editor)} + className={cn("flex flex-row items-center justify-between gap-4", { + "bg-accent": action.isActive(editor), + })} + aria-label={action.label} + > + {action.label} + + + ), + [editor], + ) + + const isDropdownActive = React.useMemo( + () => dropdownActions.some((action) => action.isActive(editor)), + [dropdownActions, editor], + ) + + return ( + <> + {mainActions.map((element) => renderToolbarButton(element))} + {dropdownActions.length > 0 && ( + + + + {dropdownIcon || } + + + + {dropdownActions.map((element) => renderDropdownMenuItem(element))} + + + )} + + ) +} + +export default ToolbarSection diff --git a/packages/tiptap/src/extensions/code-block-lowlight/code-block-lowlight.ts b/packages/tiptap/src/extensions/code-block-lowlight/code-block-lowlight.ts new file mode 100644 index 0000000..36f8e97 --- /dev/null +++ b/packages/tiptap/src/extensions/code-block-lowlight/code-block-lowlight.ts @@ -0,0 +1,17 @@ +import { CodeBlockLowlight as TiptapCodeBlockLowlight } from "@tiptap/extension-code-block-lowlight" +import { common, createLowlight } from "lowlight" + +export const CodeBlockLowlight = TiptapCodeBlockLowlight.extend({ + addOptions() { + return { + ...this.parent?.(), + lowlight: createLowlight(common), + defaultLanguage: null, + HTMLAttributes: { + class: "block-node", + }, + } + }, +}) + +export default CodeBlockLowlight diff --git a/packages/tiptap/src/extensions/code-block-lowlight/index.ts b/packages/tiptap/src/extensions/code-block-lowlight/index.ts new file mode 100644 index 0000000..4d6759e --- /dev/null +++ b/packages/tiptap/src/extensions/code-block-lowlight/index.ts @@ -0,0 +1 @@ +export * from "./code-block-lowlight" diff --git a/packages/tiptap/src/extensions/color/color.ts b/packages/tiptap/src/extensions/color/color.ts new file mode 100644 index 0000000..ecb2843 --- /dev/null +++ b/packages/tiptap/src/extensions/color/color.ts @@ -0,0 +1,20 @@ +import { Color as TiptapColor } from "@tiptap/extension-color" +import { Plugin } from "@tiptap/pm/state" + +export const Color = TiptapColor.extend({ + addProseMirrorPlugins() { + return [ + ...(this.parent?.() || []), + new Plugin({ + props: { + handleKeyDown: (_, event) => { + if (event.key === "Enter") { + this.editor.commands.unsetColor() + } + return false + }, + }, + }), + ] + }, +}) diff --git a/packages/tiptap/src/extensions/color/index.ts b/packages/tiptap/src/extensions/color/index.ts new file mode 100644 index 0000000..eb96334 --- /dev/null +++ b/packages/tiptap/src/extensions/color/index.ts @@ -0,0 +1 @@ +export * from "./color" diff --git a/packages/tiptap/src/extensions/file-handler/index.ts b/packages/tiptap/src/extensions/file-handler/index.ts new file mode 100644 index 0000000..21bab9c --- /dev/null +++ b/packages/tiptap/src/extensions/file-handler/index.ts @@ -0,0 +1,102 @@ +import { Plugin, PluginKey } from "@tiptap/pm/state" +import type { Editor } from "@tiptap/react" +import { Extension } from "@tiptap/react" + +import type { FileError, FileValidationOptions } from "../../utils" +import { filterFiles } from "../../utils" + +type FileHandlePluginOptions = { + key?: PluginKey + editor: Editor + onPaste?: (editor: Editor, files: File[], pasteContent?: string) => void + onDrop?: (editor: Editor, files: File[], pos: number) => void + onValidationError?: (errors: FileError[]) => void +} & FileValidationOptions + +const FileHandlePlugin = (options: FileHandlePluginOptions) => { + const { key, editor, onPaste, onDrop, onValidationError, allowedMimeTypes, maxFileSize } = options + + return new Plugin({ + key: key || new PluginKey("fileHandler"), + + props: { + handleDrop(view, event) { + event.preventDefault() + event.stopPropagation() + + const { dataTransfer } = event + + if (!dataTransfer?.files.length) { + return + } + + const pos = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }) + + const [validFiles, errors] = filterFiles(Array.from(dataTransfer.files), { + allowedMimeTypes, + maxFileSize, + allowBase64: options.allowBase64, + }) + + if (errors.length > 0 && onValidationError) { + onValidationError(errors) + } + + if (validFiles.length > 0 && onDrop) { + onDrop(editor, validFiles, pos?.pos ?? 0) + } + }, + + handlePaste(_, event) { + event.preventDefault() + event.stopPropagation() + + const { clipboardData } = event + + if (!clipboardData?.files.length) { + return + } + + const [validFiles, errors] = filterFiles(Array.from(clipboardData.files), { + allowedMimeTypes, + maxFileSize, + allowBase64: options.allowBase64, + }) + const html = clipboardData.getData("text/html") + + if (errors.length > 0 && onValidationError) { + onValidationError(errors) + } + + if (validFiles.length > 0 && onPaste) { + onPaste(editor, validFiles, html) + } + }, + }, + }) +} + +export const FileHandler = Extension.create>({ + name: "fileHandler", + + addOptions() { + return { + allowBase64: false, + allowedMimeTypes: [], + maxFileSize: 0, + } + }, + + addProseMirrorPlugins() { + return [ + FileHandlePlugin({ + key: new PluginKey(this.name), + editor: this.editor, + ...this.options, + }), + ] + }, +}) diff --git a/packages/tiptap/src/extensions/horizontal-rule/horizontal-rule.ts b/packages/tiptap/src/extensions/horizontal-rule/horizontal-rule.ts new file mode 100644 index 0000000..4b6d533 --- /dev/null +++ b/packages/tiptap/src/extensions/horizontal-rule/horizontal-rule.ts @@ -0,0 +1,18 @@ +/* + * Wrap the horizontal rule in a div element. + * Also add a keyboard shortcut to insert a horizontal rule. + */ +import { HorizontalRule as TiptapHorizontalRule } from "@tiptap/extension-horizontal-rule" + +export const HorizontalRule = TiptapHorizontalRule.extend({ + addKeyboardShortcuts() { + return { + "Mod-Alt--": () => + this.editor.commands.insertContent({ + type: this.name, + }), + } + }, +}) + +export default HorizontalRule diff --git a/packages/tiptap/src/extensions/horizontal-rule/index.ts b/packages/tiptap/src/extensions/horizontal-rule/index.ts new file mode 100644 index 0000000..7cc7854 --- /dev/null +++ b/packages/tiptap/src/extensions/horizontal-rule/index.ts @@ -0,0 +1 @@ +export * from "./horizontal-rule" diff --git a/packages/tiptap/src/extensions/image/components/image-actions.tsx b/packages/tiptap/src/extensions/image/components/image-actions.tsx new file mode 100644 index 0000000..b33ddf7 --- /dev/null +++ b/packages/tiptap/src/extensions/image/components/image-actions.tsx @@ -0,0 +1,123 @@ +import { ClipboardCopyIcon, DotsHorizontalIcon, DownloadIcon, Link2Icon, SizeIcon } from "@radix-ui/react-icons" +import * as React from "react" + +import { Button } from "@/components/ui/button" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { cn } from "@/lib/utils" + +interface ImageActionsProps { + shouldMerge?: boolean + isLink?: boolean + onView?: () => void + onDownload?: () => void + onCopy?: () => void + onCopyLink?: () => void +} + +interface ActionButtonProps extends React.ButtonHTMLAttributes { + icon: React.ReactNode + tooltip: string +} + +export const ActionWrapper = React.memo( + React.forwardRef>(({ children, className, ...props }, ref) => ( +
+ {children} +
+ )), +) + +ActionWrapper.displayName = "ActionWrapper" + +export const ActionButton = React.memo( + React.forwardRef(({ icon, tooltip, className, ...props }, ref) => ( + + + + + {tooltip} + + )), +) + +ActionButton.displayName = "ActionButton" + +type ActionKey = "onView" | "onDownload" | "onCopy" | "onCopyLink" + +const ActionItems: Array<{ + key: ActionKey + icon: React.ReactNode + tooltip: string + isLink?: boolean +}> = [ + { key: "onView", icon: , tooltip: "View image" }, + { key: "onDownload", icon: , tooltip: "Download image" }, + { key: "onCopy", icon: , tooltip: "Copy image to clipboard" }, + { key: "onCopyLink", icon: , tooltip: "Copy image link", isLink: true }, +] + +export const ImageActions: React.FC = React.memo( + ({ shouldMerge = false, isLink = false, ...actions }) => { + const [isOpen, setIsOpen] = React.useState(false) + + const handleAction = React.useCallback((e: React.MouseEvent, action: (() => void) | undefined) => { + e.preventDefault() + e.stopPropagation() + action?.() + }, []) + + const filteredActions = React.useMemo(() => ActionItems.filter((item) => isLink || !item.isLink), [isLink]) + + return ( + + {shouldMerge ? ( + + + } + tooltip="Open menu" + onClick={(e) => e.preventDefault()} + /> + + + {filteredActions.map(({ key, icon, tooltip }) => ( + handleAction(e, actions[key])}> +
+ {icon} + {tooltip} +
+
+ ))} +
+
+ ) : ( + filteredActions.map(({ key, icon, tooltip }) => ( + handleAction(e, actions[key])} /> + )) + )} +
+ ) + }, +) + +ImageActions.displayName = "ImageActions" diff --git a/packages/tiptap/src/extensions/image/components/image-overlay.tsx b/packages/tiptap/src/extensions/image/components/image-overlay.tsx new file mode 100644 index 0000000..02cc08c --- /dev/null +++ b/packages/tiptap/src/extensions/image/components/image-overlay.tsx @@ -0,0 +1,20 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +import { Spinner } from "../../../components/spinner" + +export const ImageOverlay = React.memo(() => { + return ( +
+ +
+ ) +}) + +ImageOverlay.displayName = "ImageOverlay" diff --git a/packages/tiptap/src/extensions/image/components/image-view-block.tsx b/packages/tiptap/src/extensions/image/components/image-view-block.tsx new file mode 100644 index 0000000..8365cdf --- /dev/null +++ b/packages/tiptap/src/extensions/image/components/image-view-block.tsx @@ -0,0 +1,289 @@ +import { InfoCircledIcon, TrashIcon } from "@radix-ui/react-icons" +import type { NodeViewProps } from "@tiptap/react" +import { NodeViewWrapper } from "@tiptap/react" +import * as React from "react" +import { Controlled as ControlledZoom } from "react-medium-image-zoom" + +import { cn } from "@/lib/utils" + +import { Spinner } from "../../../components/spinner" +import { blobUrlToBase64, randomId } from "../../../utils" +import type { ElementDimensions } from "../hooks/use-drag-resize" +import { useDragResize } from "../hooks/use-drag-resize" +import { useImageActions } from "../hooks/use-image-actions" +import type { UploadReturnType } from "../image" +import { ActionButton, ActionWrapper, ImageActions } from "./image-actions" +import { ImageOverlay } from "./image-overlay" +import { ResizeHandle } from "./resize-handle" + +const MAX_HEIGHT = 600 +const MIN_HEIGHT = 120 +const MIN_WIDTH = 120 + +interface ImageState { + src: string + isServerUploading: boolean + imageLoaded: boolean + isZoomed: boolean + error: boolean + naturalSize: ElementDimensions +} + +const normalizeUploadResponse = (res: UploadReturnType) => ({ + src: typeof res === "string" ? res : res.src, + id: typeof res === "string" ? randomId() : res.id, +}) + +export const ImageViewBlock: React.FC = ({ editor, node, selected, updateAttributes }) => { + const { src: initialSrc, width: initialWidth, height: initialHeight, fileName } = node.attrs + const uploadAttemptedRef = React.useRef(false) + + const initSrc = React.useMemo(() => { + if (typeof initialSrc === "string") { + return initialSrc + } + return initialSrc.src + }, [initialSrc]) + + const [imageState, setImageState] = React.useState({ + src: initSrc, + isServerUploading: false, + imageLoaded: false, + isZoomed: false, + error: false, + naturalSize: { width: initialWidth, height: initialHeight }, + }) + + const containerRef = React.useRef(null) + const [activeResizeHandle, setActiveResizeHandle] = React.useState<"left" | "right" | null>(null) + + const onDimensionsChange = React.useCallback( + ({ width, height }: ElementDimensions) => { + updateAttributes({ width, height }) + }, + [updateAttributes], + ) + + const aspectRatio = imageState.naturalSize.width / imageState.naturalSize.height + const maxWidth = MAX_HEIGHT * aspectRatio + const containerMaxWidth = containerRef.current ? + Number.parseFloat(getComputedStyle(containerRef.current).getPropertyValue("--editor-width")) : + Infinity + + const { isLink, onView, onDownload, onCopy, onCopyLink, onRemoveImg } = useImageActions({ + editor, + node, + src: imageState.src, + onViewClick: (isZoomed) => setImageState((prev) => ({ ...prev, isZoomed })), + }) + + const { currentWidth, currentHeight, updateDimensions, initiateResize, isResizing } = useDragResize({ + initialWidth: initialWidth ?? imageState.naturalSize.width, + initialHeight: initialHeight ?? imageState.naturalSize.height, + contentWidth: imageState.naturalSize.width, + contentHeight: imageState.naturalSize.height, + gridInterval: 0.1, + onDimensionsChange, + minWidth: MIN_WIDTH, + minHeight: MIN_HEIGHT, + maxWidth: containerMaxWidth > 0 ? containerMaxWidth : maxWidth, + }) + + const shouldMerge = React.useMemo(() => currentWidth <= 180, [currentWidth]) + + const handleImageLoad = React.useCallback( + (ev: React.SyntheticEvent) => { + const img = ev.target as HTMLImageElement + const newNaturalSize = { + width: img.naturalWidth, + height: img.naturalHeight, + } + setImageState((prev) => ({ + ...prev, + naturalSize: newNaturalSize, + imageLoaded: true, + })) + updateAttributes({ + width: img.width || newNaturalSize.width, + height: img.height || newNaturalSize.height, + alt: img.alt, + title: img.title, + }) + + if (!initialWidth) { + updateDimensions((state) => ({ ...state, width: newNaturalSize.width })) + } + }, + [initialWidth, updateAttributes, updateDimensions], + ) + + const handleImageError = React.useCallback(() => { + setImageState((prev) => ({ ...prev, error: true, imageLoaded: true })) + }, []) + + const handleResizeStart = React.useCallback( + (direction: "left" | "right") => (event: React.PointerEvent) => { + setActiveResizeHandle(direction) + initiateResize(direction)(event) + }, + [initiateResize], + ) + + const handleResizeEnd = React.useCallback(() => { + setActiveResizeHandle(null) + }, []) + + React.useEffect(() => { + if (!isResizing) { + handleResizeEnd() + } + }, [isResizing, handleResizeEnd]) + + React.useEffect(() => { + const handleImage = async () => { + if (!initSrc.startsWith("blob:") || uploadAttemptedRef.current) { + return + } + + uploadAttemptedRef.current = true + const imageExtension = editor.options.extensions.find((ext) => ext.name === "image") + const { uploadFn } = imageExtension?.options ?? {} + + if (!uploadFn) { + try { + const base64 = await blobUrlToBase64(initSrc) + setImageState((prev) => ({ ...prev, src: base64 })) + updateAttributes({ src: base64 }) + } catch { + setImageState((prev) => ({ ...prev, error: true })) + } + return + } + + try { + setImageState((prev) => ({ ...prev, isServerUploading: true })) + const response = await fetch(initSrc) + const blob = await response.blob() + const file = new File([blob], fileName, { type: blob.type }) + + const url = await uploadFn(file, editor) + const normalizedData = normalizeUploadResponse(url) + + setImageState((prev) => ({ + ...prev, + ...normalizedData, + isServerUploading: false, + })) + + updateAttributes(normalizedData) + } catch { + setImageState((prev) => ({ + ...prev, + error: true, + isServerUploading: false, + })) + } + } + + handleImage() + }, [editor, fileName, initSrc, updateAttributes]) + + return ( + +
+
+
+
+ {imageState.isServerUploading && !imageState.error && ( +
+ +
+ )} + + {imageState.error && ( +
+ +

Failed to load image

+
+ )} + + setImageState((prev) => ({ ...prev, isZoomed: false }))} + > + {node.attrs.alt + +
+ + {imageState.isServerUploading && } + + {editor.isEditable && imageState.imageLoaded && !imageState.error && !imageState.isServerUploading && ( + <> + + + + )} +
+ + {imageState.error && ( + + } tooltip="Remove image" onClick={onRemoveImg} /> + + )} + + {!isResizing && !imageState.error && !imageState.isServerUploading && ( + + )} +
+
+
+ ) +} diff --git a/packages/tiptap/src/extensions/image/components/resize-handle.tsx b/packages/tiptap/src/extensions/image/components/resize-handle.tsx new file mode 100644 index 0000000..44350df --- /dev/null +++ b/packages/tiptap/src/extensions/image/components/resize-handle.tsx @@ -0,0 +1,30 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +interface ResizeProps extends React.HTMLAttributes { + isResizing?: boolean +} + +export const ResizeHandle = React.forwardRef( + ({ className, isResizing = false, ...props }, ref) => { + return ( +
+ ) + }, +) + +ResizeHandle.displayName = "ResizeHandle" diff --git a/packages/tiptap/src/extensions/image/hooks/use-drag-resize.ts b/packages/tiptap/src/extensions/image/hooks/use-drag-resize.ts new file mode 100644 index 0000000..29bc329 --- /dev/null +++ b/packages/tiptap/src/extensions/image/hooks/use-drag-resize.ts @@ -0,0 +1,142 @@ +import { useCallback, useEffect, useState } from "react" + +type ResizeDirection = "left" | "right" +export type ElementDimensions = { width: number, height: number } + +type HookParams = { + initialWidth?: number + initialHeight?: number + contentWidth?: number + contentHeight?: number + gridInterval: number + minWidth: number + minHeight: number + maxWidth: number + onDimensionsChange?: (dimensions: ElementDimensions) => void +} + +export function useDragResize({ + initialWidth, + initialHeight, + contentWidth, + contentHeight, + gridInterval, + minWidth, + minHeight, + maxWidth, + onDimensionsChange, +}: HookParams) { + const [dimensions, updateDimensions] = useState({ + width: Math.max(initialWidth ?? minWidth, minWidth), + height: Math.max(initialHeight ?? minHeight, minHeight), + }) + const [boundaryWidth, setBoundaryWidth] = useState(Infinity) + const [resizeOrigin, setResizeOrigin] = useState(0) + const [initialDimensions, setInitialDimensions] = useState(dimensions) + const [resizeDirection, setResizeDirection] = useState() + + const widthConstraint = useCallback( + (proposedWidth: number, maxAllowedWidth: number) => { + const effectiveMinWidth = Math.max( + minWidth, + Math.min(contentWidth ?? minWidth, (gridInterval / 100) * maxAllowedWidth), + ) + return Math.min(maxAllowedWidth, Math.max(proposedWidth, effectiveMinWidth)) + }, + [gridInterval, contentWidth, minWidth], + ) + + const handlePointerMove = useCallback( + (event: PointerEvent) => { + event.preventDefault() + const movementDelta = (resizeDirection === "left" ? resizeOrigin - event.pageX : event.pageX - resizeOrigin) * 2 + const gridUnitWidth = (gridInterval / 100) * boundaryWidth + const proposedWidth = initialDimensions.width + movementDelta + const alignedWidth = Math.round(proposedWidth / gridUnitWidth) * gridUnitWidth + const finalWidth = widthConstraint(alignedWidth, boundaryWidth) + const aspectRatio = contentHeight && contentWidth ? contentHeight / contentWidth : 1 + + updateDimensions({ + width: Math.max(finalWidth, minWidth), + height: Math.max(contentWidth ? finalWidth * aspectRatio : (contentHeight ?? minHeight), minHeight), + }) + }, + [ + widthConstraint, + resizeDirection, + boundaryWidth, + resizeOrigin, + gridInterval, + contentHeight, + contentWidth, + initialDimensions.width, + minWidth, + minHeight, + ], + ) + + const handlePointerUp = useCallback( + (event: PointerEvent) => { + event.preventDefault() + event.stopPropagation() + + setResizeOrigin(0) + setResizeDirection(undefined) + onDimensionsChange?.(dimensions) + }, + [onDimensionsChange, dimensions], + ) + + const handleKeydown = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault() + event.stopPropagation() + updateDimensions({ + width: Math.max(initialDimensions.width, minWidth), + height: Math.max(initialDimensions.height, minHeight), + }) + setResizeDirection(undefined) + } + }, + [initialDimensions, minWidth, minHeight], + ) + + const initiateResize = useCallback( + (direction: ResizeDirection) => (event: React.PointerEvent) => { + event.preventDefault() + event.stopPropagation() + + setBoundaryWidth(maxWidth) + setInitialDimensions({ + width: Math.max(widthConstraint(dimensions.width, maxWidth), minWidth), + height: Math.max(dimensions.height, minHeight), + }) + setResizeOrigin(event.pageX) + setResizeDirection(direction) + }, + [maxWidth, widthConstraint, dimensions.width, dimensions.height, minWidth, minHeight], + ) + + useEffect(() => { + if (resizeDirection) { + document.addEventListener("keydown", handleKeydown) + document.addEventListener("pointermove", handlePointerMove) + document.addEventListener("pointerup", handlePointerUp) + + return () => { + document.removeEventListener("keydown", handleKeydown) + document.removeEventListener("pointermove", handlePointerMove) + document.removeEventListener("pointerup", handlePointerUp) + } + } + }, [resizeDirection, handleKeydown, handlePointerMove, handlePointerUp]) + + return { + initiateResize, + isResizing: !!resizeDirection, + updateDimensions, + currentWidth: Math.max(dimensions.width, minWidth), + currentHeight: Math.max(dimensions.height, minHeight), + } +} diff --git a/packages/tiptap/src/extensions/image/hooks/use-image-actions.ts b/packages/tiptap/src/extensions/image/hooks/use-image-actions.ts new file mode 100644 index 0000000..8f72c8a --- /dev/null +++ b/packages/tiptap/src/extensions/image/hooks/use-image-actions.ts @@ -0,0 +1,55 @@ +import type { Node } from "@tiptap/pm/model" +import type { Editor } from "@tiptap/react" +import * as React from "react" + +import { isUrl } from "../../../utils" + +interface UseImageActionsProps { + editor: Editor + node: Node + src: string + onViewClick: (value: boolean) => void +} + +export type ImageActionHandlers = { + onView?: () => void + onDownload?: () => void + onCopy?: () => void + onCopyLink?: () => void + onRemoveImg?: () => void +} + +export const useImageActions = ({ editor, node, src, onViewClick }: UseImageActionsProps) => { + const isLink = React.useMemo(() => isUrl(src), [src]) + + const onView = React.useCallback(() => { + onViewClick(true) + }, [onViewClick]) + + const onDownload = React.useCallback(() => { + editor.commands.downloadImage({ src: node.attrs.src, alt: node.attrs.alt }) + }, [editor.commands, node.attrs.alt, node.attrs.src]) + + const onCopy = React.useCallback(() => { + editor.commands.copyImage({ src: node.attrs.src }) + }, [editor.commands, node.attrs.src]) + + const onCopyLink = React.useCallback(() => { + editor.commands.copyLink({ src: node.attrs.src }) + }, [editor.commands, node.attrs.src]) + + const onRemoveImg = React.useCallback(() => { + editor.commands.command(({ tr, dispatch }) => { + const { selection } = tr + const nodeAtSelection = tr.doc.nodeAt(selection.from) + + if (nodeAtSelection && nodeAtSelection.type.name === "image" && dispatch) { + tr.deleteSelection() + return true + } + return false + }) + }, [editor.commands]) + + return { isLink, onView, onDownload, onCopy, onCopyLink, onRemoveImg } +} diff --git a/packages/tiptap/src/extensions/image/image.ts b/packages/tiptap/src/extensions/image/image.ts new file mode 100644 index 0000000..8966155 --- /dev/null +++ b/packages/tiptap/src/extensions/image/image.ts @@ -0,0 +1,316 @@ +import type { ImageOptions } from "@tiptap/extension-image" +import { Image as TiptapImage } from "@tiptap/extension-image" +import type { Attrs } from "@tiptap/pm/model" +import { ReplaceStep } from "@tiptap/pm/transform" +import type { Editor } from "@tiptap/react" +import { ReactNodeViewRenderer } from "@tiptap/react" + +import type { FileError, FileValidationOptions } from "../../utils" +import { filterFiles, randomId } from "../../utils" +import { ImageViewBlock } from "./components/image-view-block" + +type ImageAction = "download" | "copyImage" | "copyLink" + +interface DownloadImageCommandProps { + src: string + alt?: string +} + +interface ImageActionProps extends DownloadImageCommandProps { + action: ImageAction +} + +export type UploadReturnType = + | string + | { + id: string | number + src: string + } + +interface CustomImageOptions extends ImageOptions, Omit { + uploadFn?: (file: File, editor: Editor) => Promise + onImageRemoved?: (props: Attrs) => void + onActionSuccess?: (props: ImageActionProps) => void + onActionError?: (error: Error, props: ImageActionProps) => void + downloadImage?: (props: ImageActionProps, options: CustomImageOptions) => Promise + copyImage?: (props: ImageActionProps, options: CustomImageOptions) => Promise + copyLink?: (props: ImageActionProps, options: CustomImageOptions) => Promise + onValidationError?: (errors: FileError[]) => void + onToggle?: (editor: Editor, files: File[], pos: number) => void +} + +declare module "@tiptap/react" { + interface Commands { + setImages: { + setImages: (attrs: { src: string | File, alt?: string, title?: string }[]) => ReturnType + } + downloadImage: { + downloadImage: (attrs: DownloadImageCommandProps) => ReturnType + } + copyImage: { + copyImage: (attrs: DownloadImageCommandProps) => ReturnType + } + copyLink: { + copyLink: (attrs: DownloadImageCommandProps) => ReturnType + } + toggleImage: { + toggleImage: () => ReturnType + } + } +} + +const handleError = ( + error: unknown, + props: ImageActionProps, + errorHandler?: (error: Error, props: ImageActionProps) => void, +): void => { + const typedError = error instanceof Error ? error : new Error("Unknown error") + errorHandler?.(typedError, props) +} + +const handleDataUrl = (src: string): { blob: Blob, extension?: string } => { + const [header, base64Data] = src.split(",") + if (!header || !base64Data) throw new Error("Invalid data URL") + const mimeType = header?.split(":")[1]?.split(";")[0] + if (!mimeType) throw new Error("Invalid data URL") + const extension = mimeType.split("/")[1] + const byteCharacters = atob(base64Data) + const byteArray = new Uint8Array(byteCharacters.length) + for (let i = 0; i < byteCharacters.length; i++) { + byteArray[i] = byteCharacters.codePointAt(i)! + } + const blob = new Blob([byteArray], { type: mimeType }) + return { blob, extension } +} + +const handleImageUrl = async (src: string): Promise<{ blob: Blob, extension?: string }> => { + const response = await fetch(src) + if (!response.ok) throw new Error("Failed to fetch image") + const blob = await response.blob() + const extension = blob.type.split(/\/|\+/)[1] + return { blob, extension } +} + +const fetchImageBlob = async (src: string): Promise<{ blob: Blob, extension?: string }> => { + return src.startsWith("data:") ? handleDataUrl(src) : handleImageUrl(src) +} + +const saveImage = async (blob: Blob, name: string, extension: string): Promise => { + const imageURL = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = imageURL + link.download = `${name}.${extension}` + document.body.append(link) + link.click() + link.remove() + URL.revokeObjectURL(imageURL) +} + +const downloadImage = async (props: ImageActionProps, options: CustomImageOptions): Promise => { + const { src, alt } = props + const potentialName = alt || "image" + + try { + const { blob, extension } = await fetchImageBlob(src) + await saveImage(blob, potentialName, extension) + options.onActionSuccess?.({ ...props, action: "download" }) + } catch (error) { + handleError(error, { ...props, action: "download" }, options.onActionError) + } +} + +const copyImage = async (props: ImageActionProps, options: CustomImageOptions): Promise => { + const { src } = props + try { + const res = await fetch(src) + const blob = await res.blob() + await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]) + options.onActionSuccess?.({ ...props, action: "copyImage" }) + } catch (error) { + handleError(error, { ...props, action: "copyImage" }, options.onActionError) + } +} + +const copyLink = async (props: ImageActionProps, options: CustomImageOptions): Promise => { + const { src } = props + try { + await navigator.clipboard.writeText(src) + options.onActionSuccess?.({ ...props, action: "copyLink" }) + } catch (error) { + handleError(error, { ...props, action: "copyLink" }, options.onActionError) + } +} + +export const Image = TiptapImage.extend({ + atom: true, + + addOptions() { + return { + ...this.parent?.(), + allowedMimeTypes: [], + maxFileSize: 0, + uploadFn: undefined, + onToggle: undefined, + downloadImage: undefined, + copyImage: undefined, + copyLink: undefined, + } + }, + + addAttributes() { + return { + src: { + default: null, + }, + alt: { + default: null, + }, + title: { + default: null, + }, + id: { + default: null, + }, + width: { + default: null, + }, + height: { + default: null, + }, + fileName: { + default: null, + }, + } + }, + + addCommands() { + return { + setImages: + (attrs) => + ({ commands }) => { + const [validImages, errors] = filterFiles(attrs, { + allowedMimeTypes: this.options.allowedMimeTypes, + maxFileSize: this.options.maxFileSize, + allowBase64: this.options.allowBase64, + }) + + if (errors.length > 0 && this.options.onValidationError) { + this.options.onValidationError(errors) + } + + if (validImages.length > 0) { + return commands.insertContent( + validImages.map((image) => { + if (image.src instanceof File) { + const blobUrl = URL.createObjectURL(image.src) + const id = randomId() + + return { + type: this.type.name, + attrs: { + id, + src: blobUrl, + alt: image.alt, + title: image.title, + fileName: image.src.name, + }, + } + } else { + return { + type: this.type.name, + attrs: { + id: randomId(), + src: image.src, + alt: image.alt, + title: image.title, + fileName: null, + }, + } + } + }), + ) + } + + return false + }, + + downloadImage: (attrs) => () => { + const downloadFunc = this.options.downloadImage || downloadImage + void downloadFunc({ ...attrs, action: "download" }, this.options) + return true + }, + + copyImage: (attrs) => () => { + const copyImageFunc = this.options.copyImage || copyImage + void copyImageFunc({ ...attrs, action: "copyImage" }, this.options) + return true + }, + + copyLink: (attrs) => () => { + const copyLinkFunc = this.options.copyLink || copyLink + void copyLinkFunc({ ...attrs, action: "copyLink" }, this.options) + return true + }, + + toggleImage: + () => + ({ editor }) => { + const input = document.createElement("input") + input.type = "file" + input.accept = this.options.allowedMimeTypes.join(",") + input.onchange = () => { + const { files } = input + if (!files) return + + const [validImages, errors] = filterFiles(Array.from(files), { + allowedMimeTypes: this.options.allowedMimeTypes, + maxFileSize: this.options.maxFileSize, + allowBase64: this.options.allowBase64, + }) + + if (errors.length > 0 && this.options.onValidationError) { + this.options.onValidationError(errors) + return false + } + + if (validImages.length === 0) return false + + if (this.options.onToggle) { + this.options.onToggle(editor, validImages, editor.state.selection.from) + } + + return false + } + + input.click() + return true + }, + } + }, + + onTransaction({ transaction }) { + transaction.steps.forEach((step) => { + if (step instanceof ReplaceStep && step.slice.size === 0) { + const deletedPages = transaction.before.content.cut(step.from, step.to) + + deletedPages.forEach((node) => { + if (node.type.name === "image") { + const { attrs } = node + + if (attrs.src.startsWith("blob:")) { + URL.revokeObjectURL(attrs.src) + } + + this.options.onImageRemoved?.(attrs) + } + }) + } + }) + }, + + addNodeView() { + return ReactNodeViewRenderer(ImageViewBlock, { + className: "block-node", + }) + }, +}) diff --git a/packages/tiptap/src/extensions/image/index.ts b/packages/tiptap/src/extensions/image/index.ts new file mode 100644 index 0000000..61769c6 --- /dev/null +++ b/packages/tiptap/src/extensions/image/index.ts @@ -0,0 +1 @@ +export * from "./image" diff --git a/packages/tiptap/src/extensions/index.ts b/packages/tiptap/src/extensions/index.ts new file mode 100644 index 0000000..0be12a0 --- /dev/null +++ b/packages/tiptap/src/extensions/index.ts @@ -0,0 +1,9 @@ +export * from "./code-block-lowlight" +export * from "./color" +export * from "./file-handler" +export * from "./horizontal-rule" +export * from "./image" +export * from "./link" +export * from "./reset-marks-on-enter" +export * from "./selection" +export * from "./unset-all-marks" diff --git a/packages/tiptap/src/extensions/link/index.ts b/packages/tiptap/src/extensions/link/index.ts new file mode 100644 index 0000000..6ada303 --- /dev/null +++ b/packages/tiptap/src/extensions/link/index.ts @@ -0,0 +1 @@ +export * from "./link" diff --git a/packages/tiptap/src/extensions/link/link.ts b/packages/tiptap/src/extensions/link/link.ts new file mode 100644 index 0000000..f8765e6 --- /dev/null +++ b/packages/tiptap/src/extensions/link/link.ts @@ -0,0 +1,88 @@ +import TiptapLink from "@tiptap/extension-link" +import { Plugin, TextSelection } from "@tiptap/pm/state" +import type { EditorView } from "@tiptap/pm/view" +import { getMarkRange, mergeAttributes } from "@tiptap/react" + +export const Link = TiptapLink.extend({ + /* + * Determines whether typing next to a link automatically becomes part of the link. + * In this case, we dont want any characters to be included as part of the link. + */ + inclusive: false, + + /* + * Match all elements that have an href attribute, except for: + * - elements with a data-type attribute set to button + * - elements with an href attribute that contains 'javascript:' + */ + parseHTML() { + return [{ tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])' }] + }, + + renderHTML({ HTMLAttributes }) { + return ["a", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] + }, + + addOptions() { + return { + ...this.parent?.(), + openOnClick: false, + HTMLAttributes: { + class: "link", + }, + } + }, + + addProseMirrorPlugins() { + const { editor } = this + + return [ + ...(this.parent?.() || []), + new Plugin({ + props: { + handleKeyDown: (_: EditorView, event: KeyboardEvent) => { + const { selection } = editor.state + + /* + * Handles the 'Escape' key press when there's a selection within the link. + * This will move the cursor to the end of the link. + */ + if (event.key === "Escape" && selection.empty !== true) { + editor.commands.focus(selection.to, { scrollIntoView: false }) + } + + return false + }, + handleClick(view, pos) { + /* + * Marks the entire link when the user clicks on it. + */ + + const { schema, doc, tr } = view.state + const range = getMarkRange(doc.resolve(pos), schema.marks.link) + + if (!range) { + return + } + + const { from, to } = range + const start = Math.min(from, to) + const end = Math.max(from, to) + + if (pos < start || pos > end) { + return + } + + const $start = doc.resolve(start) + const $end = doc.resolve(end) + const transaction = tr.setSelection(new TextSelection($start, $end)) + + view.dispatch(transaction) + }, + }, + }), + ] + }, +}) + +export default Link diff --git a/packages/tiptap/src/extensions/reset-marks-on-enter/index.ts b/packages/tiptap/src/extensions/reset-marks-on-enter/index.ts new file mode 100644 index 0000000..4c06c8d --- /dev/null +++ b/packages/tiptap/src/extensions/reset-marks-on-enter/index.ts @@ -0,0 +1 @@ +export * from "./reset-marks-on-enter" diff --git a/packages/tiptap/src/extensions/reset-marks-on-enter/reset-marks-on-enter.ts b/packages/tiptap/src/extensions/reset-marks-on-enter/reset-marks-on-enter.ts new file mode 100644 index 0000000..7daee52 --- /dev/null +++ b/packages/tiptap/src/extensions/reset-marks-on-enter/reset-marks-on-enter.ts @@ -0,0 +1,25 @@ +import { Extension } from "@tiptap/react" + +export const ResetMarksOnEnter = Extension.create({ + name: "resetMarksOnEnter", + + addKeyboardShortcuts() { + return { + Enter: ({ editor }) => { + if ( + editor.isActive("bold") || + editor.isActive("italic") || + editor.isActive("strike") || + editor.isActive("underline") || + editor.isActive("code") + ) { + editor.commands.splitBlock({ keepMarks: false }) + + return true + } + + return false + }, + } + }, +}) diff --git a/packages/tiptap/src/extensions/selection/index.ts b/packages/tiptap/src/extensions/selection/index.ts new file mode 100644 index 0000000..c8f6102 --- /dev/null +++ b/packages/tiptap/src/extensions/selection/index.ts @@ -0,0 +1 @@ +export * from "./selection" diff --git a/packages/tiptap/src/extensions/selection/selection.ts b/packages/tiptap/src/extensions/selection/selection.ts new file mode 100644 index 0000000..822432c --- /dev/null +++ b/packages/tiptap/src/extensions/selection/selection.ts @@ -0,0 +1,36 @@ +import { Plugin, PluginKey } from "@tiptap/pm/state" +import { Decoration, DecorationSet } from "@tiptap/pm/view" +import { Extension } from "@tiptap/react" + +export const Selection = Extension.create({ + name: "selection", + + addProseMirrorPlugins() { + const { editor } = this + + return [ + new Plugin({ + key: new PluginKey("selection"), + props: { + decorations(state) { + if (state.selection.empty) { + return null + } + + if (editor.isFocused === true) { + return null + } + + return DecorationSet.create(state.doc, [ + Decoration.inline(state.selection.from, state.selection.to, { + class: "selection", + }), + ]) + }, + }, + }), + ] + }, +}) + +export default Selection diff --git a/packages/tiptap/src/extensions/unset-all-marks/index.ts b/packages/tiptap/src/extensions/unset-all-marks/index.ts new file mode 100644 index 0000000..4abdbe8 --- /dev/null +++ b/packages/tiptap/src/extensions/unset-all-marks/index.ts @@ -0,0 +1 @@ +export * from "./unset-all-marks" diff --git a/packages/tiptap/src/extensions/unset-all-marks/unset-all-marks.ts b/packages/tiptap/src/extensions/unset-all-marks/unset-all-marks.ts new file mode 100644 index 0000000..10dcbb2 --- /dev/null +++ b/packages/tiptap/src/extensions/unset-all-marks/unset-all-marks.ts @@ -0,0 +1,9 @@ +import { Extension } from "@tiptap/react" + +export const UnsetAllMarks = Extension.create({ + addKeyboardShortcuts() { + return { + "Mod-\\": () => this.editor.commands.unsetAllMarks(), + } + }, +}) diff --git a/packages/tiptap/src/hooks/use-container-size.ts b/packages/tiptap/src/hooks/use-container-size.ts new file mode 100644 index 0000000..45e9336 --- /dev/null +++ b/packages/tiptap/src/hooks/use-container-size.ts @@ -0,0 +1,53 @@ +import { useCallback, useEffect, useState } from "react" + +const DEFAULT_RECT: DOMRect = { + top: 0, + left: 0, + bottom: 0, + right: 0, + x: 0, + y: 0, + width: 0, + height: 0, + toJSON: () => "{}", +} + +export function useContainerSize(element: HTMLElement | null): DOMRect { + const [size, setSize] = useState(() => element?.getBoundingClientRect() ?? DEFAULT_RECT) + + const handleResize = useCallback(() => { + if (!element) return + + const newRect = element.getBoundingClientRect() + + setSize((prevRect) => { + if ( + Math.round(prevRect.width) === Math.round(newRect.width) && + Math.round(prevRect.height) === Math.round(newRect.height) && + Math.round(prevRect.x) === Math.round(newRect.x) && + Math.round(prevRect.y) === Math.round(newRect.y) + ) { + return prevRect + } + return newRect + }) + }, [element]) + + useEffect(() => { + if (!element) return + + const resizeObserver = new ResizeObserver(handleResize) + resizeObserver.observe(element) + + window.addEventListener("click", handleResize) + window.addEventListener("resize", handleResize) + + return () => { + resizeObserver.disconnect() + window.removeEventListener("click", handleResize) + window.removeEventListener("resize", handleResize) + } + }, [element, handleResize]) + + return size +} diff --git a/packages/tiptap/src/hooks/use-minimal-tiptap.ts b/packages/tiptap/src/hooks/use-minimal-tiptap.ts new file mode 100644 index 0000000..74e0455 --- /dev/null +++ b/packages/tiptap/src/hooks/use-minimal-tiptap.ts @@ -0,0 +1,212 @@ +import { Placeholder } from "@tiptap/extension-placeholder" +import { TextStyle } from "@tiptap/extension-text-style" +import { Typography } from "@tiptap/extension-typography" +import { Underline } from "@tiptap/extension-underline" +import type { Content, Editor, UseEditorOptions } from "@tiptap/react" +import { useEditor } from "@tiptap/react" +import { StarterKit } from "@tiptap/starter-kit" +import * as React from "react" +import { toast } from "sonner" + +import { cn } from "@/lib/utils" + +import { + CodeBlockLowlight, + Color, + FileHandler, + HorizontalRule, + Image, + Link, + ResetMarksOnEnter, + Selection, + UnsetAllMarks, +} from "../extensions" +import { fileToBase64, getOutput, randomId } from "../utils" +import { useThrottle } from "./use-throttle" + +export interface UseMinimalTiptapEditorProps extends UseEditorOptions { + value?: Content + output?: "html" | "json" | "text" + placeholder?: string + editorClassName?: string + throttleDelay?: number + onUpdate?: (content: Content) => void + onBlur?: (content: Content) => void +} + +const createExtensions = (placeholder: string) => [ + StarterKit.configure({ + horizontalRule: false, + codeBlock: false, + paragraph: { HTMLAttributes: { class: "text-node" } }, + heading: { HTMLAttributes: { class: "heading-node" } }, + blockquote: { HTMLAttributes: { class: "block-node" } }, + bulletList: { HTMLAttributes: { class: "list-node" } }, + orderedList: { HTMLAttributes: { class: "list-node" } }, + code: { HTMLAttributes: { class: "inline", spellcheck: "false" } }, + dropcursor: { width: 2, class: "ProseMirror-dropcursor border" }, + }), + Link, + Underline, + Image.configure({ + allowedMimeTypes: ["image/*"], + maxFileSize: 5 * 1024 * 1024, + allowBase64: true, + uploadFn: async (file) => { + // NOTE: This is a fake upload function. Replace this with your own upload logic. + // This function should return the uploaded image URL. + + // wait 3s to simulate upload + await new Promise((resolve) => setTimeout(resolve, 3000)) + + const src = await fileToBase64(file) + + // either return { id: string | number, src: string } or just src + // return src; + return { id: randomId(), src } + }, + onToggle(editor, files, pos) { + editor.commands.insertContentAt( + pos, + files.map((image) => { + const blobUrl = URL.createObjectURL(image) + const id = randomId() + + return { + type: "image", + attrs: { + id, + src: blobUrl, + alt: image.name, + title: image.name, + fileName: image.name, + }, + } + }), + ) + }, + onImageRemoved({ id, src }) { + // eslint-disable-next-line no-console + console.log("Image removed", { id, src }) + }, + onValidationError(errors) { + errors.forEach((error) => { + toast.error("Image validation error", { + position: "bottom-right", + description: error.reason, + }) + }) + }, + onActionSuccess({ action }) { + const mapping = { + copyImage: "Copy Image", + copyLink: "Copy Link", + download: "Download", + } + toast.success(mapping[action], { + position: "bottom-right", + description: "Image action success", + }) + }, + onActionError(error, { action }) { + const mapping = { + copyImage: "Copy Image", + copyLink: "Copy Link", + download: "Download", + } + toast.error(`Failed to ${mapping[action]}`, { + position: "bottom-right", + description: error.message, + }) + }, + }), + FileHandler.configure({ + allowBase64: true, + allowedMimeTypes: ["image/*"], + maxFileSize: 5 * 1024 * 1024, + onDrop: (editor, files, pos) => { + files.forEach(async (file) => { + const src = await fileToBase64(file) + editor.commands.insertContentAt(pos, { + type: "image", + attrs: { src }, + }) + }) + }, + onPaste: (editor, files) => { + files.forEach(async (file) => { + const src = await fileToBase64(file) + editor.commands.insertContent({ + type: "image", + attrs: { src }, + }) + }) + }, + onValidationError: (errors) => { + errors.forEach((error) => { + toast.error("Image validation error", { + position: "bottom-right", + description: error.reason, + }) + }) + }, + }), + Color, + TextStyle, + Selection, + Typography, + UnsetAllMarks, + HorizontalRule, + ResetMarksOnEnter, + CodeBlockLowlight, + Placeholder.configure({ placeholder: () => placeholder }), +] + +export const useMinimalTiptapEditor = ({ + value, + output = "html", + placeholder = "", + editorClassName, + throttleDelay = 0, + onUpdate, + onBlur, + ...props +}: UseMinimalTiptapEditorProps) => { + const throttledSetValue = useThrottle((value: Content) => onUpdate?.(value), throttleDelay) + + const handleUpdate = React.useCallback( + (editor: Editor) => throttledSetValue(getOutput(editor, output)), + [output, throttledSetValue], + ) + + const handleCreate = React.useCallback( + (editor: Editor) => { + if (value && editor.isEmpty) { + editor.commands.setContent(value) + } + }, + [value], + ) + + const handleBlur = React.useCallback((editor: Editor) => onBlur?.(getOutput(editor, output)), [output, onBlur]) + + const editor = useEditor({ + extensions: createExtensions(placeholder), + editorProps: { + attributes: { + autocomplete: "off", + autocorrect: "off", + autocapitalize: "off", + class: cn("focus:outline-none", editorClassName), + }, + }, + onUpdate: ({ editor }) => handleUpdate(editor), + onCreate: ({ editor }) => handleCreate(editor), + onBlur: ({ editor }) => handleBlur(editor), + ...props, + }) + + return editor +} + +export default useMinimalTiptapEditor diff --git a/packages/tiptap/src/hooks/use-theme.ts b/packages/tiptap/src/hooks/use-theme.ts new file mode 100644 index 0000000..d9b7088 --- /dev/null +++ b/packages/tiptap/src/hooks/use-theme.ts @@ -0,0 +1,25 @@ +import * as React from "react" + +export const useTheme = () => { + const [isDarkMode, setIsDarkMode] = React.useState(false) + + React.useEffect(() => { + const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + setIsDarkMode(darkModeMediaQuery.matches) + + const handleChange = (e: MediaQueryListEvent) => { + const newDarkMode = e.matches + setIsDarkMode(newDarkMode) + } + + darkModeMediaQuery.addEventListener("change", handleChange) + + return () => { + darkModeMediaQuery.removeEventListener("change", handleChange) + } + }, []) + + return isDarkMode +} + +export default useTheme diff --git a/packages/tiptap/src/hooks/use-throttle.ts b/packages/tiptap/src/hooks/use-throttle.ts new file mode 100644 index 0000000..4a0ce61 --- /dev/null +++ b/packages/tiptap/src/hooks/use-throttle.ts @@ -0,0 +1,34 @@ +import { useCallback, useRef } from "react" + +export function useThrottle void>( + callback: T, + delay: number, +): (...args: Parameters) => void { + const lastRan = useRef(Date.now()) + const timeoutRef = useRef(null) + + return useCallback( + (...args: Parameters) => { + const handler = () => { + if (Date.now() - lastRan.current >= delay) { + callback(...args) + lastRan.current = Date.now() + } else { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + timeoutRef.current = setTimeout( + () => { + callback(...args) + lastRan.current = Date.now() + }, + delay - (Date.now() - lastRan.current), + ) + } + } + + handler() + }, + [callback, delay], + ) +} diff --git a/packages/tiptap/src/index.ts b/packages/tiptap/src/index.ts new file mode 100644 index 0000000..ce8d654 --- /dev/null +++ b/packages/tiptap/src/index.ts @@ -0,0 +1 @@ +export * from "./minimal-tiptap" diff --git a/packages/tiptap/src/lib/utils.ts b/packages/tiptap/src/lib/utils.ts new file mode 100644 index 0000000..c66a9d9 --- /dev/null +++ b/packages/tiptap/src/lib/utils.ts @@ -0,0 +1,7 @@ +import type { ClassValue } from "clsx" +import { clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/packages/tiptap/src/minimal-tiptap-one.tsx b/packages/tiptap/src/minimal-tiptap-one.tsx new file mode 100644 index 0000000..d19614b --- /dev/null +++ b/packages/tiptap/src/minimal-tiptap-one.tsx @@ -0,0 +1,66 @@ +import "./styles/index.css" + +import type { Content, Editor } from "@tiptap/react" +import { EditorContent } from "@tiptap/react" +import * as React from "react" + +import { cn } from "@/lib/utils" + +import { LinkBubbleMenu } from "./components/bubble-menu/link-bubble-menu" +import { MeasuredContainer } from "./components/measured-container" +import { SectionTwo } from "./components/section/two" +import type { UseMinimalTiptapEditorProps } from "./hooks/use-minimal-tiptap" +import { useMinimalTiptapEditor } from "./hooks/use-minimal-tiptap" + +export interface MinimalTiptapProps extends Omit { + value?: Content + onChange?: (value: Content) => void + className?: string + editorContentClassName?: string +} + +const Toolbar = ({ editor }: { editor: Editor }) => ( +
+
+ +
+
+) + +export const MinimalTiptapOne = React.forwardRef( + ({ value, onChange, className, editorContentClassName, ...props }, ref) => { + const editor = useMinimalTiptapEditor({ + value, + onUpdate: onChange, + ...props, + }) + + if (!editor) { + return null + } + + return ( + + + + + + ) + }, +) + +MinimalTiptapOne.displayName = "MinimalTiptapOne" + +export default MinimalTiptapOne diff --git a/packages/tiptap/src/minimal-tiptap-three.tsx b/packages/tiptap/src/minimal-tiptap-three.tsx new file mode 100644 index 0000000..803e2f6 --- /dev/null +++ b/packages/tiptap/src/minimal-tiptap-three.tsx @@ -0,0 +1,98 @@ +import "@/components/minimal-tiptap/styles/index.css" + +import type { Content, Editor } from "@tiptap/react" +import { EditorContent } from "@tiptap/react" +import * as React from "react" + +import { LinkBubbleMenu } from "@/components/minimal-tiptap/components/bubble-menu/link-bubble-menu" +import { SectionFive } from "@/components/minimal-tiptap/components/section/five" +import { SectionFour } from "@/components/minimal-tiptap/components/section/four" +import { SectionOne } from "@/components/minimal-tiptap/components/section/one" +import { SectionThree } from "@/components/minimal-tiptap/components/section/three" +import { SectionTwo } from "@/components/minimal-tiptap/components/section/two" +import type { UseMinimalTiptapEditorProps } from "@/components/minimal-tiptap/hooks/use-minimal-tiptap" +import { useMinimalTiptapEditor } from "@/components/minimal-tiptap/hooks/use-minimal-tiptap" +import { Separator } from "@/components/ui/separator" +import { cn } from "@/lib/utils" + +import { MeasuredContainer } from "../minimal-tiptap/components/measured-container" + +export interface MinimalTiptapProps extends Omit { + value?: Content + onChange?: (value: Content) => void + className?: string + editorContentClassName?: string +} + +const Toolbar = ({ editor }: { editor: Editor }) => ( +
+
+ + + + + + + + + + + + + + + + + +
+
+) + +export const MinimalTiptapThree = React.forwardRef( + ({ value, onChange, className, editorContentClassName, ...props }, ref) => { + const editor = useMinimalTiptapEditor({ + value, + onUpdate: onChange, + ...props, + }) + + if (!editor) { + return null + } + + return ( + + + + + + ) + }, +) + +MinimalTiptapThree.displayName = "MinimalTiptapThree" + +export default MinimalTiptapThree diff --git a/packages/tiptap/src/minimal-tiptap.tsx b/packages/tiptap/src/minimal-tiptap.tsx new file mode 100644 index 0000000..087c939 --- /dev/null +++ b/packages/tiptap/src/minimal-tiptap.tsx @@ -0,0 +1,87 @@ +import "./styles/index.css" + +import type { Content, Editor } from "@tiptap/react" +import { EditorContent } from "@tiptap/react" +import * as React from "react" + +import { Separator } from "@/components/ui/separator" +import { cn } from "@/lib/utils" + +import { LinkBubbleMenu } from "./components/bubble-menu/link-bubble-menu" +import { MeasuredContainer } from "./components/measured-container" +import { SectionFive } from "./components/section/five" +import { SectionFour } from "./components/section/four" +import { SectionOne } from "./components/section/one" +import { SectionThree } from "./components/section/three" +import { SectionTwo } from "./components/section/two" +import type { UseMinimalTiptapEditorProps } from "./hooks/use-minimal-tiptap" +import { useMinimalTiptapEditor } from "./hooks/use-minimal-tiptap" + +export interface MinimalTiptapProps extends Omit { + value?: Content + onChange?: (value: Content) => void + className?: string + editorContentClassName?: string +} + +const Toolbar = ({ editor }: { editor: Editor }) => ( +
+
+ + + + + + + + + + + + + + + + + +
+
+) + +export const MinimalTiptapEditor = React.forwardRef( + ({ value, onChange, className, editorContentClassName, ...props }, ref) => { + const editor = useMinimalTiptapEditor({ + value, + onUpdate: onChange, + ...props, + }) + + if (!editor) { + return null + } + + return ( + + + + + + ) + }, +) + +MinimalTiptapEditor.displayName = "MinimalTiptapEditor" + +export default MinimalTiptapEditor diff --git a/packages/tiptap/src/styles/index.css b/packages/tiptap/src/styles/index.css new file mode 100644 index 0000000..34d4168 --- /dev/null +++ b/packages/tiptap/src/styles/index.css @@ -0,0 +1,189 @@ +@import './partials/code.css'; +@import './partials/placeholder.css'; +@import './partials/lists.css'; +@import './partials/typography.css'; +@import './partials/zoom.css'; + +:root { + --mt-overlay: rgba(251, 251, 251, 0.75); + --mt-transparent-foreground: rgba(0, 0, 0, 0.4); + --mt-bg-secondary: rgba(251, 251, 251, 0.8); + --mt-code-background: #082b781f; + --mt-code-color: #d4d4d4; + --mt-secondary: #9d9d9f; + --mt-pre-background: #ececec; + --mt-pre-border: #e0e0e0; + --mt-pre-color: #2f2f31; + --mt-hr: #dcdcdc; + --mt-drag-handle-hover: #5c5c5e; + + --mt-accent-bold-blue: #05c; + --mt-accent-bold-teal: #206a83; + --mt-accent-bold-green: #216e4e; + --mt-accent-bold-orange: #a54800; + --mt-accent-bold-red: #ae2e24; + --mt-accent-bold-purple: #5e4db2; + + --mt-accent-gray: #758195; + --mt-accent-blue: #1d7afc; + --mt-accent-teal: #2898bd; + --mt-accent-green: #22a06b; + --mt-accent-orange: #fea362; + --mt-accent-red: #c9372c; + --mt-accent-purple: #8270db; + + --mt-accent-blue-subtler: #cce0ff; + --mt-accent-teal-subtler: #c6edfb; + --mt-accent-green-subtler: #baf3db; + --mt-accent-yellow-subtler: #f8e6a0; + --mt-accent-red-subtler: #ffd5d2; + --mt-accent-purple-subtler: #dfd8fd; + + --hljs-string: #aa430f; + --hljs-title: #b08836; + --hljs-comment: #999999; + --hljs-keyword: #0c5eb1; + --hljs-attr: #3a92bc; + --hljs-literal: #c82b0f; + --hljs-name: #259792; + --hljs-selector-tag: #c8500f; + --hljs-number: #3da067; +} + +.dark { + --mt-overlay: rgba(31, 32, 35, 0.75); + --mt-transparent-foreground: rgba(255, 255, 255, 0.4); + --mt-bg-secondary: rgba(31, 32, 35, 0.8); + --mt-code-background: #ffffff13; + --mt-code-color: #2c2e33; + --mt-secondary: #595a5c; + --mt-pre-background: #080808; + --mt-pre-border: #23252a; + --mt-pre-color: #e3e4e6; + --mt-hr: #26282d; + --mt-drag-handle-hover: #969799; + + --mt-accent-bold-blue: #85b8ff; + --mt-accent-bold-teal: #9dd9ee; + --mt-accent-bold-green: #7ee2b8; + --mt-accent-bold-orange: #fec195; + --mt-accent-bold-red: #fd9891; + --mt-accent-bold-purple: #b8acf6; + + --mt-accent-gray: #738496; + --mt-accent-blue: #388bff; + --mt-accent-teal: #42b2d7; + --mt-accent-green: #2abb7f; + --mt-accent-orange: #a54800; + --mt-accent-red: #e2483d; + --mt-accent-purple: #8f7ee7; + + --mt-accent-blue-subtler: #09326c; + --mt-accent-teal-subtler: #164555; + --mt-accent-green-subtler: #164b35; + --mt-accent-yellow-subtler: #533f04; + --mt-accent-red-subtler: #5d1f1a; + --mt-accent-purple-subtler: #352c63; + + --hljs-string: #da936b; + --hljs-title: #f1d59d; + --hljs-comment: #aaaaaa; + --hljs-keyword: #6699cc; + --hljs-attr: #90cae8; + --hljs-literal: #f2777a; + --hljs-name: #5fc0a0; + --hljs-selector-tag: #e8c785; + --hljs-number: #b6e7b6; +} + +.minimal-tiptap-editor .ProseMirror { + @apply flex max-w-full cursor-text flex-col; + @apply z-0 outline-0; +} + +.minimal-tiptap-editor .ProseMirror > div.editor { + @apply block flex-1 whitespace-pre-wrap; +} + +.minimal-tiptap-editor .ProseMirror .block-node:not(:last-child), +.minimal-tiptap-editor .ProseMirror .list-node:not(:last-child), +.minimal-tiptap-editor .ProseMirror .text-node:not(:last-child) { + @apply mb-2.5; +} + +.minimal-tiptap-editor .ProseMirror ol, +.minimal-tiptap-editor .ProseMirror ul { + @apply pl-6; +} + +.minimal-tiptap-editor .ProseMirror blockquote, +.minimal-tiptap-editor .ProseMirror dl, +.minimal-tiptap-editor .ProseMirror ol, +.minimal-tiptap-editor .ProseMirror p, +.minimal-tiptap-editor .ProseMirror pre, +.minimal-tiptap-editor .ProseMirror ul { + @apply m-0; +} + +.minimal-tiptap-editor .ProseMirror li { + @apply leading-7; +} + +.minimal-tiptap-editor .ProseMirror p { + @apply break-words; +} + +.minimal-tiptap-editor .ProseMirror li .text-node:has(+ .list-node), +.minimal-tiptap-editor .ProseMirror li > .list-node, +.minimal-tiptap-editor .ProseMirror li > .text-node, +.minimal-tiptap-editor .ProseMirror li p { + @apply mb-0; +} + +.minimal-tiptap-editor .ProseMirror blockquote { + @apply relative pl-3.5; +} + +.minimal-tiptap-editor .ProseMirror blockquote::before, +.minimal-tiptap-editor .ProseMirror blockquote.is-empty::before { + @apply absolute bottom-0 left-0 top-0 h-full w-1 rounded-sm bg-accent-foreground/15 content-['']; +} + +.minimal-tiptap-editor .ProseMirror hr { + @apply my-3 h-0.5 w-full border-none bg-[var(--mt-hr)]; +} + +.minimal-tiptap-editor .ProseMirror-focused hr.ProseMirror-selectednode { + @apply rounded-full outline outline-2 outline-offset-1 outline-muted-foreground; +} + +.minimal-tiptap-editor .ProseMirror .ProseMirror-gapcursor { + @apply pointer-events-none absolute hidden; +} + +.minimal-tiptap-editor .ProseMirror .ProseMirror-hideselection { + @apply caret-transparent; +} + +.minimal-tiptap-editor .ProseMirror.resize-cursor { + @apply cursor-col-resize; +} + +.minimal-tiptap-editor .ProseMirror .selection { + @apply inline-block; +} + +.minimal-tiptap-editor .ProseMirror s span { + @apply line-through; +} + +.minimal-tiptap-editor .ProseMirror .selection, +.minimal-tiptap-editor .ProseMirror *::selection, +::selection { + @apply bg-primary/25; +} + +/* Override native selection when custom selection is present */ +.minimal-tiptap-editor .ProseMirror .selection::selection { + background: transparent; +} diff --git a/packages/tiptap/src/styles/partials/code.css b/packages/tiptap/src/styles/partials/code.css new file mode 100644 index 0000000..b1d03ea --- /dev/null +++ b/packages/tiptap/src/styles/partials/code.css @@ -0,0 +1,86 @@ +.minimal-tiptap-editor .ProseMirror code.inline { + @apply rounded border border-[var(--mt-code-color)] bg-[var(--mt-code-background)] px-1 py-0.5 text-sm; +} + +.minimal-tiptap-editor .ProseMirror pre { + @apply relative overflow-auto rounded border font-mono text-sm; + @apply border-[var(--mt-pre-border)] bg-[var(--mt-pre-background)] text-[var(--mt-pre-color)]; + @apply hyphens-none whitespace-pre text-left; +} + +.minimal-tiptap-editor .ProseMirror code { + @apply break-words leading-[1.7em]; +} + +.minimal-tiptap-editor .ProseMirror pre code { + @apply block overflow-x-auto p-3.5; +} + +.minimal-tiptap-editor .ProseMirror pre { + .hljs-keyword, + .hljs-operator, + .hljs-function, + .hljs-built_in, + .hljs-builtin-name { + color: var(--hljs-keyword); + } + + .hljs-attr, + .hljs-symbol, + .hljs-property, + .hljs-attribute, + .hljs-variable, + .hljs-template-variable, + .hljs-params { + color: var(--hljs-attr); + } + + .hljs-name, + .hljs-regexp, + .hljs-link, + .hljs-type, + .hljs-addition { + color: var(--hljs-name); + } + + .hljs-string, + .hljs-bullet { + color: var(--hljs-string); + } + + .hljs-title, + .hljs-subst, + .hljs-section { + color: var(--hljs-title); + } + + .hljs-literal, + .hljs-type, + .hljs-deletion { + color: var(--hljs-literal); + } + + .hljs-selector-tag, + .hljs-selector-id, + .hljs-selector-class { + color: var(--hljs-selector-tag); + } + + .hljs-number { + color: var(--hljs-number); + } + + .hljs-comment, + .hljs-meta, + .hljs-quote { + color: var(--hljs-comment); + } + + .hljs-emphasis { + @apply italic; + } + + .hljs-strong { + @apply font-bold; + } +} diff --git a/packages/tiptap/src/styles/partials/lists.css b/packages/tiptap/src/styles/partials/lists.css new file mode 100644 index 0000000..0525860 --- /dev/null +++ b/packages/tiptap/src/styles/partials/lists.css @@ -0,0 +1,23 @@ +.minimal-tiptap-editor .ProseMirror ol { + @apply list-decimal; +} + +.minimal-tiptap-editor .ProseMirror ol ol { + list-style: lower-alpha; +} + +.minimal-tiptap-editor .ProseMirror ol ol ol { + list-style: lower-roman; +} + +.minimal-tiptap-editor .ProseMirror ul { + list-style: disc; +} + +.minimal-tiptap-editor .ProseMirror ul ul { + list-style: circle; +} + +.minimal-tiptap-editor .ProseMirror ul ul ul { + list-style: square; +} diff --git a/packages/tiptap/src/styles/partials/placeholder.css b/packages/tiptap/src/styles/partials/placeholder.css new file mode 100644 index 0000000..04bcfdf --- /dev/null +++ b/packages/tiptap/src/styles/partials/placeholder.css @@ -0,0 +1,4 @@ +.minimal-tiptap-editor .ProseMirror > p.is-editor-empty::before { + content: attr(data-placeholder); + @apply pointer-events-none float-left h-0 text-[var(--mt-secondary)]; +} diff --git a/packages/tiptap/src/styles/partials/typography.css b/packages/tiptap/src/styles/partials/typography.css new file mode 100644 index 0000000..eae53b5 --- /dev/null +++ b/packages/tiptap/src/styles/partials/typography.css @@ -0,0 +1,39 @@ +.minimal-tiptap-editor .ProseMirror .heading-node { + @apply relative font-semibold; +} + +.minimal-tiptap-editor .ProseMirror .heading-node:first-child { + @apply mt-0; +} + +.minimal-tiptap-editor .ProseMirror h1 { + @apply mb-4 mt-[46px] text-[1.375rem] leading-7 tracking-[-0.004375rem]; +} + +.minimal-tiptap-editor .ProseMirror h2 { + @apply mb-3.5 mt-8 text-[1.1875rem] leading-7 tracking-[0.003125rem]; +} + +.minimal-tiptap-editor .ProseMirror h3 { + @apply mb-3 mt-6 text-[1.0625rem] leading-6 tracking-[0.00625rem]; +} + +.minimal-tiptap-editor .ProseMirror h4 { + @apply mb-2 mt-4 text-[0.9375rem] leading-6; +} + +.minimal-tiptap-editor .ProseMirror h5 { + @apply mb-2 mt-4 text-sm; +} + +.minimal-tiptap-editor .ProseMirror h5 { + @apply mb-2 mt-4 text-sm; +} + +.minimal-tiptap-editor .ProseMirror a.link { + @apply cursor-pointer text-primary; +} + +.minimal-tiptap-editor .ProseMirror a.link:hover { + @apply underline; +} diff --git a/packages/tiptap/src/styles/partials/zoom.css b/packages/tiptap/src/styles/partials/zoom.css new file mode 100644 index 0000000..55684e2 --- /dev/null +++ b/packages/tiptap/src/styles/partials/zoom.css @@ -0,0 +1,94 @@ +[data-rmiz-ghost] { + position: absolute; + pointer-events: none; +} +[data-rmiz-btn-zoom], +[data-rmiz-btn-unzoom] { + background-color: rgba(0, 0, 0, 0.7); + border-radius: 50%; + border: none; + box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); + color: #fff; + height: 40px; + margin: 0; + outline-offset: 2px; + padding: 9px; + touch-action: manipulation; + width: 40px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +[data-rmiz-btn-zoom]:not(:focus):not(:active) { + position: absolute; + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + pointer-events: none; + white-space: nowrap; + width: 1px; +} +[data-rmiz-btn-zoom] { + position: absolute; + inset: 10px 10px auto auto; + cursor: zoom-in; +} +[data-rmiz-btn-unzoom] { + position: absolute; + inset: 20px 20px auto auto; + cursor: zoom-out; + z-index: 1; +} +[data-rmiz-content='found'] img, +[data-rmiz-content='found'] svg, +[data-rmiz-content='found'] [role='img'], +[data-rmiz-content='found'] [data-zoom] { + cursor: inherit; +} +[data-rmiz-modal]::backdrop { + display: none; +} +[data-rmiz-modal][open] { + position: fixed; + width: 100vw; + width: 100dvw; + height: 100vh; + height: 100dvh; + max-width: none; + max-height: none; + margin: 0; + padding: 0; + border: 0; + background: transparent; + overflow: hidden; +} +[data-rmiz-modal-overlay] { + position: absolute; + inset: 0; + transition: background-color 0.3s; +} +[data-rmiz-modal-overlay='hidden'] { + background-color: rgba(255, 255, 255, 0); +} +[data-rmiz-modal-overlay='visible'] { + background-color: rgba(255, 255, 255, 1); +} +[data-rmiz-modal-content] { + position: relative; + width: 100%; + height: 100%; +} +[data-rmiz-modal-img] { + position: absolute; + cursor: zoom-out; + image-rendering: high-quality; + transform-origin: top left; + transition: transform 0.3s; +} +@media (prefers-reduced-motion: reduce) { + [data-rmiz-modal-overlay], + [data-rmiz-modal-img] { + transition-duration: 0.01ms !important; + } +} diff --git a/packages/tiptap/src/types.ts b/packages/tiptap/src/types.ts new file mode 100644 index 0000000..1c8f5ca --- /dev/null +++ b/packages/tiptap/src/types.ts @@ -0,0 +1,28 @@ +import type { EditorState } from "@tiptap/pm/state" +import type { EditorView } from "@tiptap/pm/view" +import type { Editor } from "@tiptap/react" + +export interface LinkProps { + url: string + text?: string + openInNewTab?: boolean +} + +export interface ShouldShowProps { + editor: Editor + view: EditorView + state: EditorState + oldState?: EditorState + from: number + to: number +} + +export interface FormatAction { + label: string + icon?: React.ReactNode + action: (editor: Editor) => void + isActive: (editor: Editor) => boolean + canExecute: (editor: Editor) => boolean + shortcuts: string[] + value: string +} diff --git a/packages/tiptap/src/utils.ts b/packages/tiptap/src/utils.ts new file mode 100644 index 0000000..7006a55 --- /dev/null +++ b/packages/tiptap/src/utils.ts @@ -0,0 +1,212 @@ +import type { Editor } from "@tiptap/react" + +import type { MinimalTiptapProps } from "./minimal-tiptap" + +type ShortcutKeyResult = { + symbol: string + readable: string +} + +export type FileError = { + file: File | string + reason: "type" | "size" | "invalidBase64" | "base64NotAllowed" +} + +export type FileValidationOptions = { + allowedMimeTypes: string[] + maxFileSize?: number + allowBase64: boolean +} + +type FileInput = File | { src: string | File, alt?: string, title?: string } + +export const isClient = (): boolean => typeof window !== "undefined" +export const isServer = (): boolean => !isClient() +export const isMacOS = (): boolean => isClient() && window.navigator.platform === "MacIntel" + +const shortcutKeyMap: Record = { + mod: isMacOS() ? { symbol: "⌘", readable: "Command" } : { symbol: "Ctrl", readable: "Control" }, + alt: isMacOS() ? { symbol: "⌥", readable: "Option" } : { symbol: "Alt", readable: "Alt" }, + shift: { symbol: "⇧", readable: "Shift" }, +} + +export const getShortcutKey = (key: string): ShortcutKeyResult => + shortcutKeyMap[key.toLowerCase()] || { symbol: key, readable: key } + +export const getShortcutKeys = (keys: string[]): ShortcutKeyResult[] => keys.map((element) => getShortcutKey(element)) + +export const getOutput = (editor: Editor, format: MinimalTiptapProps["output"]): object | string => { + switch (format) { + case "json": { + return editor.getJSON() + } + case "html": { + return editor.getText() ? editor.getHTML() : "" + } + default: { + return editor.getText() + } + } +} + +export const isUrl = ( + text: string, + options: { requireHostname: boolean, allowBase64?: boolean }, +): boolean => { + const { requireHostname = false } = options + if (text.includes("\n")) return false + + try { + const url = new URL(text) + const blockedProtocols = ["javascript:", "file:", "vbscript:", ...(options.allowBase64 ? [] : ["data:"])] + + if (blockedProtocols.includes(url.protocol)) return false + if (options.allowBase64 && url.protocol === "data:") return /^data:image\/[a-z]+;base64,/.test(text) + if (url.hostname) return true + + return ( + url.protocol !== "" && + (url.pathname.startsWith("//") || url.pathname.startsWith("http")) && + !requireHostname + ) + } catch { + return false + } +} + +export const sanitizeUrl = ( + url: string | null | undefined, + options: { allowBase64?: boolean } = {}, +): string | undefined => { + if (!url) return undefined + + if (options.allowBase64 && url.startsWith("data:image")) { + return isUrl(url, { requireHostname: false, allowBase64: true }) ? url : undefined + } + + return isUrl(url, { requireHostname: false, allowBase64: options.allowBase64 }) || + /^(?:\/|#|mailto:|sms:|fax:|tel:)/.test(url) ? + url : + `https://${url}` +} + +export const blobUrlToBase64 = async (blobUrl: string): Promise => { + const response = await fetch(blobUrl) + const blob = await response.blob() + + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => { + if (typeof reader.result === "string") { + resolve(reader.result) + } else { + reject(new Error("Failed to convert Blob to base64")) + } + } + reader.onerror = reject + reader.readAsDataURL(blob) + }) +} + +export const randomId = (): string => Math.random().toString(36).slice(2, 11) + +export const fileToBase64 = (file: File | Blob): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => { + if (typeof reader.result === "string") { + resolve(reader.result) + } else { + reject(new Error("Failed to convert File to base64")) + } + } + reader.onerror = reject + reader.readAsDataURL(file) + }) +} + +const validateFileOrBase64 = ( + input: File | string, + options: FileValidationOptions, + originalFile: T, + validFiles: T[], + errors: FileError[], +): void => { + const { isValidType, isValidSize } = checkTypeAndSize(input, options) + + if (isValidType && isValidSize) { + validFiles.push(originalFile) + } else { + if (!isValidType) errors.push({ file: input, reason: "type" }) + if (!isValidSize) errors.push({ file: input, reason: "size" }) + } +} + +const checkTypeAndSize = ( + input: File | string, + { allowedMimeTypes, maxFileSize }: FileValidationOptions, +): { isValidType: boolean, isValidSize: boolean } => { + const mimeType = input instanceof File ? input.type : base64MimeType(input) + const size = input instanceof File ? input.size : atob(input.split(",")[1]).length + + const isValidType = + allowedMimeTypes.length === 0 || + allowedMimeTypes.includes(mimeType) || + allowedMimeTypes.includes(`${mimeType.split("/")[0]}/*`) + + const isValidSize = !maxFileSize || size <= maxFileSize + + return { isValidType, isValidSize } +} + +const base64MimeType = (encoded: string): string => { + const result = encoded.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]).*,.*/) + if (!result) return "unknown" + return result.length > 1 ? result[1]! : "unknown" +} + +const isBase64 = (str: string): boolean => { + if (str.startsWith("data:")) { + const matches = str.match(/^data:[^;]+;base64,(.+)$/) + if (matches && matches[1]) { + str = matches[1] + } else { + return false + } + } + + try { + return btoa(atob(str)) === str + } catch { + return false + } +} + +export const filterFiles = (files: T[], options: FileValidationOptions): [T[], FileError[]] => { + const validFiles: T[] = [] + const errors: FileError[] = [] + + files.forEach((file) => { + const actualFile = "src" in file ? file.src : file + + if (actualFile instanceof File) { + validateFileOrBase64(actualFile, options, file, validFiles, errors) + } else if (typeof actualFile === "string") { + if (isBase64(actualFile)) { + if (options.allowBase64) { + validateFileOrBase64(actualFile, options, file, validFiles, errors) + } else { + errors.push({ file: actualFile, reason: "base64NotAllowed" }) + } + } else { + if (!sanitizeUrl(actualFile, { allowBase64: options.allowBase64 })) { + errors.push({ file: actualFile, reason: "invalidBase64" }) + } else { + validFiles.push(file) + } + } + } + }) + + return [validFiles, errors] +} diff --git a/packages/tiptap/tsconfig.json b/packages/tiptap/tsconfig.json new file mode 100644 index 0000000..e166bc4 --- /dev/null +++ b/packages/tiptap/tsconfig.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "module": "preserve", + "noEmit": true, + "lib": ["es2022", "dom", "dom.iterable"], + "jsx": "react-jsx", + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": false, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@/components/*": ["../../packages/ui/src/components/*"] + } + }, + "include": ["src/**/*", "tailwind.config.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index cec99e2..11d0f0d 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -18,7 +18,8 @@ "verbatimModuleSyntax": false, "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@/components/*": ["./src/components/*"] }, }, "include": ["src/**/*", "tailwind.config.ts"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 795b4a5..cc0c825 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: '@repo/pro-table': specifier: workspace:* version: link:../../packages/pro-table + '@repo/tiptap': + specifier: workspace:* + version: link:../../packages/tiptap '@repo/ui': specifier: workspace:* version: link:../../packages/ui @@ -78,6 +81,9 @@ importers: '@tanstack/react-table': specifier: ^8.20.5 version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tiptap/react': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -533,6 +539,78 @@ importers: specifier: ^5.0.1 version: 5.0.1(typescript@5.6.3)(vite@5.4.10(@types/node@22.9.3)(lightningcss@1.25.1)(terser@5.31.6)) + packages/tiptap: + dependencies: + '@radix-ui/react-icons': + specifier: ^1.3.2 + version: 1.3.2(react@18.3.1) + '@repo/ui': + specifier: workspace:* + version: link:../ui + '@tiptap/extension-code-block-lowlight': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/extension-code-block@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)(highlight.js@11.10.0)(lowlight@3.2.0) + '@tiptap/extension-color': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/extension-text-style@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))) + '@tiptap/extension-heading': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-horizontal-rule': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/extension-image': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-link': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/extension-placeholder': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/extension-text-style': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-typography': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-underline': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/pm': + specifier: ^2.10.3 + version: 2.10.3 + '@tiptap/react': + specifier: ^2.10.3 + version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)(react-dom@19.0.0-rc-66855b96-20241106(react@18.3.1))(react@18.3.1) + '@tiptap/starter-kit': + specifier: ^2.10.3 + version: 2.10.3 + '@types/react': + specifier: ^18.3.12 + version: 18.3.12 + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lowlight: + specifier: ^3.2.0 + version: 3.2.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-medium-image-zoom: + specifier: ^5.2.11 + version: 5.2.11(react-dom@19.0.0-rc-66855b96-20241106(react@18.3.1))(react@18.3.1) + sonner: + specifier: ^1.6.1 + version: 1.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^2.5.4 + version: 2.5.4 + packages/ui: dependencies: '@hookform/resolvers': @@ -540,85 +618,85 @@ importers: version: 3.9.1(react-hook-form@7.53.1(react@18.3.1)) '@radix-ui/react-accordion': specifier: ^1.2.1 - version: 1.2.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: 1.2.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-alert-dialog': specifier: ^1.1.2 - version: 1.1.2(@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: 1.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-aspect-ratio': specifier: ^1.1.0 - version: 1.1.0(@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: 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-avatar': specifier: ^1.1.1 - version: 1.1.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: 1.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-checkbox': specifier: ^1.1.2 - version: 1.1.2(@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: 1.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-collapsible': specifier: ^1.1.1 - version: 1.1.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: 1.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-context-menu': specifier: ^2.2.2 - version: 2.2.2(@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: 2.2.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': specifier: ^2.1.2 - version: 2.1.2(@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: 2.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-hover-card': specifier: ^1.1.2 - version: 1.1.2(@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: 1.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-label': specifier: ^2.1.0 - version: 2.1.0(@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: 2.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-menubar': specifier: ^1.1.2 - version: 1.1.2(@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: 1.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-navigation-menu': specifier: ^1.2.1 - version: 1.2.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: 1.2.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-popover': specifier: ^1.1.2 - version: 1.1.2(@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: 1.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-progress': specifier: ^1.1.0 - version: 1.1.0(@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: 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-radio-group': specifier: ^1.2.1 - version: 1.2.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: 1.2.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.2.0 - version: 1.2.0(@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: 1.2.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': specifier: ^2.1.2 - version: 2.1.2(@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: 2.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-separator': specifier: ^1.1.0 - version: 1.1.0(@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: 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slider': specifier: ^1.2.1 - version: 1.2.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: 1.2.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-switch': specifier: ^1.1.1 - version: 1.1.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: 1.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tabs': specifier: ^1.1.1 - version: 1.1.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: 1.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-toast': specifier: ^1.2.2 - version: 1.2.2(@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: 1.2.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-toggle': specifier: ^1.1.0 - version: 1.1.0(@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: 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-toggle-group': specifier: ^1.1.0 - version: 1.1.0(@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: 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.1.3 - version: 1.1.3(@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: 1.1.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tailwindcss/typography': specifier: ^0.5.15 version: 0.5.15(tailwindcss@3.4.14) @@ -633,7 +711,7 @@ importers: version: 2.1.1 cmdk: specifier: 1.0.0 - version: 1.0.0(@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: 1.0.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -2296,6 +2374,9 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@radix-ui/number@1.1.0': resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} @@ -2373,8 +2454,8 @@ packages: '@radix-ui/react-checkbox@1.1.2': resolution: {integrity: sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==} peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' + '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -3079,6 +3160,9 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@remix-run/router@1.20.0': resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==} engines: {node: '>=14.0.0'} @@ -3727,6 +3811,179 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tiptap/core@2.10.3': + resolution: {integrity: sha512-wAG/0/UsLeZLmshWb6rtWNXKJftcmnned91/HLccHVQAuQZ1UWH+wXeQKu/mtodxEO7JcU2mVPR9mLGQkK0McQ==} + peerDependencies: + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-blockquote@2.10.3': + resolution: {integrity: sha512-u9Mq4r8KzoeGVT8ms6FQDIMN95dTh3TYcT7fZpwcVM96mIl2Oyt+Bk66mL8z4zuFptfRI57Cu9QdnHEeILd//w==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bold@2.10.3': + resolution: {integrity: sha512-xnF1tS2BsORenr11qyybW120gHaeHKiKq+ZOP14cGA0MsriKvWDnaCSocXP/xMEYHy7+2uUhJ0MsKkHVj4bPzQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bubble-menu@2.10.3': + resolution: {integrity: sha512-e9a4yMjQezuKy0rtyyzxbV2IAE1bm1PY3yoZEFrcaY0o47g1CMUn2Hwe+9As2HdntEjQpWR7NO1mZeKxHlBPYA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-bullet-list@2.10.3': + resolution: {integrity: sha512-PTkwJOVlHi4RR4Wrs044tKMceweXwNmWA6EoQ93hPUVtQcwQL990Es5Izp+i88twTPLuGD9dH+o9QDyH9SkWdA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-code-block-lowlight@2.10.3': + resolution: {integrity: sha512-ieRSdfDW06pmKcsh73N506/EWNJrpMrZzyuFx3YGJtfM+Os0a9hMLy2TSuNleyRsihBi5mb+zvdeqeGdaJm7Ng==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-code-block': ^2.7.0 + '@tiptap/pm': ^2.7.0 + highlight.js: ^11 + lowlight: ^2 || ^3 + + '@tiptap/extension-code-block@2.10.3': + resolution: {integrity: sha512-yiDVNg22fYkzsFk5kBlDSHcjwVJgajvO/M5fDXA+Hfxwo2oNcG6aJyyHXFe+UaXTVjdkPej0J6kcMKrTMCiFug==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-code@2.10.3': + resolution: {integrity: sha512-JyLbfyY3cPctq9sVdpcRWTcoUOoq3/MnGE1eP6eBNyMTHyBPcM9TPhOkgj+xkD1zW/884jfelB+wa70RT/AMxQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-color@2.10.3': + resolution: {integrity: sha512-FC2hPMSQ4w9UmO9kJCAdoU7gHpDbJ6MeJAmikB9EPp16dbGwFLrZm9TZ/4pv74fGfVm0lv720316ALOEgPEDjQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-text-style': ^2.7.0 + + '@tiptap/extension-document@2.10.3': + resolution: {integrity: sha512-6i8+xbS2zB6t8iFzli1O/QB01MmwyI5Hqiiv4m5lOxqavmJwLss2sRhoMC2hB3CyFg5UmeODy/f/RnI6q5Vixg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-dropcursor@2.10.3': + resolution: {integrity: sha512-wzWf82ixWzZQr0hxcf/A0ul8NNxgy1N63O+c56st6OomoLuKUJWOXF+cs9O7V+/5rZKWdbdYYoRB5QLvnDBAlQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-floating-menu@2.10.3': + resolution: {integrity: sha512-Prg8rYLxeyzHxfzVu1mDkkUWMnD9ZN3y370O/1qy55e+XKVw9jFkTSuz0y0+OhMJG6bulYpDUMtb+N3+2xOWlQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-gapcursor@2.10.3': + resolution: {integrity: sha512-FskZi2DqDSTH1WkgLF2OLy0xU7qj3AgHsKhVsryeAtld4jAK5EsonneWgaipbz0e/MxuIvc1oyacfZKABpLaNg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-hard-break@2.10.3': + resolution: {integrity: sha512-2rFlimUKAgKDwT6nqAMtPBjkrknQY8S7oBNyIcDOUGyFkvbDUl3Jd0PiC929S5F3XStJRppnMqhpNDAlWmvBLA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-heading@2.10.3': + resolution: {integrity: sha512-AlxXXPCWIvw8hQUDFRskasj32iMNB8Sb19VgyFWqwvntGs2/UffNu8VdsVqxD2HpZ0g5rLYCYtSW4wigs9R3og==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-history@2.10.3': + resolution: {integrity: sha512-HaSiMdx9Im9Pb9qGlVud7W8bweRDRMez33Uzs5a2x0n1RWkelfH7TwYs41Y3wus8Ujs7kw6qh7jyhvPpQBKaSA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-horizontal-rule@2.10.3': + resolution: {integrity: sha512-1a2IWhD00tgUNg/91RLnBvfENL7DLCui5L245+smcaLu+OXOOEpoBHawx59/M4hEpsjqvRRM79TzO9YXfopsPw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-image@2.10.3': + resolution: {integrity: sha512-YIjAF5CwDkMe28OQ5pvnmdRgbJ9JcGMIHY1kyqNunSf2iwphK+6SWz9UEIkDFiT7AsRZySqxFSq93iK1XyTifw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-italic@2.10.3': + resolution: {integrity: sha512-wAiO6ZxoHx2H90phnKttLWGPjPZXrfKxhOCsqYrK8BpRByhr48godOFRuGwYnKaiwoVjpxc63t+kDJDWvqmgMw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-link@2.10.3': + resolution: {integrity: sha512-8esKlkZBzEiNcpt7I8Cd6l1mWmCc/66pPbUq9LfnIniDXE3U+ahBf4m3TJltYFBGbiiTR/xqMtJyVHOpuLDtAw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-list-item@2.10.3': + resolution: {integrity: sha512-9sok81gvZfSta2K1Dwrq5/HSz1jk4zHBpFqCx0oydzodGslx6X1bNxdca+eXJpXZmQIWALK7zEr4X8kg3WZsgw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-ordered-list@2.10.3': + resolution: {integrity: sha512-/SFuEDnbJxy3jvi72LeyiPHWkV+uFc0LUHTUHSh20vwyy+tLrzncJfXohGbTIv5YxYhzExQYZDRD4VbSghKdlw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-paragraph@2.10.3': + resolution: {integrity: sha512-sNkTX/iN+YoleDiTJsrWSBw9D7c4vsYwnW5y/G5ydfuJMIRQMF78pWSIWZFDRNOMkgK5UHkhu9anrbCFYgBfaA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-placeholder@2.10.3': + resolution: {integrity: sha512-0OkwnDLguZgoiJM85cfnOySuMmPUF7qqw7DHQ+c3zwTAYnvzpvqrvpupc+2Zi9GfC1sDgr+Ajrp8imBHa6PHfA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-strike@2.10.3': + resolution: {integrity: sha512-jYoPy6F6njYp3txF3u23bgdRy/S5ATcWDO9LPZLHSeikwQfJ47nqb+EUNo5M8jIOgFBTn4MEbhuZ6OGyhnxopA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text-style@2.10.3': + resolution: {integrity: sha512-TalYIdlF7vBA4afFhmido7AORdBbu3sV+HCByda0FiNbM6cjng3Nr9oxHOCVJy+ChqrcgF4m54zDfLmamdyu5Q==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text@2.10.3': + resolution: {integrity: sha512-7p9XiRprsRZm8y9jvF/sS929FCELJ5N9FQnbzikOiyGNUx5mdI+exVZlfvBr9xOD5s7fBLg6jj9Vs0fXPNRkPg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-typography@2.10.3': + resolution: {integrity: sha512-lLUm6PSufACffAFQaK3bwoM3nFlQ/RdG21a3rKOoLWh+abYvIZ8UilYgebH9r2+DBET6UrG7I/0mBtm+L/Lheg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-underline@2.10.3': + resolution: {integrity: sha512-VeGs0jeNiTnXddHHJEgOc/sKljZiyTEgSSuqMmsBACrr9aGFXbLTgKTvNjkZ9WzSnu7LwgJuBrwEhg8yYixUyQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/pm@2.10.3': + resolution: {integrity: sha512-771p53aU0KFvujvKpngvq2uAxThlEsjYaXcVVmwrhf0vxSSg+psKQEvqvWvHv/3BwkPVCGwmEKNVJZjaXFKu4g==} + + '@tiptap/react@2.10.3': + resolution: {integrity: sha512-5GBL3arWai8WZuCl1MMA7bT5aWwqDi5AOQhX+hovKjwHvttpKDogRoUBL5k6Eds/eQMBMGTpsfmZlGNiFxSv1g==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@2.10.3': + resolution: {integrity: sha512-oq8xdVIMqohSs91ofHSr7i5dCp2F56Lb9aYIAI25lZmwNwQJL2geGOYjMSfL0IC4cQHPylIuSKYCg7vRFdZmAA==} + '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} @@ -3874,6 +4131,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} @@ -4808,6 +5068,9 @@ packages: typescript: optional: true + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -6058,6 +6321,10 @@ packages: hermes-parser@0.20.1: resolution: {integrity: sha512-BL5P83cwCogI8D7rrDCgsFY0tdYUtmFP9XaXtl2IQjC+2Xo+4okjfXintlTxcIwl4qeGddEl28Z11kbVIw0aNA==} + highlight.js@11.10.0: + resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==} + engines: {node: '>=12.0.0'} + hono@4.6.11: resolution: {integrity: sha512-f0LwJQFKdUUrCUAVowxSvNCjyzI7ZLt8XWYU/EApyeq5FfOvHFarBaE5rjU9HTNFk4RI0FkdB2edb3p/7xZjzQ==} engines: {node: '>=16.9.0'} @@ -6702,6 +6969,12 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + linkifyjs@4.1.4: + resolution: {integrity: sha512-0/NxkHNpiJ0k9VrYCkAn9OtU1eu8xEr1tCCpDtSsVRm/SF0xAak2Gzv3QimSfgUgqLBCDlfhMbu73XvaEHUTPQ==} + lint-staged@15.2.10: resolution: {integrity: sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==} engines: {node: '>=18.12.0'} @@ -6810,6 +7083,9 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lowlight@3.2.0: + resolution: {integrity: sha512-8Me8xHTCBYEXwcJIPcurnXTeERl3plwb4207v6KPye48kX/oaYDiwXy+OCm3M/pyAPUrkMhalKsbYPm24f/UDg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -6859,6 +7135,10 @@ packages: mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + markdown-table@3.0.3: resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} @@ -6907,6 +7187,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + memoizerific@1.11.3: resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} @@ -7309,6 +7592,9 @@ packages: resolution: {integrity: sha512-GQEkNkH/GHOhPFXcqZs3IDahXEQcQxsSjEkK4KvEEST4t7eNzoMjxTzef+EZ+JluDEV+Raoi3WQ2CflnRdSVnQ==} engines: {node: '>=18'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + os-name@5.1.0: resolution: {integrity: sha512-YEIoAnM6zFmzw3PQ201gCVCIWbXNyKObGlVvpAVvraAeOHnlYVKFssbA/riRX5R40WA6kKrZ7Dr7dWzO3nKSeQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -7666,6 +7952,64 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + prosemirror-changeset@2.2.1: + resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.6.2: + resolution: {integrity: sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==} + + prosemirror-dropcursor@1.8.1: + resolution: {integrity: sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==} + + prosemirror-gapcursor@1.3.2: + resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} + + prosemirror-history@1.4.1: + resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} + + prosemirror-inputrules@1.4.0: + resolution: {integrity: sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==} + + prosemirror-keymap@1.2.2: + resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==} + + prosemirror-markdown@1.13.1: + resolution: {integrity: sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==} + + prosemirror-menu@1.2.4: + resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==} + + prosemirror-model@1.24.0: + resolution: {integrity: sha512-Ft7epNnycoQSM+2ObF35SBbBX+5WY39v8amVlrtlAcpglhlHs2tCTnWl7RX5tbp/PsMKcRcWV9cXPuoBWq0AIQ==} + + prosemirror-schema-basic@1.2.3: + resolution: {integrity: sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==} + + prosemirror-schema-list@1.4.1: + resolution: {integrity: sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==} + + prosemirror-state@1.4.3: + resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + + prosemirror-tables@1.6.1: + resolution: {integrity: sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.10.2: + resolution: {integrity: sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==} + + prosemirror-view@1.37.0: + resolution: {integrity: sha512-z2nkKI1sJzyi7T47Ji/ewBPuIma1RNvQCCYVdV+MqWBV7o4Sa1n94UJCJJ1aQRF/xRkFfyqLGlGFWitIcCOtbg==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -7679,6 +8023,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -7773,6 +8121,12 @@ packages: '@types/react': '>=18' react: '>=18' + react-medium-image-zoom@5.2.11: + resolution: {integrity: sha512-K3REdn96k2H+6iQlRSl7C7O5lMhdhRx3W1NFJXRar6wMeHpOwp5wI/6N0SfuF/NiKu+HIPxY0FSdvMIJwynTCw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-merge-refs@1.1.0: resolution: {integrity: sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==} @@ -8054,6 +8408,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -8524,6 +8881,9 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} @@ -8710,6 +9070,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -8812,6 +9175,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.2.2: + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -8956,6 +9324,9 @@ packages: typescript: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -10616,6 +10987,8 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@popperjs/core@2.11.8': {} + '@radix-ui/number@1.1.0': {} '@radix-ui/primitive@1.0.1': @@ -10624,11 +10997,11 @@ snapshots: '@radix-ui/primitive@1.1.0': {} - '@radix-ui/react-accordion@1.2.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)': + '@radix-ui/react-accordion@1.2.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collapsible': 1.1.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) - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collapsible': 1.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -10639,9 +11012,8 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-alert-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-alert-dialog@1.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -10653,27 +11025,24 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-arrow@1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-aspect-ratio@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-aspect-ratio@1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-avatar@1.1.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)': + '@radix-ui/react-avatar@1.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10683,9 +11052,8 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-checkbox@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-checkbox@1.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -10699,9 +11067,8 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-collapsible@1.1.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)': + '@radix-ui/react-collapsible@1.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -10715,9 +11082,8 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -10727,7 +11093,6 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.12)(react@18.3.1)': dependencies: @@ -10742,11 +11107,11 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@radix-ui/react-context-menu@2.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-context-menu@2.2.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-menu': 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menu': 2.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -10754,7 +11119,6 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-context@1.0.1(@types/react@18.3.12)(react@18.3.1)': dependencies: @@ -10775,19 +11139,19 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dialog@1.0.5(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.4(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.0.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.0.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) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.0.2(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.12)(react@18.3.1) aria-hidden: 1.2.4 @@ -10796,7 +11160,6 @@ snapshots: react-remove-scroll: 2.5.5(@types/react@18.3.12)(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-dialog@1.1.2(@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: @@ -10826,19 +11189,18 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.0.5(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-dismissable-layer@1.1.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: @@ -10853,20 +11215,19 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 - '@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dropdown-menu@2.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-menu': 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menu': 2.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.12)(react@18.3.1)': dependencies: @@ -10881,17 +11242,16 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.0.4(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-focus-scope@1.1.0(@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: @@ -10904,13 +11264,13 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 - '@radix-ui/react-hover-card@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-hover-card@1.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-dismissable-layer': 1.1.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) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-presence': 1.1.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) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10919,7 +11279,6 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-icons@1.3.2(react@18.3.1)': dependencies: @@ -10940,19 +11299,18 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-label@2.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-menu@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-menu@2.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -10960,11 +11318,11 @@ snapshots: '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-presence': 1.1.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) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) aria-hidden: 1.2.4 @@ -10973,30 +11331,28 @@ snapshots: react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-menubar@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-menubar@1.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-menu': 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menu': 2.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-navigation-menu@1.2.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)': + '@radix-ui/react-navigation-menu@1.2.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -11008,14 +11364,13 @@ snapshots: '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-popover@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popover@1.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -11024,7 +11379,7 @@ snapshots: '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-presence': 1.1.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) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11036,12 +11391,11 @@ snapshots: react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popper@1.2.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11054,17 +11408,15 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.0.4(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-portal@1.1.2(@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: @@ -11076,7 +11428,7 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 - '@radix-ui/react-presence@1.0.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)': + '@radix-ui/react-presence@1.0.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) @@ -11085,7 +11437,6 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-presence@1.1.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: @@ -11097,7 +11448,7 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 - '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@1.0.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 '@radix-ui/react-slot': 1.0.2(@types/react@18.3.12)(react@18.3.1) @@ -11105,7 +11456,6 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-primitive@2.0.0(@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: @@ -11116,7 +11466,7 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 - '@radix-ui/react-progress@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-progress@1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11124,9 +11474,8 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-radio-group@1.2.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)': + '@radix-ui/react-radio-group@1.2.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -11134,7 +11483,7 @@ snapshots: '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-presence': 1.1.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) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -11142,12 +11491,11 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -11159,9 +11507,8 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-scroll-area@1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-scroll-area@1.2.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.0 '@radix-ui/primitive': 1.1.0 @@ -11176,13 +11523,12 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-select@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-select@2.1.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.0 '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -11190,7 +11536,7 @@ snapshots: '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -11198,29 +11544,27 @@ snapshots: '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-separator@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-separator@1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-slider@1.2.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)': + '@radix-ui/react-slider@1.2.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.0 '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -11233,7 +11577,6 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-slot@1.0.2(@types/react@18.3.12)(react@18.3.1)': dependencies: @@ -11250,7 +11593,7 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@radix-ui/react-switch@1.1.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)': + '@radix-ui/react-switch@1.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -11263,9 +11606,8 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-tabs@1.1.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)': + '@radix-ui/react-tabs@1.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) @@ -11273,18 +11615,17 @@ snapshots: '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-presence': 1.1.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) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-toast@1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-toast@1.2.2(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-dismissable-layer': 1.1.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) @@ -11294,29 +11635,27 @@ snapshots: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-toggle-group@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-toggle-group@1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-toggle': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-toggle@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-toggle@1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11325,27 +11664,25 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - '@radix-ui/react-tooltip@1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tooltip@1.1.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-dismissable-layer': 1.1.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) '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-presence': 1.1.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) '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.12)(react@18.3.1)': dependencies: @@ -11423,17 +11760,18 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.1.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 '@radix-ui/rect@1.1.0': {} + '@remirror/core-constants@3.0.0': {} + '@remix-run/router@1.20.0': {} '@rollup/pluginutils@4.2.1': @@ -12077,6 +12415,208 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 + '@tiptap/core@2.10.3(@tiptap/pm@2.10.3)': + dependencies: + '@tiptap/pm': 2.10.3 + + '@tiptap/extension-blockquote@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-bold@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-bubble-menu@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + tippy.js: 6.3.7 + + '@tiptap/extension-bullet-list@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-code-block-lowlight@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/extension-code-block@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)(highlight.js@11.10.0)(lowlight@3.2.0)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/extension-code-block': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + highlight.js: 11.10.0 + lowlight: 3.2.0 + + '@tiptap/extension-code-block@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + + '@tiptap/extension-code@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-color@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/extension-text-style@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/extension-text-style': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + + '@tiptap/extension-document@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-dropcursor@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + + '@tiptap/extension-floating-menu@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + tippy.js: 6.3.7 + + '@tiptap/extension-gapcursor@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + + '@tiptap/extension-hard-break@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-heading@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-history@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + + '@tiptap/extension-horizontal-rule@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + + '@tiptap/extension-image@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-italic@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-link@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + linkifyjs: 4.1.4 + + '@tiptap/extension-list-item@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-ordered-list@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-paragraph@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-placeholder@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + + '@tiptap/extension-strike@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-text-style@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-text@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-typography@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/extension-underline@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + + '@tiptap/pm@2.10.3': + dependencies: + prosemirror-changeset: 2.2.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.6.2 + prosemirror-dropcursor: 1.8.1 + prosemirror-gapcursor: 1.3.2 + prosemirror-history: 1.4.1 + prosemirror-inputrules: 1.4.0 + prosemirror-keymap: 1.2.2 + prosemirror-markdown: 1.13.1 + prosemirror-menu: 1.2.4 + prosemirror-model: 1.24.0 + prosemirror-schema-basic: 1.2.3 + prosemirror-schema-list: 1.4.1 + prosemirror-state: 1.4.3 + prosemirror-tables: 1.6.1 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.24.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0) + prosemirror-transform: 1.10.2 + prosemirror-view: 1.37.0 + + '@tiptap/react@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/extension-bubble-menu': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/extension-floating-menu': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + '@types/use-sync-external-store': 0.0.6 + fast-deep-equal: 3.1.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.2.2(react@18.3.1) + + '@tiptap/react@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)(react-dom@19.0.0-rc-66855b96-20241106(react@18.3.1))(react@18.3.1)': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/extension-bubble-menu': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/extension-floating-menu': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/pm': 2.10.3 + '@types/use-sync-external-store': 0.0.6 + fast-deep-equal: 3.1.3 + react: 18.3.1 + react-dom: 19.0.0-rc-66855b96-20241106(react@18.3.1) + use-sync-external-store: 1.2.2(react@18.3.1) + + '@tiptap/starter-kit@2.10.3': + dependencies: + '@tiptap/core': 2.10.3(@tiptap/pm@2.10.3) + '@tiptap/extension-blockquote': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-bold': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-bullet-list': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-code': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-code-block': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/extension-document': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-dropcursor': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/extension-gapcursor': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/extension-hard-break': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-heading': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-history': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/extension-horizontal-rule': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3) + '@tiptap/extension-italic': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-list-item': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-ordered-list': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-paragraph': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-strike': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-text': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/extension-text-style': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3)) + '@tiptap/pm': 2.10.3 + '@tootallnate/quickjs-emscripten@0.23.0': {} '@types/aria-query@5.0.4': {} @@ -12233,6 +12773,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/uuid@9.0.8': {} '@types/web-bluetooth@0.0.20': {} @@ -13300,10 +13842,10 @@ snapshots: clsx@2.1.1: {} - cmdk@1.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + cmdk@1.0.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': 1.0.5(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -13446,6 +13988,8 @@ snapshots: optionalDependencies: typescript: 5.6.3 + crelt@1.0.6: {} + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -15123,6 +15667,8 @@ snapshots: dependencies: hermes-estree: 0.20.1 + highlight.js@11.10.0: {} + hono@4.6.11: {} hookable@5.5.3: {} @@ -15707,6 +16253,12 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.1.4: {} + lint-staged@15.2.10: dependencies: chalk: 5.3.0 @@ -15820,6 +16372,12 @@ snapshots: dependencies: tslib: 2.7.0 + lowlight@3.2.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.10.0 + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -15866,6 +16424,15 @@ snapshots: mark.js@8.11.1: {} + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.3: {} mdast-util-find-and-replace@3.0.1: @@ -16020,6 +16587,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdurl@2.0.0: {} + memoizerific@1.11.3: dependencies: map-or-similar: 1.5.0 @@ -16558,6 +17127,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.0 + orderedmap@2.1.1: {} + os-name@5.1.0: dependencies: macos-release: 3.3.0 @@ -16898,6 +17469,109 @@ snapshots: property-information@6.5.0: {} + prosemirror-changeset@2.2.1: + dependencies: + prosemirror-transform: 1.10.2 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.3 + + prosemirror-commands@1.6.2: + dependencies: + prosemirror-model: 1.24.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-dropcursor@1.8.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.37.0 + + prosemirror-gapcursor@1.3.2: + dependencies: + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.24.0 + prosemirror-state: 1.4.3 + prosemirror-view: 1.37.0 + + prosemirror-history@1.4.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.37.0 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.4.0: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-keymap@1.2.2: + dependencies: + prosemirror-state: 1.4.3 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.1: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + prosemirror-model: 1.24.0 + + prosemirror-menu@1.2.4: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.6.2 + prosemirror-history: 1.4.1 + prosemirror-state: 1.4.3 + + prosemirror-model@1.24.0: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.3: + dependencies: + prosemirror-model: 1.24.0 + + prosemirror-schema-list@1.4.1: + dependencies: + prosemirror-model: 1.24.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-state@1.4.3: + dependencies: + prosemirror-model: 1.24.0 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.37.0 + + prosemirror-tables@1.6.1: + dependencies: + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.24.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.37.0 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.24.0)(prosemirror-state@1.4.3)(prosemirror-view@1.37.0): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.24.0 + prosemirror-state: 1.4.3 + prosemirror-view: 1.37.0 + + prosemirror-transform@1.10.2: + dependencies: + prosemirror-model: 1.24.0 + + prosemirror-view@1.37.0: + dependencies: + prosemirror-model: 1.24.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + proto-list@1.2.4: {} protocols@2.0.1: {} @@ -16917,6 +17591,8 @@ snapshots: proxy-from-env@1.1.0: {} + punycode.js@2.3.1: {} + punycode@2.3.1: {} pupa@3.1.0: @@ -16981,6 +17657,11 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-dom@19.0.0-rc-66855b96-20241106(react@18.3.1): + dependencies: + react: 18.3.1 + scheduler: 0.25.0-rc-66855b96-20241106 + react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106): dependencies: react: 19.0.0-rc-66855b96-20241106 @@ -17027,6 +17708,11 @@ snapshots: transitivePeerDependencies: - supports-color + react-medium-image-zoom@5.2.11(react-dom@19.0.0-rc-66855b96-20241106(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 19.0.0-rc-66855b96-20241106(react@18.3.1) + react-merge-refs@1.1.0: {} react-refresh@0.14.2: {} @@ -17420,6 +18106,8 @@ snapshots: fsevents: 2.3.3 optional: true + rope-sequence@1.3.4: {} + rrweb-cssom@0.7.1: {} run-applescript@5.0.0: @@ -17614,6 +18302,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + sonner@1.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 19.0.0-rc-66855b96-20241106(react@18.3.1) + sort-object-keys@1.1.3: {} sort-package-json@1.57.0: @@ -17974,6 +18667,10 @@ snapshots: tinyspy@3.0.2: {} + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + titleize@3.0.0: {} tldts-core@6.1.58: {} @@ -18139,6 +18836,8 @@ snapshots: typescript@5.6.3: {} + uc.micro@2.1.0: {} + ufo@1.5.4: {} unbox-primitive@1.0.2: @@ -18262,6 +18961,10 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + use-sync-external-store@1.2.2(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} util@0.12.5: @@ -18565,6 +19268,8 @@ snapshots: optionalDependencies: typescript: 5.6.3 + w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0