Files
PubScript/packages/editor/src/nodes/SceneBreak.tsx
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

69 lines
1.9 KiB
TypeScript

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;