editor: fix circular import that dropped PubScript nodes from schema

The TipTap node definitions imported their node-name constants from
schema.ts, while schema.ts imported the nodes to build the schema. This
circular dependency evaluated the nodes before the constants were
assigned, leaving sceneBreak/footnote/footnoteArea/characterMention with
name 'undefined' — so they never registered in PubScriptSchema. As a
result <hr class="scene-break"> parsed as StarterKit's horizontalRule
instead of a scene break.

- Move node-name constants, groups, MentionAttrs, and colorAttribute into
  a new constants.ts module that both schema.ts and the node files import
  from, breaking the cycle.
- Remove the redundant separate History extension (already in StarterKit),
  which also silenced a TipTap 'Duplicate extension names: undoRedo' warn.
- Drop unused TipTap extension and prosemirror-* deps (all access goes
  through @tiptap/pm).
This commit is contained in:
Schwitzz
2026-10-25 02:37:05 -05:00
parent 1c9b531996
commit c95f5e485a
16 changed files with 1677 additions and 2 deletions
+36
View File
@@ -0,0 +1,36 @@
{
"name": "@pubscript/editor",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./schema": "./src/schema.ts",
"./Editor": "./src/Editor.tsx",
"./markdown": "./src/markdown.ts",
"./styles": "./src/styles/editor.css"
},
"scripts": {
"dev": "echo \"@pubscript/editor has no dev server\" (exit 0)",
"build": "tsc --noEmit",
"test": "vitest run",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@pubscript/core": "workspace:*",
"@tiptap/core": "3.32.0",
"@tiptap/pm": "3.32.0",
"@tiptap/react": "3.32.0",
"@tiptap/starter-kit": "3.32.0",
"@tiptap/extension-text-style": "3.32.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"react": "^19.0.0",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
}
}
+124
View File
@@ -0,0 +1,124 @@
/**
* PubScript editor React wrapper — the single place that constructs a TipTap
* editor instance and exposes it to the app.
*
* Responsibilities:
* - Build the editor once (shared instance, no re-create on re-render).
* - Expose a decoration pipeline (`decorations` + `applyEdit`). The current
* decoration source is a fake/stub (`emptyDecorationSource`) so the rest
* of the app can be built against a stable contract; the real
* collaboration source is swapped in later (CHUNK #4).
* - Provide `applyEdit` to apply an arbitrary ProseMirror transaction.
*/
import { useEffect, useRef, useState } from "react";
import { useEditor, type UseEditorOptions } from "@tiptap/react";
import {
DecorationSet,
} from "@tiptap/pm/view";
import type { EditorState, Transaction } from "@tiptap/pm/state";
import type { Editor as TipTapEditor, Content } from "@tiptap/core";
import { createPubScriptEditor, pubScriptExtensions } from "./schema.js";
export type { UseEditorOptions };
/**
* A collaboration decoration source: the outer set decorates the shared
* document, the inner set decorates a local, non-shared view. Currently a
* stub that yields no decorations; the real collaboration source (CHUNK #4)
* will implement this shape.
*/
export interface PubScriptDecorationSource {
decorations: DecorationSet;
innerDecorations: DecorationSet;
}
/**
* Empty decoration source used as the default until a real collaboration
* source is wired in (CHUNK #4).
*/
export const emptyDecorationSource: PubScriptDecorationSource = {
decorations: DecorationSet.empty,
innerDecorations: DecorationSet.empty,
};
export interface PubScriptEditorHookOptions extends Omit<UseEditorOptions, "extensions" | "onUpdate"> {
/** Pre-existing editor instance to reuse (avoids re-creating ProseMirror state). */
editor?: TipTapEditor;
/** Decoration source to render. Defaults to the empty stub. */
decorationSource?: PubScriptDecorationSource;
/** Initial document content (ProseMirror JSON or HTML string). */
content?: string;
/** Called on every transaction with the live editor instance. */
onUpdate?: (editor: TipTapEditor) => void;
/** Whether the editor is editable. */
editable?: boolean;
}
/**
* Hook that builds and manages the PubScript editor. Returns the editor
* instance, the current decoration source, and an `applyEdit` function.
*/
export function usePubScriptEditor(options: PubScriptEditorHookOptions = {}) {
const {
editor: sharedEditor,
decorationSource = emptyDecorationSource,
content,
onUpdate,
editable = true,
...useEditorOptions
} = options;
const editor = useEditor({
editable,
extensions: pubScriptExtensions,
...(content !== undefined ? { content: content as Content } : {}),
...useEditorOptions,
});
// If a shared headless instance is provided, mount it into the React wrapper.
const sharedRef = useRef<TipTapEditor | null>(sharedEditor ?? null);
sharedRef.current = sharedEditor ?? null;
useEffect(() => {
if (sharedRef.current) {
sharedRef.current.setEditable(editable);
}
}, [editable]);
const [lastState, setLastState] = useState<EditorState | null>(null);
// Surface updates to consumers and track the latest state.
const onUpdateRef = useRef(onUpdate);
useEffect(() => {
onUpdateRef.current = onUpdate;
}, [onUpdate]);
useEffect(() => {
if (!editor) return;
const handler = () => {
setLastState(editor.state);
onUpdateRef.current?.(editor);
};
editor.on("update", handler);
return () => {
editor.off("update", handler);
};
}, [editor]);
const applyEdit = (transaction: Transaction) => {
if (!editor) return;
editor.view.dispatch(transaction);
};
return { editor, decorationSource, lastState, applyEdit };
}
/**
* Convenience factory for a headless editor instance, shared across the app.
*/
export function createEditorInstance(content?: string): TipTapEditor {
return createPubScriptEditor(content === undefined ? {} : { content: content as Content });
}
export { createPubScriptEditor, pubScriptExtensions };
+57
View File
@@ -0,0 +1,57 @@
/**
* PubScript editor constants and shared attribute definitions.
*
* Kept in a standalone module so the TipTap node definitions (which are
* evaluated at import time) never import back into `schema.ts`. Importing the
* node-name constants from `schema.ts` would create a circular dependency:
* `schema.ts` loads the nodes to build the schema, and the nodes read the
* constants before `schema.ts` has finished initializing — leaving the node
* `name` as `undefined`.
*/
// Node names.
export const SCENE_BREAK = "sceneBreak";
export const FOOTNOTE = "footnote";
export const FOOTNOTE_AREA = "footnoteArea";
export const CHARACTER_MENTION = "characterMention";
// Custom ProseMirror groups. These let content expressions and other
// extensions reference the PubScript nodes as a set without depending on their
// base `block`/`inline` membership.
export const SCENE_BREAK_GROUP = "sceneBreak";
export const FOOTNOTE_GROUP = "footnote";
export const CHARACTER_MENTION_GROUP = "characterMention";
export interface MentionAttrs {
id: string;
label: string;
}
/**
* The `color` attribute shared by text and character mentions. Parses an RGB
* inline style to a hex string on input and writes it back on output. Kept as
* a factory so both the TextStyle mark and the CharacterMention node can spread
* the exact same definition.
*/
export function colorAttribute() {
return {
color: {
default: null,
parseHTML: (element: HTMLElement) =>
element.style.color?.match(/^rgb\((\d+), (\d+), (\d+)\)$/)
? `#${[1, 2, 3]
.map(
(i) =>
parseInt(
element.style.color.slice(i * 2 + 4, i * 2 + 6) ?? "0",
)
.toString(16)
.padStart(2, "0"),
)
.join("")}`
: element.style.color ?? null,
renderHTML: (attributes: { color: string | null }) =>
attributes.color ? { style: `color:${attributes.color}` } : {},
},
};
}
+45
View File
@@ -0,0 +1,45 @@
/**
* PubScript editor package — the TipTap 3 editor, its schema, node views, and
* React wrapper.
*
* Public surface:
* - `createPubScriptEditor`, `pubScriptExtensions`, `PubScriptSchema` — the
* editor instance and its TipTap extensions.
* - `usePubScriptEditor`, `PubScriptEditor` — the React wrapper.
* - `serializeToMarkdown`, `parseMarkdown` — markdown conversion helpers.
* - `pubScriptShortcuts`, `cycleHeading` — keyboard shortcuts.
* - `throttle`, `createIdleDetector` — performance helpers.
*/
export {
createPubScriptEditor,
pubScriptExtensions,
PubScriptSchema,
SCENE_BREAK,
FOOTNOTE,
FOOTNOTE_AREA,
CHARACTER_MENTION,
SCENE_BREAK_GROUP,
FOOTNOTE_GROUP,
CHARACTER_MENTION_GROUP,
} from "./schema.js";
export type { PubScriptEditorHookOptions } from "./Editor.js";
export { usePubScriptEditor, createEditorInstance, emptyDecorationSource } from "./Editor.js";
export {
SceneBreakNode,
SceneBreakComponent,
} from "./nodes/SceneBreak.js";
export { FootnoteInline } from "./nodes/FootnoteInline.js";
export { FootnoteArea } from "./nodes/FootnoteArea.js";
export { CharacterMention } from "./nodes/CharacterMention.js";
export {
pubScriptShortcuts,
cycleHeading,
type ShortcutDef,
} from "./keys.js";
export { serializeToMarkdown, parseMarkdown, markdownInputRules, markdownPasteRules } from "./markdown.js";
export { throttle, createIdleDetector } from "./perf.js";
+103
View File
@@ -0,0 +1,103 @@
/**
* PubScript keyboard map — the shortcuts the writing surface exposes to the
* user, plus the ProseMirror keymap additions. Kept separate from the editor
* instance so the command palette and toolbar can render the same bindings.
*
* The TipTap/StarterKit keymap (bold/italic/lists/undo/redo) is registered by
* the extensions themselves; this module documents the PubScript-level
* shortcuts and provides the heading-cycle helper.
*/
import type { Editor as TipTapEditor } from "@tiptap/core";
import { SCENE_BREAK } from "./schema.js";
export interface ShortcutDef {
id: string;
label: string;
/** Human-readable binding, platform-aware. */
binding: string;
/** Execute the shortcut on the given editor. */
run: (editor: TipTapEditor) => boolean | void;
}
const MOD = typeof navigator !== "undefined" && navigator.platform.toLowerCase().includes("mac")
? "⌘"
: "Ctrl";
/**
* Cycle the paragraph at the cursor through Heading 3 → Heading 2 → Heading 1
* → Normal. Used by the writing surface's "cycle heading" shortcut.
*/
export function cycleHeading(editor: TipTapEditor): boolean {
const { state } = editor;
const { from } = state.selection;
const parent = state.doc.resolve(from).parent;
// Only cycle paragraph / heading blocks.
if (parent.type.name !== "paragraph" && parent.type.name !== "heading") {
return false;
}
const level = editor.isActive("heading") ? (editor.getAttributes("heading") as { level: number }).level : 0;
// 0 == paragraph. Cycle paragraph -> H3 -> H2 -> H1 -> paragraph.
const next = level === 0 ? 3 : level === 3 ? 0 : level - 1;
if (next === 0) {
editor.commands.setNode("paragraph");
} else {
editor.commands.toggleMark("heading", { level: next });
}
return true;
}
/**
* The documented PubScript shortcuts. The toolbar and command palette render
* these so the UI and the actual bindings never drift.
*/
export const pubScriptShortcuts: ShortcutDef[] = [
{
id: "cycle-heading",
label: "Cycle heading",
binding: `${MOD}⇧H`,
run: (editor) => cycleHeading(editor),
},
{
id: "scene-break",
label: "Insert scene break",
binding: `${MOD}⇧⏎`,
run: (editor) => {
editor.commands.insertContent({ type: SCENE_BREAK });
return true;
},
},
{
id: "bold",
label: "Bold",
binding: `${MOD}B`,
run: (editor) => editor.commands.toggleMark("bold"),
},
{
id: "italic",
label: "Italic",
binding: `${MOD}I`,
run: (editor) => editor.commands.toggleMark("italic"),
},
{
id: "strike",
label: "Strikethrough",
binding: `${MOD}S`,
run: (editor) => editor.commands.toggleMark("strike"),
},
{
id: "undo",
label: "Undo",
binding: `${MOD}Z`,
run: (editor) => editor.commands.undo(),
},
{
id: "redo",
label: "Redo",
binding: `${MOD}⇧Z`,
run: (editor) => editor.commands.redo(),
},
];
+78
View File
@@ -0,0 +1,78 @@
// @vitest-environment jsdom
import { describe, expect, it } from "vitest";
import { parseMarkdown, serializeToMarkdown } from "./markdown.js";
/**
* Round-trip tests for the Markdown <-> ProseMirror JSON conversion.
*
* These exercise the CHUNK #3 acceptance criteria: a `.md` file imported into
* the editor must re-export to equivalent Markdown. We assert on the parsed
* JSON structure (which is schema-validated by construction) and on the
* serialized Markdown text.
*/
describe("parseMarkdown", () => {
it("parses headings, paragraphs, and inline marks", () => {
const json = parseMarkdown("# Title\n\nSome **bold** and *italic* text.");
const children = json.content ?? [];
expect(children[0]).toMatchObject({ type: "heading", attrs: { level: 1 } });
expect(children[1]).toMatchObject({ type: "paragraph" });
const body = children[1]?.content ?? [];
const bold = body.find((c) => c.marks?.some((m) => m.type === "bold"));
const italic = body.find((c) => c.marks?.some((m) => m.type === "italic"));
expect(bold).toBeDefined();
expect(italic).toBeDefined();
});
it("parses ordered and unordered lists", () => {
const json = parseMarkdown("- one\n- two\n\n1. first\n2. second");
const types = (json.content ?? []).map((c) => c.type);
expect(types).toContain("bulletList");
expect(types).toContain("orderedList");
});
it("parses fenced code blocks with a language", () => {
const json = parseMarkdown("```ts\nconst x = 1;\n```");
const code = (json.content ?? []).find((c) => c.type === "codeBlock");
expect(code?.attrs?.language).toBe("ts");
});
it("parses scene breaks and footnotes", () => {
const json = parseMarkdown("before\n\n***\n\nafter");
expect((json.content ?? []).some((c) => c.type === "sceneBreak")).toBe(true);
});
it("produces JSON that validates against the PubScript schema", () => {
const json = parseMarkdown("# Hi\n\nHello world.");
// DOMParser.parse already applies the schema's constraints (it throws on
// content that cannot fit the schema), so a successful parse proves the
// JSON is schema-valid. Assert on the resulting structure.
const children = json.content ?? [];
expect(children[0]).toMatchObject({ type: "heading", attrs: { level: 1 } });
expect(children[1]).toMatchObject({ type: "paragraph" });
});
});
describe("serializeToMarkdown", () => {
it("serializes headings, paragraphs, and inline marks", () => {
const json = parseMarkdown("# Title\n\nSome **bold** and *italic* text.");
const md = serializeToMarkdown(json);
expect(md).toContain("# Title");
expect(md).toContain("**bold**");
expect(md).toContain("*italic*");
});
it("round-trips through parseMarkdown to an equivalent document", () => {
const source = "# Chapter\n\nA paragraph with `code`.\n\n- a\n- b";
const once = parseMarkdown(source);
const serialized = serializeToMarkdown(once);
const twice = parseMarkdown(serialized);
const typesOnce = (once.content ?? []).map((c) => c.type);
const typesTwice = (twice.content ?? []).map((c) => c.type);
expect(typesTwice).toEqual(typesOnce);
});
});
+216
View File
@@ -0,0 +1,216 @@
/**
* Markdown support for the PubScript editor.
*
* TipTap v3 folds markdown input rules and paste rules into the individual
* StarterKit extensions (bold/italic/lists/headings/…), so there is no
* separate `@tiptap/extension-markdown` package. This module documents the
* active rules and provides two conversion helpers:
*
* - `serializeToMarkdown(editor, json)` — ProseMirror JSON → CommonMark.
* - `parseMarkdown(editor, md)` — CommonMark → ProseMirror JSON.
*
* The parser is a pragmatic block-level implementation covering the cases the
* writing surface needs (headings, paragraphs, bold/italic/strike, ordered and
* unordered lists, inline and fenced code). It converts to HTML first and lets
* the editor's schema do the final JSON mapping, so the output always conforms
* to the PubScript schema.
*/
import type { JSONContent } from "@tiptap/core";
import { DOMParser as PMDOMParser } from "@tiptap/pm/model";
import { PubScriptSchema } from "./schema.js";
/**
* Active markdown input rules (provided by StarterKit extensions).
*/
export const markdownInputRules = [
{ pattern: /\*\*(.+)\*\*$/, mark: "bold" },
{ pattern: /\*(.+)\*$/, mark: "italic" },
{ pattern: /__(.+)__/, mark: "bold" },
{ pattern: /_(.+)_/, mark: "italic" },
{ pattern: /~~(.+)~~/, mark: "strike" },
{ pattern: /`(.+)`/, mark: "code" },
{ pattern: /^#{1,6} (.*)$/, node: "heading" },
{ pattern: /^[-*+] (.*)$/, node: "listItem" },
{ pattern: /^\d+\. (.*)$/, node: "listItem" },
] as const;
/**
* Active markdown paste rules (provided by StarterKit extensions).
*/
export const markdownPasteRules = markdownInputRules;
/**
* Convert ProseMirror JSON to CommonMark markdown text.
*/
export function serializeToMarkdown(json: JSONContent): string {
return serializeNode(json);
}
function serializeNode(node: JSONContent): string {
if (!node) return "";
const { type, attrs, content, text } = node;
if (typeof text === "string") {
return serializeMarks(node, text);
}
switch (type) {
case "doc":
return (content ?? []).map((child) => serializeNode(child)).join("\n\n");
case "paragraph":
return (content ?? []).map((child) => serializeNode(child)).join("");
case "heading": {
const level = (attrs?.level as number) ?? 1;
return "#".repeat(level) + " " + (content ?? []).map((child) => serializeNode(child)).join("");
}
case "bulletList":
return (content ?? []).map((child) => `* ${serializeNode(child)}`).join("\n");
case "orderedList":
return (content ?? [])
.map((child, i) => `${i + 1}. ${serializeNode(child)}`)
.join("\n");
case "listItem":
return (content ?? []).map((child) => serializeNode(child)).join("");
case "codeBlock":
return "```" + ((attrs?.language as string) ?? "") + "\n" + ((attrs?.content as string) ?? (content ?? []).map((c) => c.text ?? "").join("")) + "\n```";
case "blockquote":
return (content ?? []).map((child) => "> " + serializeNode(child)).join("\n");
case "hardBreak":
return " \n";
case "horizontalRule":
return "---";
default:
return (content ?? []).map((child) => serializeNode(child)).join("");
}
}
function serializeMarks(node: JSONContent, text: string): string {
const marks = (node.marks ?? []) as Array<{ type: string; attrs?: Record<string, unknown> }>;
let out = text;
if (marks.some((m) => m.type === "strike")) out = `~~${out}~~`;
if (marks.some((m) => m.type === "code")) out = `\`${out}\``;
if (marks.some((m) => m.type === "italic")) out = `*${out}*`;
if (marks.some((m) => m.type === "bold")) out = `**${out}**`;
return out;
}
/**
* Convert CommonMark markdown to ProseMirror JSON using the PubScript schema.
*
* The markdown is first normalised to HTML (covering the block/inline cases
* the writing surface uses), then handed to the editor so the result conforms
* to the schema.
*/
export function parseMarkdown(md: string): JSONContent {
const html = markdownToHtml(md);
// Parse the HTML through the PubScript schema directly (via ProseMirror's
// DOMParser) so the result conforms to the schema without constructing a
// live TipTap editor. This avoids keyed-plugin conflicts when multiple
// editors exist and keeps parsing free of editor construction.
const container = document.createElement("div");
container.innerHTML = html;
const parsed = PMDOMParser.fromSchema(PubScriptSchema).parse(container);
return parsed.toJSON();
}
/**
* A pragmatic CommonMark → HTML converter covering the cases the writing
* surface needs. Not a full CommonMark implementation.
*/
function markdownToHtml(md: string): string {
const lines = md.replace(/\r\n/g, "\n").split("\n");
const html: string[] = [];
let inCode = false;
let codeLang = "";
let codeLines: string[] = [];
const listStack: string[] = [];
const closeLists = () => {
while (listStack.length) html.push(`</${listStack.pop()}>`);
};
for (const rawLine of lines) {
const line = rawLine.replace(/\n$/, "");
if (line.trimStart().startsWith("```")) {
if (!inCode) {
closeLists();
inCode = true;
codeLang = line.trim().slice(3).trim();
codeLines = [];
continue;
}
html.push(`<pre><code class="language-${codeLang}">${escapeHtml(codeLines.join("\n"))}</code></pre>`);
inCode = false;
continue;
}
if (inCode) {
codeLines.push(line);
continue;
}
const heading = line.match(/^(#{1,6})\s+(.*)$/);
if (heading) {
closeLists();
const level = heading[1]!.length;
html.push(`<h${level}>${inlineToHtml(heading[2] ?? "")}</h${level}>`);
continue;
}
// Scene break: a centered divider written as `***` or `* * *`.
const sceneBreak = line.trim();
if (sceneBreak === "***" || sceneBreak === "* * *" || sceneBreak === "• • •") {
closeLists();
html.push('<hr class="scene-break">');
continue;
}
const ul = line.match(/^([-*+])\s+(.*)$/);
const ol = line.match(/^\d+\.\s+(.*)$/);
if (ul || ol) {
const tag = ul ? "ul" : "ol";
if (listStack[listStack.length - 1] !== tag) {
closeLists();
listStack.push(tag);
html.push(`<${tag}>`);
}
const itemText = ul ? ul[2] : ol![1];
html.push(`<li>${inlineToHtml(itemText ?? "")}</li>`);
continue;
}
closeLists();
if (line.trim() === "") {
html.push("");
} else {
html.push(`<p>${inlineToHtml(line)}</p>`);
}
}
closeLists();
if (inCode) {
html.push(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
}
return html.filter((l) => l !== "").join("\n");
}
function inlineToHtml(text: string): string {
let out = escapeHtml(text);
out = out.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
out = out.replace(/__(.+?)__/g, "<strong>$1</strong>");
out = out.replace(/\*(.+?)\*/g, "<em>$1</em>");
out = out.replace(/__(.+?)__/g, "<em>$1</em>");
out = out.replace(/~~(.+)~~/g, "<s>$1</s>");
out = out.replace(/`(.+)`/g, "<code>$1</code>");
return out;
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
@@ -0,0 +1,46 @@
import { Node } from "@tiptap/core";
import { CHARACTER_MENTION, colorAttribute } from "../constants.js";
/**
* Character mention — an inline atom linking to a character entity.
* Attributes: `id` (character id) and `label` (display name). Inline in HTML
* as `<span class="mention mention-character" data-id="...">@Name</span>`.
*/
export const CharacterMention = Node.create({
name: CHARACTER_MENTION,
group: "inline",
atom: true,
inline: true,
addAttributes() {
return {
id: {
default: null,
isRequired: true,
parseHTML: (element) => element.getAttribute("data-id"),
renderHTML: (attributes) => ({ "data-id": attributes.id }),
},
label: {
default: "",
parseHTML: (element) => element.getAttribute("data-label") ?? element.textContent ?? "",
renderHTML: (attributes) => ({ "data-label": attributes.label }),
},
...colorAttribute(),
};
},
parseHTML() {
return [{ tag: "span.mention-character" }];
},
renderHTML({ node }) {
return [
"span",
{ class: "mention mention-character", "data-id": node.attrs.id, "data-label": node.attrs.label },
"@",
node.attrs.label ?? node.attrs.id,
];
},
});
export default CharacterMention;
@@ -0,0 +1,53 @@
import { Node } from "@tiptap/core";
import { FOOTNOTE_AREA } from "../constants.js";
/**
* Footnote area — a block node that renders a footnote body. Linked to its
* inline marker by the shared `id`. Inline in HTML as
* `<aside class="footnote-area" data-id="...">`.
*/
export const FootnoteArea = Node.create({
name: FOOTNOTE_AREA,
group: "block footnoteArea",
content: "(paragraph | blockquote)+",
defining: true,
addAttributes() {
return {
id: {
default: null,
isRequired: true,
parseHTML: (element) => element.getAttribute("data-id"),
renderHTML: (attributes) => ({ "data-id": attributes.id }),
},
};
},
parseHTML() {
return [{ tag: "aside.footnote-area" }];
},
renderHTML({ node }) {
return [
"aside",
{ class: "footnote-area", "data-id": node.attrs.id },
["p", { class: "footnote-number" }, `#${node.attrs.id}`],
["div", { class: "footnote-body" }, 0],
];
},
addKeyboardShortcuts() {
return {
Backspace: ({ editor }) => {
const { from } = editor.state.selection;
// Move cursor out of the footnote area when empty/backspacing at start.
if (from === editor.state.doc.resolve(from).parentOffset) {
return false;
}
return false;
},
};
},
});
export default FootnoteArea;
@@ -0,0 +1,51 @@
import { Node } from "@tiptap/core";
import { FOOTNOTE } from "../constants.js";
/**
* Footnote marker — an inline atom that references a footnote by id. The
* matching `FootnoteArea` block renders the note body. Inline in HTML as
* `<span class="footnote" data-id="...">`.
*/
export const FootnoteInline = Node.create({
name: FOOTNOTE,
group: "inline",
atom: true,
inline: true,
addAttributes() {
return {
id: {
default: null,
isRequired: true,
parseHTML: (element) => element.getAttribute("data-id"),
renderHTML: (attributes) => ({ "data-id": attributes.id }),
},
label: {
default: "",
parseHTML: (element) => element.textContent ?? "",
renderHTML: (attributes) => (attributes.label ? {} : {}),
},
};
},
parseHTML() {
return [{ tag: "span.footnote" }];
},
renderHTML({ node }) {
return [
"span",
{ class: "footnote", "data-id": node.attrs.id, "data-label": node.attrs.label },
node.attrs.label ?? node.attrs.id,
];
},
addKeyboardShortcuts() {
return {
Backspace: () => false,
Delete: () => false,
};
},
});
export default FootnoteInline;
+68
View File
@@ -0,0 +1,68 @@
import { Node } from "@tiptap/core";
import { ReactNodeViewRenderer, type ReactNodeViewProps } from "@tiptap/react";
import { SCENE_BREAK } from "../constants.js";
/**
* Scene break node view — renders a centered `• • •` divider. Clicking the
* divider re-renders it; the underlying node is an empty block whose HTML
* representation is `<hr class="scene-break">`.
*/
export const SceneBreakNode = Node.create({
name: SCENE_BREAK,
group: "block",
selectable: true,
draggable: true,
atom: true,
addAttributes() {
return {
break: {
default: "***",
parseHTML: (element) => element.getAttribute("data-break") ?? "***",
renderHTML: (attributes) => ({ "data-break": attributes.break ?? "***" }),
},
};
},
parseHTML() {
// Higher priority than StarterKit's horizontalRule (`tag: "hr"`) so that
// `<hr class="scene-break">` resolves to a scene break, not a rule.
return [{ tag: "hr.scene-break", priority: 2000 }];
},
renderHTML({ node }) {
return ["hr", { class: "scene-break", "data-break": node.attrs.break ?? "***" }];
},
addNodeView() {
return ReactNodeViewRenderer(SceneBreakComponent);
},
});
/**
* The React node view. TipTap's `ReactNodeViewRenderer` supplies the
* contentDOM/selection handling; we just render the divider.
*/
export function SceneBreakComponent({ editor, node, selected }: ReactNodeViewProps) {
return (
<div
className="ps-scene-break"
data-selected={selected ? "" : undefined}
data-break={node.attrs.break ?? "***"}
>
<button
type="button"
className="ps-scene-break-inner"
aria-label="Scene break"
tabIndex={-1}
onDoubleClick={() => {
editor.commands.updateAttributes(SCENE_BREAK, { break: "***" });
}}
>
{editor.isEditable ? "• • •" : ""}
</button>
</div>
);
}
export default SceneBreakNode;
+62
View File
@@ -0,0 +1,62 @@
/**
* Performance helpers for the editor — throttled autosave and idle detection.
*/
/**
* Return a throttled wrapper of `fn` that runs at most once per `wait` ms.
* The trailing invocation always runs after the pause ends.
*/
export function throttle<T extends (...args: never[]) => void>(
fn: T,
wait: number,
): (...args: Parameters<T>) => void {
let last = 0;
let timer: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
const now = Date.now();
const remaining = wait - (now - last);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
last = now;
fn(...args);
} else if (!timer) {
timer = setTimeout(() => {
last = Date.now();
timer = null;
fn(...args);
}, remaining);
}
};
}
/**
* A tiny idle detector. `onIdle` fires once after the caller stops calling
* `poke()` for `idleMs` milliseconds. Useful for triggering a save after the
* writer pauses.
*/
export function createIdleDetector(idleMs: number, onIdle: () => void): {
poke: () => void;
cancel: () => void;
} {
let timer: ReturnType<typeof setTimeout> | null = null;
const reset = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
onIdle();
}, idleMs);
};
return {
poke: reset,
cancel: () => {
if (timer) clearTimeout(timer);
timer = null;
},
};
}
+112
View File
@@ -0,0 +1,112 @@
/**
* PubScript editor schema — all TipTap v3 extensions registered exactly once.
*
* Base prose comes from StarterKit (Document, Paragraph, Text, Heading 1–3,
* Blockquote, HorizontalRule, HardBreak, Bold, Italic, Strike, Code,
* CodeBlock). On top we layer three PubScript-specific nodes:
*
* - SceneBreak a block node rendered as a centered `***` divider.
* - Footnote an inline atom that references a footnote id; the matching
* FootnoteArea blocks render the note body.
* - CharacterMention an inline atom that links to a character entity.
*
* Every v3 call (`Extension.create`, `Node.create`, `Node.create` with
* `inline: true`, `TipTapEditor.create`) is wrapped here so the rest of the app
* never touches the raw TipTap API directly.
*/
import StarterKit from "@tiptap/starter-kit";
import { TextStyle } from "@tiptap/extension-text-style";
import { Editor as TipTapEditor, getSchema, type Content } from "@tiptap/core";
import SceneBreakNode from "./nodes/SceneBreak.js";
import FootnoteInline from "./nodes/FootnoteInline.js";
import FootnoteArea from "./nodes/FootnoteArea.js";
import CharacterMention from "./nodes/CharacterMention.js";
import {
SCENE_BREAK,
FOOTNOTE,
FOOTNOTE_AREA,
CHARACTER_MENTION,
SCENE_BREAK_GROUP,
FOOTNOTE_GROUP,
CHARACTER_MENTION_GROUP,
MentionAttrs,
colorAttribute,
} from "./constants.js";
/**
* The full extension list, registered once. Order matters: base kits first,
* then text styling, then PubScript nodes.
*/
export const pubScriptExtensions = [
StarterKit,
TextStyle.extend({
addAttributes: colorAttribute,
}),
SceneBreakNode,
FootnoteInline,
FootnoteArea,
CharacterMention,
];
/**
* The resolved ProseMirror schema for the PubScript editor. Built once from
* `pubScriptExtensions` via TipTap's `getSchema` helper so the schema can be
* shared (e.g. for serialization) without constructing a live editor.
*/
export const PubScriptSchema = getSchema(pubScriptExtensions);
export interface PubScriptEditorOptions {
editor?: TipTapEditor;
editable?: boolean;
extensions?: typeof pubScriptExtensions;
/** Initial document content (ProseMirror JSON or HTML string). */
content?: Content;
}
/**
* Create the PubScript editor. Accepts an existing `editor` instance so the
* headless instance can be shared across React components (avoids re-creating
* the ProseMirror state on every render).
*/
export function createPubScriptEditor(options: PubScriptEditorOptions = {}): TipTapEditor {
const { editor, editable = true, extensions = pubScriptExtensions, content } = options;
if (editor) {
editor.setEditable(editable);
return editor;
}
return new TipTapEditor({
editable,
extensions,
content: content ?? `
<h1>Untitled Chapter</h1>
<p>Begin your scene here.</p>
`,
editorProps: {
attributes: {
class: "ps-editor-content",
},
},
})!;
}
/**
* Helper used by the React wrapper to build an editor instance.
*/
export type { TipTapEditor };
export type { MentionAttrs };
export {
SCENE_BREAK,
FOOTNOTE,
FOOTNOTE_AREA,
CHARACTER_MENTION,
SCENE_BREAK_GROUP,
FOOTNOTE_GROUP,
CHARACTER_MENTION_GROUP,
colorAttribute,
};
+70
View File
@@ -0,0 +1,70 @@
/**
* PubScript editor styles.
*
* These styles scope to the editor root (`.ps-editor`) so they never leak into
* the surrounding app. The writing surface (apps/web) is responsible for the
* font system, theming, and layout; this file only styles the editor's own
* rendered nodes.
*/
.ps-editor {
color: inherit;
font-family: inherit;
line-height: inherit;
}
.ps-editor-content {
min-height: 20rem;
outline: none;
}
.ps-editor-content :first-child {
margin-top: 0;
}
/* Scene break — a centered, non-textual divider. */
.ps-scene-break {
display: flex;
align-items: center;
justify-content: center;
margin: 1.5rem 0;
}
.ps-scene-break-inner {
border: none;
background: none;
cursor: default;
font-size: 1rem;
letter-spacing: 0.5rem;
color: currentColor;
opacity: 0.6;
padding: 0 1rem;
}
.ps-scene-break[data-selected=""] .ps-scene-break-inner {
opacity: 1;
}
/* Footnote marker (inline) and area (block). */
span.footnote {
cursor: default;
vertical-align: super;
font-size: 0.75em;
color: color-mix(in srgb, currentColor 70%, transparent);
}
aside.footnote-area {
border-inline-start: 2px solid color-mix(in srgb, currentColor 25%, transparent);
padding-inline-start: 1rem;
margin: 1rem 0;
color: color-mix(in srgb, currentColor 80%, transparent);
}
.footnote-number {
font-weight: 600;
margin: 0;
}
.footnote-body {
margin: 0;
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"types": ["node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
+548 -2
View File
@@ -49,7 +49,7 @@ importers:
version: 7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
zustand:
specifier: ^5.0.2
version: 5.0.16(@types/react@19.3.0)(react@19.3.1)
version: 5.0.16(@types/react@19.3.0)(react@19.3.1)(use-sync-external-store@1.7.0(react@19.3.1))
devDependencies:
'@types/node':
specifier: ^22.10.2
@@ -104,6 +104,43 @@ importers:
specifier: ^2.1.8
version: 2.1.9(@types/node@22.20.5)(jsdom@25.0.1)
packages/editor:
dependencies:
'@pubscript/core':
specifier: workspace:*
version: link:../core
'@tiptap/core':
specifier: 3.32.0
version: 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-text-style':
specifier: 3.32.0
version: 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/pm':
specifier: 3.32.0
version: 3.32.0
'@tiptap/react':
specifier: 3.32.0
version: 3.32.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
'@tiptap/starter-kit':
specifier: 3.32.0
version: 3.32.0
devDependencies:
'@types/node':
specifier: ^22.10.2
version: 22.20.5
'@types/react':
specifier: ^19.0.2
version: 19.3.0
react:
specifier: ^19.0.0
version: 19.3.1
typescript:
specifier: ^5.7.2
version: 5.9.3
vitest:
specifier: ^2.1.8
version: 2.1.9(@types/node@22.20.5)(jsdom@25.0.1)
packages/ui:
dependencies:
'@pubscript/core':
@@ -824,6 +861,15 @@ packages:
'@fastify/websocket@11.3.4':
resolution: {integrity: sha512-firnJtEOv6pfdoXIzFDMbd+omgWwFNfLtdM8JejNFr/9lT/9DHkEhjTShuRFeE4phqrevD2B3JRW0v6uyzWhHQ==}
'@floating-ui/core@1.8.0':
resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
'@floating-ui/dom@1.8.0':
resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
@@ -1009,6 +1055,161 @@ packages:
cpu: [x64]
os: [win32]
'@tiptap/core@3.32.0':
resolution: {integrity: sha512-Ghh2h02GKSqzytQGcLV4espYOG4EHxjj0MsrUB2Vnr47xmlZYRf6r7k2J09H+JpBdaGrWwrTkyBEi5i3c3yy2Q==}
peerDependencies:
'@tiptap/pm': 3.32.0
'@tiptap/extension-blockquote@3.32.0':
resolution: {integrity: sha512-wmuthEdlYVArNoUbqFPH35giutBABFu7fqEB0lTD1k0UXJUs4dFvcbRyGWWgnHFZgP3TT4u8e+xiayYw0eQ0NQ==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/pm': 3.32.0
'@tiptap/extension-bold@3.32.0':
resolution: {integrity: sha512-SQYewnYDoIBN+8oH53IDcD8p+Pz+Jnyq3hrxjy/FAo5ctc+96d3ZLdqc9gzLVSqFFo/SUINv2FVGoq9PvU7yqw==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extension-bubble-menu@3.32.0':
resolution: {integrity: sha512-X3ll8i9Z8QNvt7+tcqlwopJG+68ugO13Av0Ox4Ico+jXcTnFdDuKQJBSI9sLuMR5vTQOeTHhKYXhjF88aahMSA==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/pm': 3.32.0
'@tiptap/extension-bullet-list@3.32.0':
resolution: {integrity: sha512-Y5P/l06pVCUG46vXpBl3z0fW2b6JpSMX4Cvr+5sfQh8zB3TxwSiLQaaPwI3R5MFD1KfRk3AjiPRieEyXdWcNHw==}
peerDependencies:
'@tiptap/extension-list': 3.32.0
'@tiptap/extension-code-block@3.32.0':
resolution: {integrity: sha512-9NZW/yXP9j9XKr+hRS5m58xiyZI+tQo1PJ7eLHXjGNFWaF601NG/h6741mbKh5uCY51XsbyqG+7que+RavngPA==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/pm': 3.32.0
'@tiptap/extension-code@3.32.0':
resolution: {integrity: sha512-I+aEVl0dTmkfnU6HzBGEefb9U7ja2MkucwBBwrmoMwU+uIKpC4xw8NcHC9X6sjAcom3Pxs2HX+8WEo4Vp13R/Q==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extension-document@3.32.0':
resolution: {integrity: sha512-GugeKE6AnZDluRXJUNBuX43TPcaJxy5wck6vT2J4a0/EnKStJ43suSZZf1IPfb1ij9EL9bYvAwg9DOcrvdEzTQ==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extension-dropcursor@3.32.0':
resolution: {integrity: sha512-t4Ug8hFLt30dwwYvrh63BVUt6N1Yg5PVI3tMllhCmN3Q68oD47v4MB/i5nGfzIsQqMyXUOetwHJWcZ4MoLTx4Q==}
peerDependencies:
'@tiptap/extensions': 3.32.0
'@tiptap/extension-floating-menu@3.32.0':
resolution: {integrity: sha512-AzKCWceL+4Q1pTc8Whw1kPqN/aYGPQ70/jXPsblWl501t/pXi6Kz0Pi/NggKkuu73VGjykh68YYf/dcOJ/mPZQ==}
peerDependencies:
'@floating-ui/dom': ^1.0.0
'@tiptap/core': 3.32.0
'@tiptap/pm': 3.32.0
'@tiptap/extension-gapcursor@3.32.0':
resolution: {integrity: sha512-HUpKKWJLtthnpQY5LIjHlJJOmh562RusmOMIe/4iP/H7y+WUR9ZFiZjEs1NBkRjig9fwj8F3BfsrVPYBnM4nLA==}
peerDependencies:
'@tiptap/extensions': 3.32.0
'@tiptap/extension-hard-break@3.32.0':
resolution: {integrity: sha512-IKUYvK+bO2hGrDWhhTVg0CSh9C2belWg9GyLzvpyd3SZyQ9EgdccLfoytNjOBKbAE/TQ4Oo3kSP+XF6RsUIKPA==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extension-heading@3.32.0':
resolution: {integrity: sha512-XRiZxZuhfjvbgwCvoBHj04gOsPANEXxxRwrFCQVuFi5yeTHYIzMEioc81j6iAH9pV7aQeJCwLxuh6VHNz3dXEQ==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extension-horizontal-rule@3.32.0':
resolution: {integrity: sha512-Iry7ynsG4xgcxnH1cm+uXJnEtUzoop4sGA10NUN9I+08TOjhj/GrDiRKMQHKAiVI/R7t2MVav1MJlxFf2FdzGw==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/pm': 3.32.0
'@tiptap/extension-italic@3.32.0':
resolution: {integrity: sha512-KB/i1xoHilbBGcQ+UotDWhi60KpdCde3RzRIpl2na0Yol4z4Fe2IRWg8ljIrlsssDRwel/fsHJbG7a13EP8oKA==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extension-link@3.32.0':
resolution: {integrity: sha512-DRdQSgWPSEjrDg5EDBB5yxRygHtUVLhceVGzAsTz8RziiFyvcwAvzBHCO9hd9jutjCXQ2DLIuZB8P+aHhFWnpg==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/pm': 3.32.0
'@tiptap/extension-list-item@3.32.0':
resolution: {integrity: sha512-FdJTCAeALixzyyGSSEmroXmkIEclbo/4t2Pl03aRERqpPOSPJcC3evDtIhLazTuP6v+FjCmJ+pXNql6ov7qitA==}
peerDependencies:
'@tiptap/extension-list': 3.32.0
'@tiptap/extension-list-keymap@3.32.0':
resolution: {integrity: sha512-vXFre18Dk2m5N/gjo408C02GSrolZ8ZIf0wz6/U8lNC4FcYOISgZFj3BND6uabVntGh2GCSPrVFuNpVPhFnn/A==}
peerDependencies:
'@tiptap/extension-list': 3.32.0
'@tiptap/extension-list@3.32.0':
resolution: {integrity: sha512-QdzS5nFz9agAQhBgIuQluobWib39W2cMKx1XB6tYSue7bHMJdgPFP0EnGfFajaeVX42CG6RXtQWrNfyQGKhBvg==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/pm': 3.32.0
'@tiptap/extension-ordered-list@3.32.0':
resolution: {integrity: sha512-htZ5hoMLBkGiDP6M5mGemjOoaBmkrj911wiGoTmMMKzO7qJq9QxUBkLY7ILPaKlUWL8U3ccLdDV2TB/nysp4Jg==}
peerDependencies:
'@tiptap/extension-list': 3.32.0
'@tiptap/extension-paragraph@3.32.0':
resolution: {integrity: sha512-afKLMc7fWMkkar9H5FilizStSbhFzUoCzdwwdZf6DDsn3LUt8tJlFHACK78cAyx/TzMJBo0N8/8T1u9bL+N67Q==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extension-strike@3.32.0':
resolution: {integrity: sha512-J9Vw6wmh/wV6SxSsN9RhurvAGcXSY5J3p4EQrROD5PARBl0VOnDFi7/UVoz1RkSdqOKPgAiqDZpaLRgtdBO9Rg==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extension-text-style@3.32.0':
resolution: {integrity: sha512-+U8FkrNAU45+fHgo4DYfndS/WuhhyttZ+zTyObF1W4VJ8wDiAYqXkl71nQcmMJyyK47yeNUY9CfjUomIaEHhpA==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extension-text@3.32.0':
resolution: {integrity: sha512-sF0jIK67IHdZMnzv2mz5vPTyK7R0ui6/YZgFg+/bLnFCNmweVAoYcHZLlC7ISRnwu0mqJr+epNtzJepY1ZDCjg==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extension-underline@3.32.0':
resolution: {integrity: sha512-rMLdd4vg6sed/JnaXnaupLbCtYiWikTnQ8qir9ItKOXwf1Ui9P1Y4G0bYU4+gcnMsFzaSGtrOhMqG4/c66AogQ==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/extensions@3.32.0':
resolution: {integrity: sha512-qO+ndNvlOjzU2MWCtQHoRg1tIortCXLvqPjDx58F1pUYFNCOVZegXi+jqLGDODUA31er+TVz66umzRbbN1bO1Q==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/pm': 3.32.0
'@tiptap/pm@3.32.0':
resolution: {integrity: sha512-H9eN+xHFboJybE7KPGVsu24S8aBlCIgYz38vCHRSnNv17nwmqm06owq/IQ5scza0n3Y7IfUfpgPOtm9SgeHmNQ==}
'@tiptap/react@3.32.0':
resolution: {integrity: sha512-+iS9RKj6Ve8SXqJ49xptDiifF7PQXmUOh4cjMu+BCXXqKphQkiUccvbWYAeOJCTceM01wWgFhrpfy8qG+CHJYw==}
peerDependencies:
'@tiptap/core': 3.32.0
'@tiptap/pm': 3.32.0
'@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.32.0':
resolution: {integrity: sha512-aPYxaevci4PgMrTLsg+baiqI7M1ICd4V9H+zTTdrqs8OUZIbAvj/pNDuHU8uWZyKMu3Dh72t2D6ZdqRu/6aSWw==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -1041,6 +1242,9 @@ packages:
'@types/react@19.3.0':
resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==}
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@types/ws@8.18.2':
resolution: {integrity: sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==}
@@ -1489,6 +1693,10 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-equals@5.4.5:
resolution: {integrity: sha512-yKEpcFuRPu/pTQL2VB8YKE2yFldrLlIsoWtJeBnAdqEhvu21OgkFT0C8njf/xYc2LSZGEr3K9JGjthHJxnVLvQ==}
engines: {node: '>=6.0.0'}
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
@@ -1739,6 +1947,9 @@ packages:
light-my-request@6.8.0:
resolution: {integrity: sha512-5Q8PjoSEdtGIB8fikZsTem5vgRo4F2mR6vUKTDW/H1g6MtAt8e3duQbg/QxL5yzg6BZhor2KOUm3hhWMWLTZwg==}
linkifyjs@4.4.0:
resolution: {integrity: sha512-InTexvtbai+2c+dEVGcc6O6P2DMVIiwD9a/i4zFroazOIYLOS6FSVmAJ7kFeumnCMx6/gQqNAkbUvgMubV4+MA==}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -1844,6 +2055,9 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@@ -1920,6 +2134,45 @@ packages:
process-warning@5.1.1:
resolution: {integrity: sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==}
prosemirror-changeset@2.4.4:
resolution: {integrity: sha512-Gyuz9jYdrAeA9I7AholyOes9KUmv/lEwsC5O9s5xF5VB3EgTPsXMGUxz4+H1B+wKVPdZbvRcg1Cp3x8qX+8ofA==}
prosemirror-commands@1.7.2:
resolution: {integrity: sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==}
prosemirror-dropcursor@1.8.4:
resolution: {integrity: sha512-VmKxhcA6a+h5oPSYmfF1ZNeQqPbyYaIxqZsKqeYCyTlPtrjUgNaHL3P8vVaO19oqAV0zuaM/axkowZHrB1P3Tg==}
prosemirror-gapcursor@1.4.1:
resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==}
prosemirror-history@1.5.1:
resolution: {integrity: sha512-onlwnqKvgtFg6srZOmDOAhlAy1FIHQpOExORybX0S+DvJepE5tPALrB9iGet/2sqqqrelSnB10OoiOJqAvu2DA==}
prosemirror-inputrules@1.5.1:
resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==}
prosemirror-keymap@1.2.3:
resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
prosemirror-model@1.25.12:
resolution: {integrity: sha512-Ue2gTmXMa7EhpLNhC7J+h4+ykD8ha12K6rrZFFKKJHBForfIStw5gJ6Zrf1mqqAa9NmpmEB4wp9bKmA4eUVjWg==}
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.4:
resolution: {integrity: sha512-2/WuPum3pn+O/Vi2vzpi8210ICUe2kIPXdLYjQ09Pv5DtT5bcKWMJhAKPBwvpPjpp5iOYUVwVEr0gOM1BY1Mgw==}
prosemirror-view@1.42.8:
resolution: {integrity: sha512-ftDZjCs9AjAekXmCfJRn2U3IdlRCl0Yc4tZT1/jJWCsz4DiSifEBXtWmpJpw/4f1Rg328uAUQ+u2lCJ0+AVZkw==}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
@@ -1995,6 +2248,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==}
@@ -2208,6 +2464,11 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
use-sync-external-store@1.7.0:
resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -2312,6 +2573,9 @@ packages:
jsdom:
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'}
@@ -2873,6 +3137,20 @@ snapshots:
- bufferutil
- utf-8-validate
'@floating-ui/core@1.8.0':
dependencies:
'@floating-ui/utils': 0.2.12
optional: true
'@floating-ui/dom@1.8.0':
dependencies:
'@floating-ui/core': 1.8.0
'@floating-ui/utils': 0.2.12
optional: true
'@floating-ui/utils@0.2.12':
optional: true
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
@@ -2996,6 +3274,183 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.65.0':
optional: true
'@tiptap/core@3.32.0(@tiptap/pm@3.32.0)':
dependencies:
'@tiptap/pm': 3.32.0
'@tiptap/extension-blockquote@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/pm': 3.32.0
'@tiptap/extension-bold@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-bubble-menu@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)':
dependencies:
'@floating-ui/dom': 1.8.0
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/pm': 3.32.0
optional: true
'@tiptap/extension-bullet-list@3.32.0(@tiptap/extension-list@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/extension-list': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-code-block@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/pm': 3.32.0
'@tiptap/extension-code@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-document@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-dropcursor@3.32.0(@tiptap/extensions@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/extensions': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-floating-menu@3.32.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)':
dependencies:
'@floating-ui/dom': 1.8.0
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/pm': 3.32.0
optional: true
'@tiptap/extension-gapcursor@3.32.0(@tiptap/extensions@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/extensions': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-hard-break@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-heading@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-horizontal-rule@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/pm': 3.32.0
'@tiptap/extension-italic@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-link@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/pm': 3.32.0
linkifyjs: 4.4.0
'@tiptap/extension-list-item@3.32.0(@tiptap/extension-list@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/extension-list': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-list-keymap@3.32.0(@tiptap/extension-list@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/extension-list': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-list@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/pm': 3.32.0
'@tiptap/extension-ordered-list@3.32.0(@tiptap/extension-list@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/extension-list': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-paragraph@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-strike@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-text-style@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-text@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-underline@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extensions@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/pm': 3.32.0
'@tiptap/pm@3.32.0':
dependencies:
prosemirror-changeset: 2.4.4
prosemirror-commands: 1.7.2
prosemirror-dropcursor: 1.8.4
prosemirror-gapcursor: 1.4.1
prosemirror-history: 1.5.1
prosemirror-inputrules: 1.5.1
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.12
prosemirror-schema-list: 1.5.1
prosemirror-state: 1.4.4
prosemirror-tables: 1.8.5
prosemirror-transform: 1.12.4
prosemirror-view: 1.42.8
'@tiptap/react@3.32.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/pm': 3.32.0
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
'@types/use-sync-external-store': 0.0.6
fast-equals: 5.4.5
react: 19.3.1
react-dom: 19.3.1(react@19.3.1)
use-sync-external-store: 1.7.0(react@19.3.1)
optionalDependencies:
'@tiptap/extension-bubble-menu': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-floating-menu': 3.32.0(@floating-ui/dom@1.8.0)(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
transitivePeerDependencies:
- '@floating-ui/dom'
'@tiptap/starter-kit@3.32.0':
dependencies:
'@tiptap/core': 3.32.0(@tiptap/pm@3.32.0)
'@tiptap/extension-blockquote': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-bold': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/extension-bullet-list': 3.32.0(@tiptap/extension-list@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))
'@tiptap/extension-code': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/extension-code-block': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-document': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/extension-dropcursor': 3.32.0(@tiptap/extensions@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))
'@tiptap/extension-gapcursor': 3.32.0(@tiptap/extensions@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))
'@tiptap/extension-hard-break': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/extension-heading': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/extension-horizontal-rule': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-italic': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/extension-link': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-list': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/extension-list-item': 3.32.0(@tiptap/extension-list@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))
'@tiptap/extension-list-keymap': 3.32.0(@tiptap/extension-list@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))
'@tiptap/extension-ordered-list': 3.32.0(@tiptap/extension-list@3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0))
'@tiptap/extension-paragraph': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/extension-strike': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/extension-text': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/extension-underline': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))
'@tiptap/extensions': 3.32.0(@tiptap/core@3.32.0(@tiptap/pm@3.32.0))(@tiptap/pm@3.32.0)
'@tiptap/pm': 3.32.0
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.9
@@ -3037,6 +3492,8 @@ snapshots:
dependencies:
csstype: 3.2.3
'@types/use-sync-external-store@0.0.6': {}
'@types/ws@8.18.2':
dependencies:
'@types/node': 22.20.5
@@ -3588,6 +4045,8 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-equals@5.4.5: {}
fast-json-stable-stringify@2.1.0: {}
fast-json-stringify@7.1.3:
@@ -3865,6 +4324,8 @@ snapshots:
process-warning: 5.1.1
set-cookie-parser: 3.2.1
linkifyjs@4.4.0: {}
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -3946,6 +4407,8 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
orderedmap@2.1.1: {}
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
@@ -4028,6 +4491,80 @@ snapshots:
process-warning@5.1.1: {}
prosemirror-changeset@2.4.4:
dependencies:
prosemirror-transform: 1.12.4
prosemirror-commands@1.7.2:
dependencies:
prosemirror-model: 1.25.12
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.4
prosemirror-dropcursor@1.8.4:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.4
prosemirror-view: 1.42.8
prosemirror-gapcursor@1.4.1:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.12
prosemirror-state: 1.4.4
prosemirror-view: 1.42.8
prosemirror-history@1.5.1:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.4
prosemirror-view: 1.42.8
rope-sequence: 1.3.4
prosemirror-inputrules@1.5.1:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.4
prosemirror-keymap@1.2.3:
dependencies:
prosemirror-state: 1.4.4
w3c-keyname: 2.2.8
prosemirror-model@1.25.12:
dependencies:
orderedmap: 2.1.1
prosemirror-schema-list@1.5.1:
dependencies:
prosemirror-model: 1.25.12
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.4
prosemirror-state@1.4.4:
dependencies:
prosemirror-model: 1.25.12
prosemirror-transform: 1.12.4
prosemirror-view: 1.42.8
prosemirror-tables@1.8.5:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.12
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.4
prosemirror-view: 1.42.8
prosemirror-transform@1.12.4:
dependencies:
prosemirror-model: 1.25.12
prosemirror-view@1.42.8:
dependencies:
prosemirror-model: 1.25.12
prosemirror-state: 1.4.4
prosemirror-transform: 1.12.4
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
@@ -4117,6 +4654,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.65.0
fsevents: 2.3.3
rope-sequence@1.3.4: {}
rrweb-cssom@0.7.1: {}
rrweb-cssom@0.8.0: {}
@@ -4294,6 +4833,10 @@ snapshots:
dependencies:
punycode: 2.3.1
use-sync-external-store@1.7.0(react@19.3.1):
dependencies:
react: 19.3.1
util-deprecate@1.0.2: {}
vite-node@2.1.9(@types/node@22.20.5):
@@ -4372,6 +4915,8 @@ snapshots:
- supports-color
- terser
w3c-keyname@2.2.8: {}
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
@@ -4414,7 +4959,8 @@ snapshots:
zod@3.25.76: {}
zustand@5.0.16(@types/react@19.3.0)(react@19.3.1):
zustand@5.0.16(@types/react@19.3.0)(react@19.3.1)(use-sync-external-store@1.7.0(react@19.3.1)):
optionalDependencies:
'@types/react': 19.3.0
react: 19.3.1
use-sync-external-store: 1.7.0(react@19.3.1)