diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json new file mode 100644 index 00000000..2c4cb210 --- /dev/null +++ b/packages/rich-text/package.json @@ -0,0 +1,48 @@ +{ + "name": "@bytelyst/rich-text", + "version": "0.1.0", + "type": "module", + "description": "Tiptap v3 rich-text editor + viewer with a token-themed toolbar, slash-menu and async mention extensions. SSR-safe for Next.", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "vitest run --pool forks", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "dependencies": { + "@tiptap/core": "^3.23.6", + "@tiptap/extension-link": "^3.23.6", + "@tiptap/extension-mention": "^3.23.6", + "@tiptap/extension-placeholder": "^3.23.6", + "@tiptap/html": "^3.23.6", + "@tiptap/pm": "^3.23.6", + "@tiptap/react": "^3.23.6", + "@tiptap/starter-kit": "^3.23.6", + "@tiptap/suggestion": "^3.23.6", + "clsx": "^2.1.0" + }, + "devDependencies": { + "@testing-library/react": "^16.3.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "happy-dom": "^18.0.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "typescript": "^5.7.3", + "vitest": "^4.0.18" + } +} diff --git a/packages/rich-text/src/MentionList.tsx b/packages/rich-text/src/MentionList.tsx new file mode 100644 index 00000000..6b819905 --- /dev/null +++ b/packages/rich-text/src/MentionList.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import type { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion'; +import type { MentionUser } from './mention.js'; + +export interface MentionListHandle { + onKeyDown: (props: SuggestionKeyDownProps) => boolean; +} + +/** Keyboard-navigable people picker shown while typing `@`. */ +export const MentionList = React.forwardRef>( + function MentionList(props, ref) { + const { items, command } = props; + const [selected, setSelected] = React.useState(0); + + React.useEffect(() => setSelected(0), [items]); + + const select = React.useCallback( + (index: number) => { + const item = items[index]; + if (item) command({ id: item.id, label: item.label }); + }, + [items, command] + ); + + React.useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }) => { + if (event.key === 'ArrowUp') { + setSelected(s => (s + items.length - 1) % items.length); + return true; + } + if (event.key === 'ArrowDown') { + setSelected(s => (s + 1) % items.length); + return true; + } + if (event.key === 'Enter') { + select(selected); + return true; + } + return false; + }, + })); + + return ( +
+ {items.length === 0 ? ( +
No people
+ ) : ( + items.map((item, index) => ( + + )) + )} +
+ ); + } +); diff --git a/packages/rich-text/src/RichTextEditor.tsx b/packages/rich-text/src/RichTextEditor.tsx new file mode 100644 index 00000000..d744eb40 --- /dev/null +++ b/packages/rich-text/src/RichTextEditor.tsx @@ -0,0 +1,84 @@ +'use client'; + +import * as React from 'react'; +import { clsx } from 'clsx'; +import { EditorContent, useEditor } from '@tiptap/react'; +import type { AnyExtension, JSONContent } from '@tiptap/core'; + +import { buildExtensions } from './extensions.js'; +import { SlashCommands } from './slashMenu.js'; +import { createMention, type UserSearchFn } from './mention.js'; +import { Toolbar } from './Toolbar.js'; + +export interface RichTextEditorProps { + /** Initial document — Tiptap JSON or an HTML string. */ + content?: JSONContent | string; + /** Called with the latest JSON document on every change. */ + onChange?: (doc: JSONContent) => void; + /** Placeholder shown when empty. */ + placeholder?: string; + /** Enable the `/` slash command menu (default true). */ + enableSlashMenu?: boolean; + /** Provide to enable `@`-mentions backed by this people search. */ + mentionSearch?: UserSearchFn; + /** Read-only mode. */ + editable?: boolean; + className?: string; + ariaLabel?: string; +} + +/** + * Full editor surface: token-themed toolbar + Tiptap editing area, with an + * optional slash menu and async mentions. SSR-safe (`immediatelyRender: + * false`) so it hydrates cleanly under Next. + */ +export function RichTextEditor({ + content, + onChange, + placeholder, + enableSlashMenu = true, + mentionSearch, + editable = true, + className, + ariaLabel = 'Rich text editor', +}: RichTextEditorProps) { + const extra = React.useMemo(() => { + const list: AnyExtension[] = []; + if (enableSlashMenu) list.push(SlashCommands); + if (mentionSearch) list.push(createMention(mentionSearch)); + // Extensions are fixed for a mount; deliberately not reactive. + return list; + }, []); + + const editor = useEditor({ + immediatelyRender: false, + editable, + extensions: buildExtensions({ placeholder, extra }), + content, + editorProps: { + attributes: { + role: 'textbox', + 'aria-multiline': 'true', + 'aria-label': ariaLabel, + class: 'bl-rich-text-content min-h-40 p-3 outline-none', + }, + }, + onUpdate: ({ editor: e }) => onChange?.(e.getJSON()), + }); + + React.useEffect(() => { + editor?.setEditable(editable); + }, [editor, editable]); + + return ( +
+ {editable && } + +
+ ); +} diff --git a/packages/rich-text/src/RichTextViewer.tsx b/packages/rich-text/src/RichTextViewer.tsx new file mode 100644 index 00000000..45d7d858 --- /dev/null +++ b/packages/rich-text/src/RichTextViewer.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; +import { generateHTML } from '@tiptap/html'; +import Mention from '@tiptap/extension-mention'; +import type { JSONContent } from '@tiptap/core'; + +import { buildExtensions } from './extensions.js'; + +export interface RichTextViewerProps { + /** Tiptap JSON document to render read-only. */ + doc: JSONContent; + className?: string; +} + +/** + * Read-only renderer. Serialises a Tiptap JSON doc to HTML using the same + * extension schema as the editor (so output matches), with no editing surface + * and no client runtime — safe to render on the server. + */ +export function RichTextViewer({ doc, className }: RichTextViewerProps) { + const html = React.useMemo(() => generateHTML(doc, buildExtensions({ extra: [Mention] })), [doc]); + return ( +
+ ); +} diff --git a/packages/rich-text/src/SlashMenuList.tsx b/packages/rich-text/src/SlashMenuList.tsx new file mode 100644 index 00000000..1c0e5546 --- /dev/null +++ b/packages/rich-text/src/SlashMenuList.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import type { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion'; +import type { SlashItem } from './slashMenu.js'; + +export interface SlashMenuListHandle { + onKeyDown: (props: SuggestionKeyDownProps) => boolean; +} + +/** + * Keyboard-navigable list rendered inside the slash-menu popup. Exposes an + * imperative `onKeyDown` so the Suggestion plugin can route ↑/↓/Enter here. + */ +export const SlashMenuList = React.forwardRef>( + function SlashMenuList(props, ref) { + const { items, command } = props; + const [selected, setSelected] = React.useState(0); + + React.useEffect(() => setSelected(0), [items]); + + const select = React.useCallback( + (index: number) => { + const item = items[index]; + if (item) command(item); + }, + [items, command] + ); + + React.useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }) => { + if (event.key === 'ArrowUp') { + setSelected(s => (s + items.length - 1) % items.length); + return true; + } + if (event.key === 'ArrowDown') { + setSelected(s => (s + 1) % items.length); + return true; + } + if (event.key === 'Enter') { + select(selected); + return true; + } + return false; + }, + })); + + if (items.length === 0) { + return ( +
+ No matches +
+ ); + } + + return ( +
+ {items.map((item, index) => ( + + ))} +
+ ); + } +); diff --git a/packages/rich-text/src/Toolbar.tsx b/packages/rich-text/src/Toolbar.tsx new file mode 100644 index 00000000..ff010dfe --- /dev/null +++ b/packages/rich-text/src/Toolbar.tsx @@ -0,0 +1,125 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; +import type { Editor } from '@tiptap/core'; + +export interface ToolbarProps { + editor: Editor | null; +} + +interface ToolButton { + label: string; + /** aria-label / accessible name. */ + name: string; + isActive?: (e: Editor) => boolean; + run: (e: Editor) => void; +} + +const BUTTONS: ToolButton[] = [ + { + label: 'B', + name: 'Bold', + isActive: e => e.isActive('bold'), + run: e => e.chain().focus().toggleBold().run(), + }, + { + label: 'I', + name: 'Italic', + isActive: e => e.isActive('italic'), + run: e => e.chain().focus().toggleItalic().run(), + }, + { + label: 'H1', + name: 'Heading 1', + isActive: e => e.isActive('heading', { level: 1 }), + run: e => e.chain().focus().toggleHeading({ level: 1 }).run(), + }, + { + label: 'H2', + name: 'Heading 2', + isActive: e => e.isActive('heading', { level: 2 }), + run: e => e.chain().focus().toggleHeading({ level: 2 }).run(), + }, + { + label: '• List', + name: 'Bullet list', + isActive: e => e.isActive('bulletList'), + run: e => e.chain().focus().toggleBulletList().run(), + }, + { + label: '1. List', + name: 'Numbered list', + isActive: e => e.isActive('orderedList'), + run: e => e.chain().focus().toggleOrderedList().run(), + }, + { + label: '❝', + name: 'Quote', + isActive: e => e.isActive('blockquote'), + run: e => e.chain().focus().toggleBlockquote().run(), + }, + { + label: '', + name: 'Code', + isActive: e => e.isActive('code'), + run: e => e.chain().focus().toggleCode().run(), + }, + { + label: 'Link', + name: 'Link', + isActive: e => e.isActive('link'), + run: e => { + const prev = (e.getAttributes('link').href as string) ?? ''; + const url = typeof window !== 'undefined' ? window.prompt('Link URL', prev) : prev; + if (url === null) return; + if (url === '') { + e.chain().focus().extendMarkRange('link').unsetLink().run(); + return; + } + e.chain().focus().extendMarkRange('link').setLink({ href: url }).run(); + }, + }, +]; + +/** Token-themed formatting toolbar bound to a Tiptap editor instance. */ +export function Toolbar({ editor }: ToolbarProps) { + // Re-render on selection/content changes so active states stay correct. + const [, force] = React.useReducer((x: number) => x + 1, 0); + React.useEffect(() => { + if (!editor) return; + editor.on('transaction', force); + return () => { + editor.off('transaction', force); + }; + }, [editor]); + + if (!editor) return null; + + return ( +
+ {BUTTONS.map(b => { + const active = b.isActive?.(editor) ?? false; + return ( + + ); + })} +
+ ); +} diff --git a/packages/rich-text/src/__tests__/rich-text.test.tsx b/packages/rich-text/src/__tests__/rich-text.test.tsx new file mode 100644 index 00000000..4df9b511 --- /dev/null +++ b/packages/rich-text/src/__tests__/rich-text.test.tsx @@ -0,0 +1,143 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import * as React from 'react'; +import { Editor } from '@tiptap/core'; +import type { JSONContent } from '@tiptap/core'; +import type { SuggestionProps } from '@tiptap/suggestion'; + +import { RichTextViewer } from '../RichTextViewer.js'; +import { RichTextEditor } from '../RichTextEditor.js'; +import { Toolbar } from '../Toolbar.js'; +import { buildExtensions } from '../extensions.js'; +import { defaultSlashItems, filterSlashItems, type SlashItem } from '../slashMenu.js'; +import { filterUsers, type MentionUser } from '../mention.js'; +import { SlashMenuList } from '../SlashMenuList.js'; +import { MentionList } from '../MentionList.js'; + +beforeEach(() => cleanup()); + +/* ── 9.B.4 — slash filter (pure) ─────────────────────────────────────── */ +describe('filterSlashItems', () => { + it('returns all items for an empty query', () => { + expect(filterSlashItems('')).toHaveLength(defaultSlashItems.length); + }); + it('matches by title (case-insensitive)', () => { + const r = filterSlashItems('HEAD'); + expect(r.length).toBeGreaterThan(0); + expect(r.every(i => /head/i.test(i.title))).toBe(true); + }); + it('matches by alias', () => { + const r = filterSlashItems('ul'); + expect(r.some(i => i.title === 'Bullet list')).toBe(true); + }); +}); + +/* ── 9.B.5 — mention people search (pure) ────────────────────────────── */ +describe('filterUsers', () => { + const people: MentionUser[] = [ + { id: '1', label: 'Ada Lovelace' }, + { id: '2', label: 'Alan Turing' }, + { id: '3', label: 'Grace Hopper' }, + ]; + it('filters case-insensitively by label', () => { + expect(filterUsers('ada', people).map(u => u.id)).toEqual(['1']); + }); + it('returns all (capped) for an empty query', () => { + expect(filterUsers('', people, 2)).toHaveLength(2); + }); +}); + +/* ── 9.B.2 — viewer renders HTML from JSON ───────────────────────────── */ +describe('RichTextViewer', () => { + const doc: JSONContent = { + type: 'doc', + content: [ + { type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: 'Title' }] }, + { + type: 'paragraph', + content: [{ type: 'text', marks: [{ type: 'bold' }], text: 'bold' }], + }, + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'one' }] }], + }, + ], + }, + ], + }; + it('serialises headings, marks and lists', () => { + const { container } = render(); + expect(container.querySelector('h1')?.textContent).toBe('Title'); + expect(container.querySelector('strong')?.textContent).toBe('bold'); + expect(container.querySelector('ul li')?.textContent).toBe('one'); + }); +}); + +/* ── 9.B.3 — toolbar commands ────────────────────────────────────────── */ +describe('Toolbar', () => { + it('toggles bold on the bound editor', () => { + const editor = new Editor({ + element: document.createElement('div'), + extensions: buildExtensions(), + content: '

hello

', + }); + editor.commands.selectAll(); + render(); + fireEvent.click(screen.getByLabelText('Bold')); + expect(editor.getHTML()).toContain(''); + editor.destroy(); + }); + + it('renders nothing without an editor', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); +}); + +/* ── 9.B.2 — editor mounts (SSR-safe) ────────────────────────────────── */ +describe('RichTextEditor', () => { + it('mounts a toolbar + editable textbox', async () => { + render(); + expect(await screen.findByRole('textbox')).toBeTruthy(); + expect(screen.getByRole('toolbar', { name: 'Formatting' })).toBeTruthy(); + }); +}); + +/* ── 9.B.4 / 9.B.5 — suggestion list components ──────────────────────── */ +function slashProps( + items: SlashItem[], + command: (i: SlashItem) => void +): SuggestionProps { + return { items, command } as unknown as SuggestionProps; +} + +describe('SlashMenuList', () => { + it('renders an option per item and invokes command on click', () => { + const command = vi.fn(); + render(); + const options = screen.getAllByRole('option'); + expect(options).toHaveLength(defaultSlashItems.length); + fireEvent.click(screen.getByText('Heading 1')); + expect(command).toHaveBeenCalledWith(defaultSlashItems[0]); + }); + + it('shows an empty state when there are no items', () => { + render(); + expect(screen.getByText('No matches')).toBeTruthy(); + }); +}); + +describe('MentionList', () => { + it('renders people and emits {id,label} on click', () => { + const command = vi.fn(); + const people: MentionUser[] = [{ id: '7', label: 'Ada' }]; + render( + )} /> + ); + fireEvent.click(screen.getByText('Ada')); + expect(command).toHaveBeenCalledWith({ id: '7', label: 'Ada' }); + }); +}); diff --git a/packages/rich-text/src/extensions.ts b/packages/rich-text/src/extensions.ts new file mode 100644 index 00000000..01723c73 --- /dev/null +++ b/packages/rich-text/src/extensions.ts @@ -0,0 +1,26 @@ +import StarterKit from '@tiptap/starter-kit'; +import Link from '@tiptap/extension-link'; +import Placeholder from '@tiptap/extension-placeholder'; +import type { AnyExtension } from '@tiptap/core'; + +export interface BuildExtensionsOptions { + /** Placeholder text shown when the doc is empty. */ + placeholder?: string; + /** Extra extensions to append (e.g. a configured Mention or SlashCommands). */ + extra?: AnyExtension[]; +} + +/** + * The canonical extension set shared by `` and + * `` — keeping them in sync guarantees that serialised HTML + * matches what the editor produced. + */ +export function buildExtensions(options: BuildExtensionsOptions = {}): AnyExtension[] { + const { placeholder = 'Write something…', extra = [] } = options; + return [ + StarterKit.configure({ link: false }), + Link.configure({ openOnClick: false, autolink: true }), + Placeholder.configure({ placeholder }), + ...extra, + ]; +} diff --git a/packages/rich-text/src/index.ts b/packages/rich-text/src/index.ts new file mode 100644 index 00000000..37329dba --- /dev/null +++ b/packages/rich-text/src/index.ts @@ -0,0 +1,33 @@ +/** + * @bytelyst/rich-text — Tiptap v3 editor + viewer for the ByteLyst ecosystem. + * + * Exports (0.1.0 — Wave 9.B): + * toolbar + editing surface, slash menu, async mentions + * SSR-safe read-only renderer (generateHTML) + * standalone formatting toolbar bound to an Editor + * buildExtensions shared StarterKit + Link + Placeholder schema + * SlashCommands `/` command extension + defaultSlashItems / filterSlashItems + * createMention `@`-mention extension factory + filterUsers helper + * + * Built on Tiptap v3.23.x (current stable). SSR-safe for Next via + * `immediatelyRender: false`. + */ +export { RichTextEditor } from './RichTextEditor.js'; +export type { RichTextEditorProps } from './RichTextEditor.js'; + +export { RichTextViewer } from './RichTextViewer.js'; +export type { RichTextViewerProps } from './RichTextViewer.js'; + +export { Toolbar } from './Toolbar.js'; +export type { ToolbarProps } from './Toolbar.js'; + +export { buildExtensions } from './extensions.js'; +export type { BuildExtensionsOptions } from './extensions.js'; + +export { SlashCommands, defaultSlashItems, filterSlashItems } from './slashMenu.js'; +export type { SlashItem } from './slashMenu.js'; + +export { createMention, filterUsers } from './mention.js'; +export type { MentionUser, UserSearchFn } from './mention.js'; + +export type { JSONContent } from '@tiptap/core'; diff --git a/packages/rich-text/src/mention.ts b/packages/rich-text/src/mention.ts new file mode 100644 index 00000000..6559a877 --- /dev/null +++ b/packages/rich-text/src/mention.ts @@ -0,0 +1,81 @@ +import Mention from '@tiptap/extension-mention'; +import type { AnyExtension } from '@tiptap/core'; +import type { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion'; +import { ReactRenderer } from '@tiptap/react'; + +import { MentionList } from './MentionList.js'; + +export interface MentionUser { + id: string; + label: string; +} + +/** Resolver that returns matching people for a query (sync or async). */ +export type UserSearchFn = (query: string) => MentionUser[] | Promise; + +/** + * Pure, case-insensitive in-memory matcher — handy as a default `UserSearchFn` + * and directly unit-testable. Caps results at `limit`. + */ +export function filterUsers(query: string, users: MentionUser[], limit = 5): MentionUser[] { + const q = query.trim().toLowerCase(); + const matches = q ? users.filter(u => u.label.toLowerCase().includes(q)) : users; + return matches.slice(0, limit); +} + +/** + * Build a configured `@`-mention extension backed by an async people search. + * Renders a keyboard-navigable popup at the caret. + */ +export function createMention(search: UserSearchFn): AnyExtension { + return Mention.configure({ + HTMLAttributes: { class: 'bl-mention' }, + suggestion: { + char: '@', + items: async ({ query }) => await search(query), + render: () => { + let component: ReactRenderer<{ onKeyDown: (p: SuggestionKeyDownProps) => boolean }> | null = + null; + let popup: HTMLDivElement | null = null; + + const position = (props: SuggestionProps) => { + if (!popup) return; + const rect = props.clientRect?.(); + if (!rect) return; + popup.style.left = `${rect.left}px`; + popup.style.top = `${rect.bottom + 4}px`; + }; + + return { + onStart: (props: SuggestionProps) => { + component = new ReactRenderer(MentionList, { props, editor: props.editor }); + popup = document.createElement('div'); + popup.setAttribute('data-mention-menu', 'true'); + popup.style.position = 'absolute'; + popup.style.zIndex = '50'; + popup.appendChild(component.element); + document.body.appendChild(popup); + position(props); + }, + onUpdate: (props: SuggestionProps) => { + component?.updateProps(props); + position(props); + }, + onKeyDown: (props: SuggestionKeyDownProps) => { + if (props.event.key === 'Escape') { + popup?.remove(); + return true; + } + return component?.ref?.onKeyDown(props) ?? false; + }, + onExit: () => { + popup?.remove(); + popup = null; + component?.destroy(); + component = null; + }, + }; + }, + }, + }); +} diff --git a/packages/rich-text/src/slashMenu.ts b/packages/rich-text/src/slashMenu.ts new file mode 100644 index 00000000..5b146476 --- /dev/null +++ b/packages/rich-text/src/slashMenu.ts @@ -0,0 +1,138 @@ +import { Extension } from '@tiptap/core'; +import type { Editor, Range } from '@tiptap/core'; +import Suggestion, { type SuggestionProps, type SuggestionKeyDownProps } from '@tiptap/suggestion'; +import { ReactRenderer } from '@tiptap/react'; + +import { SlashMenuList } from './SlashMenuList.js'; + +export interface SlashItem { + title: string; + description: string; + aliases?: string[]; + command: (props: { editor: Editor; range: Range }) => void; +} + +/** The built-in block actions offered by the slash menu. */ +export const defaultSlashItems: SlashItem[] = [ + { + title: 'Heading 1', + description: 'Big section heading', + aliases: ['h1', 'title'], + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run(), + }, + { + title: 'Heading 2', + description: 'Medium section heading', + aliases: ['h2', 'subtitle'], + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run(), + }, + { + title: 'Bullet list', + description: 'Unordered list', + aliases: ['ul', 'unordered'], + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).toggleBulletList().run(), + }, + { + title: 'Numbered list', + description: 'Ordered list', + aliases: ['ol', 'ordered'], + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).toggleOrderedList().run(), + }, + { + title: 'Quote', + description: 'Blockquote', + aliases: ['blockquote'], + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).toggleBlockquote().run(), + }, + { + title: 'Code block', + description: 'Monospaced code', + aliases: ['code', 'pre'], + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), + }, +]; + +/** Pure, case-insensitive filter over title + aliases — unit-testable. */ +export function filterSlashItems( + query: string, + items: SlashItem[] = defaultSlashItems +): SlashItem[] { + const q = query.trim().toLowerCase(); + if (!q) return items; + return items.filter( + item => + item.title.toLowerCase().includes(q) || + (item.aliases ?? []).some(a => a.toLowerCase().includes(q)) + ); +} + +/** + * `/`-triggered command menu. Built on `@tiptap/suggestion`; the popup is a + * React component positioned at the caret. Only mounts while the user is + * typing a slash command, so it never appears on first render. + */ +export const SlashCommands = Extension.create({ + name: 'slashCommands', + + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + char: '/', + startOfLine: false, + items: ({ query }) => filterSlashItems(query), + command: ({ editor, range, props }) => props.command({ editor, range }), + render: () => { + let component: ReactRenderer<{ + onKeyDown: (p: SuggestionKeyDownProps) => boolean; + }> | null = null; + let popup: HTMLDivElement | null = null; + + const position = (props: SuggestionProps) => { + if (!popup) return; + const rect = props.clientRect?.(); + if (!rect) return; + popup.style.left = `${rect.left}px`; + popup.style.top = `${rect.bottom + 4}px`; + }; + + return { + onStart: (props: SuggestionProps) => { + component = new ReactRenderer(SlashMenuList, { props, editor: props.editor }); + popup = document.createElement('div'); + popup.setAttribute('data-slash-menu', 'true'); + popup.style.position = 'absolute'; + popup.style.zIndex = '50'; + popup.appendChild(component.element); + document.body.appendChild(popup); + position(props); + }, + onUpdate: (props: SuggestionProps) => { + component?.updateProps(props); + position(props); + }, + onKeyDown: (props: SuggestionKeyDownProps) => { + if (props.event.key === 'Escape') { + popup?.remove(); + return true; + } + return component?.ref?.onKeyDown(props) ?? false; + }, + onExit: () => { + popup?.remove(); + popup = null; + component?.destroy(); + component = null; + }, + }; + }, + }), + ]; + }, +}); diff --git a/packages/rich-text/tsconfig.json b/packages/rich-text/tsconfig.json new file mode 100644 index 00000000..4447784f --- /dev/null +++ b/packages/rich-text/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "lib": ["ES2022", "DOM"], + "jsx": "react-jsx" + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] +} diff --git a/packages/rich-text/vitest.config.ts b/packages/rich-text/vitest.config.ts new file mode 100644 index 00000000..73b69c6f --- /dev/null +++ b/packages/rich-text/vitest.config.ts @@ -0,0 +1,2 @@ +import { defineConfig } from 'vitest/config'; +export default defineConfig({ test: { environment: 'happy-dom', pool: 'forks' } }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 873d73de..6cb6b4c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1106,6 +1106,64 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/rich-text: + dependencies: + '@tiptap/core': + specifier: ^3.23.6 + version: 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/extension-link': + specifier: ^3.23.6 + version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + '@tiptap/extension-mention': + specifier: ^3.23.6 + version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@tiptap/suggestion@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)) + '@tiptap/extension-placeholder': + specifier: ^3.23.6 + version: 3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)) + '@tiptap/html': + specifier: ^3.23.6 + version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(happy-dom@18.0.1) + '@tiptap/pm': + specifier: ^3.23.6 + version: 3.23.6 + '@tiptap/react': + specifier: ^3.23.6 + version: 3.23.6(@floating-ui/dom@1.7.5)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tiptap/starter-kit': + specifier: ^3.23.6 + version: 3.23.6 + '@tiptap/suggestion': + specifier: ^3.23.6 + version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + clsx: + specifier: ^2.1.0 + version: 2.1.1 + devDependencies: + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + happy-dom: + specifier: ^18.0.1 + version: 18.0.1 + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.11)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.11)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + packages/secure-storage-web: devDependencies: fake-indexeddb: @@ -6609,6 +6667,276 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tiptap/core@3.23.6': + resolution: + { + integrity: sha512-MRB3pHz4Oxqmcawh0cQ5iOGdY5xtNYp/1CoK7hdTLzw5K0C6/gTC2VvanB1R4INaB6EpBkxG/GiWkVirDRnuXw==, + } + peerDependencies: + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-blockquote@3.23.6': + resolution: + { + integrity: sha512-2RmnqNqTltZ2k1F7IfjoDNs935Uq4rRDR7d98mqkg3OlDktcQIyBpv0t9dTay6H5bkQeZUuS8ogK2S1E8Edjug==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extension-bold@3.23.6': + resolution: + { + integrity: sha512-1LMhjnytdbbhWHSoOwnLxZAOQZWPkKyXVCNmaIk0Mhi4tLPUXptG4qKS5sVYTCveE5H6IBPFrbgBFi5dMI6krA==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extension-bubble-menu@3.23.6': + resolution: + { + integrity: sha512-Mwkyp9LkDHFbqmWRIkp63FinRxFu3ajC4qSb9t4mnHsb4kAdbNLLsGtbFg+le0SWk4CxGwAOwM7SzeJ+6UGqCA==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-bullet-list@3.23.6': + resolution: + { + integrity: sha512-RMRgfXZykr/13X8UBOwvpgysVOo9KchwqMoEbvqQSj4YFfU56iIn59C8sbxiQ1sKfeltUf0wH4fPc0I4iwKqAA==, + } + peerDependencies: + '@tiptap/extension-list': 3.23.6 + + '@tiptap/extension-code-block@3.23.6': + resolution: + { + integrity: sha512-4kccgcn5yHThxrzsIhJny3EwfEZYIk+BjUCL4uIuzOyWvExtGhZ6JMHVCZeMhI8D1/bX1LNkkAKN5DXPzH4lXQ==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-code@3.23.6': + resolution: + { + integrity: sha512-KG8KXFYyLrtYvT7AZ1WGV61ofx8pDe5g9pH658MERxqQGii+Pyfc6xkz04l7XeBts/7+571UQp/0O7i/z560TA==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extension-document@3.23.6': + resolution: + { + integrity: sha512-XDAIgG9KcKumFM9KJWUEUhXPbFIhhl47bfy5GknareWTRKke85rcoj/oxKKO9ihLZr8JfpbXjqnS4SCm5yhYPw==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extension-dropcursor@3.23.6': + resolution: + { + integrity: sha512-+XWEoRKf3lXxi7Le1aOM2xU1XHwxICGpXjT3m4QaYqUgIpsq8gQEuso6kVg8DnTD7biKQs6+oIQ0o2b/gTW9WA==, + } + peerDependencies: + '@tiptap/extensions': 3.23.6 + + '@tiptap/extension-floating-menu@3.23.6': + resolution: + { + integrity: sha512-2kjuDcEq69lEcECl75xqY5MyzUSh2zcC5aLrpwP1WwhJz5bxsIFHiaps5AP6h9R4A+ZBj5b2haay2Y1wDUU3VA==, + } + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-gapcursor@3.23.6': + resolution: + { + integrity: sha512-wbKmxXsszxWacEkrHucRpSQbiKjz4fmOebD6OVyL9AcrmlbxNk8vcM3iyh/8cVeRy09XY+morM165t/u7/z4IQ==, + } + peerDependencies: + '@tiptap/extensions': 3.23.6 + + '@tiptap/extension-hard-break@3.23.6': + resolution: + { + integrity: sha512-KeUm+tkUfIVSX9QM9XOIhaay0Fn36sLKUo5NVYjN3uJaxFvaZXZmTlxdO85OTdgF2P5sqh9LomrIgliaFRGk4w==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extension-heading@3.23.6': + resolution: + { + integrity: sha512-A/0jPhxnUh9THSZymlu0OGPZe1wdFdwHAXnRCmqvYUCwJjrG7LCC/ahzmcj1tcNzI9hgHyuYPSfev8RXYrNu/w==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extension-horizontal-rule@3.23.6': + resolution: + { + integrity: sha512-hEUlz4H+I64r+TH6LCuNCRgO7JTHncXGmx9+WbU69EOfY8O0ZurcgeJc8HeiAKL+r9YuC1e5YHfFxgCaaC0jlg==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-italic@3.23.6': + resolution: + { + integrity: sha512-wol5KdwCPAvpiYhH9PLlvO8ZnJHwZtIboVevrfOGgBcKlXRA3dedR4OAMXHnUtkkzu9KtliLg1+TYzEx4JZG9Q==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extension-link@3.23.6': + resolution: + { + integrity: sha512-KNZz7z7P2/qbQsx5bPAbSPjrKDg1VHsedGlLHJCr8U2VRD5VgmDLkMpkouP1CsDg15qgyUKv/nDib5KgPpLNWA==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-list-item@3.23.6': + resolution: + { + integrity: sha512-3zzyhdkUWcHVpXuvy6KiIwjh29rbH6gEDEqPQqHLrl1XGnO9pnShC7pSHctlCDjmcx3O4n9cd4QMtVBlUerbiA==, + } + peerDependencies: + '@tiptap/extension-list': 3.23.6 + + '@tiptap/extension-list-keymap@3.23.6': + resolution: + { + integrity: sha512-x8bPcLViGzg/RAmQM/XtmfqIwQ/Pv9Q8mkd+OgfUiTqjeJqKwVQmiqbLFNa7zw81+H61M+HDU+qGAaQ3vRIMjw==, + } + peerDependencies: + '@tiptap/extension-list': 3.23.6 + + '@tiptap/extension-list@3.23.6': + resolution: + { + integrity: sha512-z6vj9+Qht2sjdQkyyHcUpsC/yCIZqTrQiyHDhs/HGKrfvoANyAZGpqdNeKf1wSyjIso+27tQuIH5NDfk8ygyNw==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-mention@3.23.6': + resolution: + { + integrity: sha512-rSjeAAtuMwMA1lj4nbxz3rbmM06yPFUc8TFzhrEpmA4/l5XNWOk/PQef6uiGN+Isv2Z2PrIhr8XrR7Me8OSCiA==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + '@tiptap/suggestion': 3.23.6 + + '@tiptap/extension-ordered-list@3.23.6': + resolution: + { + integrity: sha512-1m/wWB/ZtXcmG2vNdiUkCqsOgqv5vBjCv/mVaHhF9OvV+zQS8YDjoWE7zEuT/GgELdT77Xq8lHrn4nCDudB3/A==, + } + peerDependencies: + '@tiptap/extension-list': 3.23.6 + + '@tiptap/extension-paragraph@3.23.6': + resolution: + { + integrity: sha512-+7m58LUSncodjrIyXks4RZ3tLNYrvgT77wRR4l3HnM5OABY3GDsDTqi7c1t1yI29NVOSk/DUacqy6UwYAj1DGg==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extension-placeholder@3.23.6': + resolution: + { + integrity: sha512-8I6b2aevF74aLgymKMxbDxSLxWA2y+2dh0zZDeI8sRZ2m6WHHes+Kyuuwkq1HIPcR+ZLpbec74cmf6lcL/yvqQ==, + } + peerDependencies: + '@tiptap/extensions': 3.23.6 + + '@tiptap/extension-strike@3.23.6': + resolution: + { + integrity: sha512-oF7FEZ37f15aCe5kPgzGDYf/m+hr7VdQ/Ko/Hds/UM9pX7AG1fdtmRrl6wqkRqDM/incZaC/AQR2/Dpo2VCNGQ==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extension-text@3.23.6': + resolution: + { + integrity: sha512-ipoC2TkIAIOTiF5ByiGgvQB1DqDyfP90wrUB3mohBcgvp7lQnwHszCDGv8dNnmcUek8uXV/uoLu2VXeVQlxjPA==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extension-underline@3.23.6': + resolution: + { + integrity: sha512-P55wGIZGYTVH92Fq0cgI4/O9AhLCaJC3hhxg15RSERP5/YegM9eJHDK/GQ1EE/DvYA+xpYGOV6agKwAUqfA/Iw==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + + '@tiptap/extensions@3.23.6': + resolution: + { + integrity: sha512-X09/Db1teB+ifXzDGVVFmOeQRx7wTAayE9/280spxpsHkHZvJ5bHRvWIzUzviMIjbBz+NPDIKYPK7gMfh9iaig==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + + '@tiptap/html@3.23.6': + resolution: + { + integrity: sha512-cfQ8ijvkhkbt02x0tjtcahubWCqxkO7IJow0j2MgS6FHdXKv2QUrIvcpAqIqdv0lA4ozWmdmUpLFv+lA95kcPg==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + happy-dom: ^20.8.9 + + '@tiptap/pm@3.23.6': + resolution: + { + integrity: sha512-in5CaMaWlJcH2A1q6GJKFtrodE8WLS3M9tIi/f89jPmIVHJShpodC0KZDNyJkrVBQomYk0DEh86Utm6ASXzQww==, + } + + '@tiptap/react@3.23.6': + resolution: + { + integrity: sha512-Tw9KZkYqFMk3vaJAEQKqEYIO/iq3cSJe7OUEGBul4k4GaMQeLItLf5EYhUd0GIPXci1WVVPNntKJsHfX25M37w==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.23.6': + resolution: + { + integrity: sha512-gykwtGWrnWCmtql1hid3opac/KV8zQvOAnu3bTqIqcHrn1FusbUwKmNzavSbfGvcktHM3hFjb35W48JyVLyu/A==, + } + + '@tiptap/suggestion@3.23.6': + resolution: + { + integrity: sha512-YAoI2jctPClcyUhIcpxb1QlrUFG2a1Xsv1gS4tIfgh5KoOuEfGfCoeCq89TKgz/rHeP+ktRhzg1E2E4EY68HEA==, + } + peerDependencies: + '@tiptap/core': 3.23.6 + '@tiptap/pm': 3.23.6 + '@ts-morph/common@0.27.0': resolution: { @@ -9739,6 +10067,13 @@ packages: integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, } + fast-equals@5.4.0: + resolution: + { + integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==, + } + engines: { node: '>=6.0.0' } + fast-glob@3.3.1: resolution: { @@ -11624,6 +11959,12 @@ packages: integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, } + linkifyjs@4.3.3: + resolution: + { + integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==, + } + lint-staged@15.5.2: resolution: { @@ -12991,6 +13332,12 @@ packages: } engines: { node: '>=18' } + orderedmap@2.1.1: + resolution: + { + integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==, + } + outdent@0.5.0: resolution: { @@ -13533,6 +13880,78 @@ packages: integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==, } + prosemirror-changeset@2.4.1: + resolution: + { + integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==, + } + + prosemirror-commands@1.7.1: + resolution: + { + integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==, + } + + prosemirror-dropcursor@1.8.2: + resolution: + { + integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==, + } + + prosemirror-gapcursor@1.4.1: + resolution: + { + integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==, + } + + prosemirror-history@1.5.0: + resolution: + { + integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==, + } + + prosemirror-keymap@1.2.3: + resolution: + { + integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==, + } + + prosemirror-model@1.25.7: + resolution: + { + integrity: sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==, + } + + prosemirror-schema-list@1.5.1: + resolution: + { + integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==, + } + + prosemirror-state@1.4.4: + resolution: + { + integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==, + } + + prosemirror-tables@1.8.5: + resolution: + { + integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==, + } + + prosemirror-transform@1.12.0: + resolution: + { + integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==, + } + + prosemirror-view@1.41.8: + resolution: + { + integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==, + } + protobufjs@7.5.4: resolution: { @@ -14067,6 +14486,12 @@ packages: engines: { node: '>=18.0.0', npm: '>=8.0.0' } hasBin: true + rope-sequence@1.3.4: + resolution: + { + integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==, + } + router@2.2.0: resolution: { @@ -15676,6 +16101,12 @@ packages: integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==, } + w3c-keyname@2.2.8: + resolution: + { + integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==, + } + w3c-xmlserializer@5.0.0: resolution: { @@ -20185,6 +20616,198 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 + '@tiptap/core@3.23.6(@tiptap/pm@3.23.6)': + dependencies: + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-blockquote@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extension-bold@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extension-bubble-menu@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)': + dependencies: + '@floating-ui/dom': 1.7.5 + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + optional: true + + '@tiptap/extension-bullet-list@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + + '@tiptap/extension-code-block@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-code@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extension-document@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extension-dropcursor@3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + + '@tiptap/extension-floating-menu@3.23.6(@floating-ui/dom@1.7.5)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)': + dependencies: + '@floating-ui/dom': 1.7.5 + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + optional: true + + '@tiptap/extension-gapcursor@3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + + '@tiptap/extension-hard-break@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extension-heading@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extension-horizontal-rule@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-italic@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extension-link@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + linkifyjs: 4.3.3 + + '@tiptap/extension-list-item@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + + '@tiptap/extension-list-keymap@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + + '@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + + '@tiptap/extension-mention@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@tiptap/suggestion@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + '@tiptap/suggestion': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + + '@tiptap/extension-ordered-list@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + + '@tiptap/extension-paragraph@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extension-placeholder@3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + + '@tiptap/extension-strike@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extension-text@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extension-underline@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + + '@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + + '@tiptap/html@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(happy-dom@18.0.1)': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + happy-dom: 18.0.1 + + '@tiptap/pm@3.23.6': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.7 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + '@tiptap/react@3.23.6(@floating-ui/dom@1.7.5)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + '@tiptap/extension-floating-menu': 3.23.6(@floating-ui/dom@1.7.5)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.23.6': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/extension-blockquote': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-bold': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-bullet-list': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)) + '@tiptap/extension-code': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-code-block': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + '@tiptap/extension-document': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-dropcursor': 3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)) + '@tiptap/extension-gapcursor': 3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)) + '@tiptap/extension-hard-break': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-heading': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-horizontal-rule': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + '@tiptap/extension-italic': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-link': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + '@tiptap/extension-list-item': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)) + '@tiptap/extension-list-keymap': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)) + '@tiptap/extension-ordered-list': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)) + '@tiptap/extension-paragraph': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-strike': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-text': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-underline': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + + '@tiptap/suggestion@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 @@ -22458,6 +23081,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.4.0: {} + fast-glob@3.3.1: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -23576,6 +24201,8 @@ snapshots: lines-and-columns@1.2.4: {} + linkifyjs@4.3.3: {} + lint-staged@15.5.2: dependencies: chalk: 5.6.2 @@ -24801,6 +25428,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + orderedmap@2.1.1: {} + outdent@0.5.0: {} outvariant@1.4.3: {} @@ -25101,6 +25730,75 @@ snapshots: property-information@7.1.0: {} + prosemirror-changeset@2.4.1: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.7 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.7 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + rope-sequence: 1.3.4 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.7: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.7 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.7 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.7 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.7 + + prosemirror-view@1.41.8: + dependencies: + prosemirror-model: 1.25.7 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + protobufjs@7.5.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -25637,6 +26335,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + router@2.2.0: dependencies: debug: 4.4.3 @@ -26557,6 +27257,10 @@ snapshots: dependencies: react: 19.2.3 + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + util-deprecate@1.0.2: {} util@0.12.5: @@ -26888,6 +27592,8 @@ snapshots: vlq@1.0.1: {} + w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0