Files
PubScript/packages/editor/src/keys.ts
T
Schwitzz c95f5e485a 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).
2026-10-25 02:37:05 -05:00

104 lines
2.9 KiB
TypeScript

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