feat(editor): add figure block with placement and caption

This commit is contained in:
pj committed 2026-06-20 12:46:18 -04:00
1 parent e76558fe1f
commit 50bde45d65
3 files changed
+78 -1

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
import { NodeViewWrapper, type NodeViewProps } from "@tiptap/react";
const PLACEMENTS = [
{ key: "inline", label: "Inline" },
{ key: "full-width", label: "Full width" },
{ key: "full-page", label: "Full page" },
{ key: "float-top", label: "Float" },
];
export function FigureView({ node, updateAttributes, selected }: NodeViewProps) {
const { src, alt, caption, placement } = node.attrs;
return (
<NodeViewWrapper className="figure" data-placement={placement} data-selected={selected} contentEditable={false}>
<div className="figure-frame">
{src ? <img src={src} alt={alt} /> : <div className="figure-empty">No image</div>}
</div>
<input
className="figure-caption"
value={caption}
placeholder="Add a caption…"
onChange={(e) => updateAttributes({ caption: e.target.value })}
/>
{selected && (
<div className="figure-controls">
{PLACEMENTS.map((p) => (
<button key={p.key} data-on={placement === p.key} onClick={() => updateAttributes({ placement: p.key })}>
{p.label}
</button>
))}
</div>
)}
</NodeViewWrapper>
);
}
+5 -1
View File
@@ -1,11 +1,15 @@
import type { Extensions } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import { Figure } from "./figure";
export const editorExtensions: Extensions = [
StarterKit.configure({
heading: { levels: [2, 3] },
codeBlock: false,
}),
Placeholder.configure({ placeholder: "Begin your chapter…" }),
Placeholder.configure({
placeholder: ({ node }) => (node.type.name === "heading" ? "" : "Begin your chapter…"),
}),
Figure,
];
+38
View File
@@ -0,0 +1,38 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { FigureView } from "./FigureView";
export const Figure = Node.create({
name: "figure",
group: "block",
atom: true,
draggable: true,
selectable: true,
addAttributes() {
return {
src: { default: "" },
alt: { default: "" },
caption: { default: "" },
placement: { default: "full-width" },
};
},
parseHTML() {
return [{ tag: "figure[data-figure]" }];
},
renderHTML({ HTMLAttributes }) {
const { src, alt, caption, placement } = HTMLAttributes;
return [
"figure",
mergeAttributes({ "data-figure": "", "data-placement": placement }),
["img", { src, alt }],
["figcaption", {}, caption ?? ""],
];
},
addNodeView() {
return ReactNodeViewRenderer(FigureView);
},
});