/** * 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(), }, ];