You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
🥕 multi-assumptions: variant A of 2 parallel framings of the same goal.
Sibling variant: #354. Please compare and tell us which assumption is load-bearing.
Carrots are crunchier when the layer is right.
My goal
Ship a Plate plugin (@platejs/pagination) that adds print-grade pagination — fixed-size pages (A4 by default) with per-page headers, footers, and footnotes — leveraging chenglou/pretext (text measurement / height oracle, MIT, ESM, v0.0.6) for the line-break and height math that Plate currently lacks.
To do so, do this (variant A — render-time overlay; pages as derived view)
Add BasePaginationPlugin (createTSlatePlugin, key: KEYS.pagination) under packages/pagination/src/lib. Pure options: pageSize, margins, headerHeight, footerHeight, footnoteWell. No page nodes ever enter editor.children.
Add BaseHeaderPlugin, BaseFooterPlugin, BasePageBreakPlugin (manual hard-break escape hatch) as sibling base plugins. Compose with the existing packages/footnote/ (BaseFootnoteDefinitionPlugin + Reference + Input) — do NOT re-author footnotes.
Author paginate(doc, ctx, measurer) -> Page[] as a pure Slate-only selector in src/lib/paginate.ts. measurer: (text, font, width) => number is injected — lib/ stays React-free and unit-testable with a fake monospace measurer.
Lift to React via toTPlatePlugin(BasePaginationPlugin, { plugins: [...], render: { aboveSlate: PageOverlay } }). PageOverlay paints absolutely-positioned PageFrames (header / footer / footnote well) above the normal Editable. Footnote definitions are hidden in main flow and portaled into the footer well of the page that contains their footnote_ref.
Real-time measurer = a React hook that calls pretext.prepare() once per (node.id, marks-fingerprint, font, width) and pretext.layout() per pagination cycle, rAF-coalesced. Use prepareRichInline for mixed-mark runs. Snap to a named font (Inter) when the theme uses system-ui (pretext caveat).
Print mode swaps PageOverlay for real <section class="page"> elements + @media print { .page { page-break-after: always; } } so Chromium honours pretext-derived breaks.
Acceptance: a 500-block document edits at ≥55fps with pagination on; resizing the viewport repaginates within one frame; print preview matches on-screen page boundaries within ±1 line; Yjs co-edit produces visually different paginations per viewport without doc divergence.
Because the code is
packages/core/src/lib/plugin/createSlatePlugin.ts:91-159 — createTSlatePlugin is the right factory; pagination owns explicit options + typed transforms, so the T-variant is correct.
packages/core/src/lib/plugin/SlatePlugin.ts:341-384 — render.aboveSlate / render.belowRootNodes are the slot mechanism for a chrome overlay; no portal infra to invent.
packages/core/src/react/plugin/toPlatePlugin.ts:70-107 — base→Plate lift preserves all extend* chains; canonical "wrap base, child plugins in wrapper" pattern.
packages/footnote/src/lib/BaseFootnoteDefinitionPlugin.ts (and Reference / Input) — existing semantic base plugins; this variant composes them rather than reinventing.
packages/utils/src/lib/plate-keys.ts — KEYS.pagination belongs here, not as a string literal.
.claude/skills/plate-plugin-creator/SKILL.md — Slate-first hard law: pages are derived presentation, not document state, so the base plugin stays React-free.
Google docs - like pagination udecode/plate#4380 — zbeyens (maintainer, 2025-07-02): "rewriting slate-react to support virtual pages without container nodes in the document structure" — explicitly his preferred direction. Same thread: closed-source predecessor was bottlenecked by container-node normalization on every keystroke.
pretext README (commit bb224e0, 2026-04-22) — pretext does NOT paginate; it is a height oracle. This variant treats it as such.
derived per-client view; pagination diverges per viewport, doc converges
deterministic shared pagination, viewport-locked
DOCX round-trip
flat doc + section meta on top-level header/footer siblings
direct mapping to w:sectPr + w:hdrRef / w:ftrRef
Reversibility
feature-flag toggle; turning pagination off is a no-op
schema migration in both directions
editor.children shape
unchanged from non-paginated docs
top-level becomes section[]
Failure mode
over-budget repagination = visible jank, doc still correct
over-budget normalization = stale layout, doc structurally correct
Risk
pretext perf is the load-bearing primitive
break-maintenance transforms on every edit are the load-bearing primitive
Validate this approach — especially but not limited to
Static analysis of packages/core/src/lib/plugin/SlatePlugin.ts — confirm render.aboveSlate survives read-only mode, plays with selection coords, and does not break Editable event delegation.
Review of our own code in packages/footnote/, packages/comment/, packages/core/src/internal/plugin/pluginInjectNodeProps.ts:25-122 — check that the portal+overlay pattern this variant proposes is not already invalidated by an existing convention.
Comparative analysis vs sibling Plate pagination plugin (headers / footers / footnotes) with pretext (variant B) #354 — name the load-bearing assumption as a single sentence in the form "If <X> matters more than <Y>, variant <A|B> wins; otherwise the other." Specifically: which matters more, liquid editing UX + per-keystroke perf (favours A) or deterministic paged structure + DOCX section fidelity (favours B)? Do not pick a winner yet — name the axis.
pretext SSR posture — confirm we can hydrate without re-running prepare() on the server.
Yjs implications of derived pagination — verify two clients with different viewport widths don't fight on editor.children.
Full plan
New package packages/pagination/ (tsdown, bun test); add to root pnpm-workspace. Run pnpm brl after file scaffolding lands.
Add KEYS.pagination, KEYS.header, KEYS.footer, KEYS.pageBreak to packages/utils/src/lib/plate-keys.ts. Footnote keys reuse what packages/footnote/ already exposes.
src/lib/: base-pagination-plugin.ts, base-header-plugin.ts, base-footer-plugin.ts, base-page-break-plugin.ts, paginate.ts, allocate-footnotes.ts, internal/font-from-style.ts, internal/measure-cache.ts, types.ts. Helpers (matchers, fallbacks, fingerprint hashing) live in internal/ per repo barrel rules.
Compose with existing packages/footnote/ plugins — no re-author. PaginationPlugin = toTPlatePlugin(BasePaginationPlugin, { plugins: [HeaderPlugin, FooterPlugin, PageBreakPlugin, FootnoteDefinitionPlugin, FootnoteReferencePlugin, FootnoteInputPlugin] }).
Public surface: editor.api.pagination.{getPages, getPageOf, getFootnotes, measure}; editor.tf.pagination.{insertFootnote, setHeader, setFooter, insertPageBreak, recomputePages}. No ({ editor }: { editor: SlateEditor }) annotations — inference from createTSlatePlugin<Config>.
Print path: alternative PrintPagination component injecting real <section> elements + @media print rules; same paginate() selector.
Tests in packages/pagination/src/**/*.spec.ts with bun: pure paginator with fake measurer (golden cases), allocator, schema round-trip; React tests only for overlay positioning.
DOCX import/export: round-trip headers/footers via w:hdrRef/w:ftrRef; footnotes through existing packages/footnote/ adapters.
Changeset under .changeset/ per .agents/rules/changeset.mdc before completing.
@coderabbitai plan
🥕 multi-assumptions: variant A of 2 parallel framings of the same goal.
Sibling variant: #354. Please compare and tell us which assumption is load-bearing.
Carrots are crunchier when the layer is right.
My goal
Ship a Plate plugin (
@platejs/pagination) that adds print-grade pagination — fixed-size pages (A4 by default) with per-page headers, footers, and footnotes — leveragingchenglou/pretext(text measurement / height oracle, MIT, ESM, v0.0.6) for the line-break and height math that Plate currently lacks.To do so, do this (variant A — render-time overlay; pages as derived view)
BasePaginationPlugin(createTSlatePlugin,key: KEYS.pagination) underpackages/pagination/src/lib. Pure options:pageSize,margins,headerHeight,footerHeight,footnoteWell. Nopagenodes ever entereditor.children.BaseHeaderPlugin,BaseFooterPlugin,BasePageBreakPlugin(manual hard-break escape hatch) as sibling base plugins. Compose with the existingpackages/footnote/(BaseFootnoteDefinitionPlugin+ Reference + Input) — do NOT re-author footnotes.paginate(doc, ctx, measurer) -> Page[]as a pure Slate-only selector insrc/lib/paginate.ts.measurer: (text, font, width) => numberis injected —lib/stays React-free and unit-testable with a fake monospace measurer.toTPlatePlugin(BasePaginationPlugin, { plugins: [...], render: { aboveSlate: PageOverlay } }).PageOverlaypaints absolutely-positionedPageFrames (header / footer / footnote well) above the normalEditable. Footnote definitions are hidden in main flow and portaled into the footer well of the page that contains theirfootnote_ref.pretext.prepare()once per(node.id, marks-fingerprint, font, width)andpretext.layout()per pagination cycle, rAF-coalesced. UseprepareRichInlinefor mixed-mark runs. Snap to a named font (Inter) when the theme usessystem-ui(pretext caveat).PageOverlayfor real<section class="page">elements +@media print { .page { page-break-after: always; } }so Chromium honours pretext-derived breaks.Acceptance: a 500-block document edits at ≥55fps with pagination on; resizing the viewport repaginates within one frame; print preview matches on-screen page boundaries within ±1 line; Yjs co-edit produces visually different paginations per viewport without doc divergence.
Because the code is
packages/core/src/lib/plugin/createSlatePlugin.ts:91-159—createTSlatePluginis the right factory; pagination owns explicit options + typed transforms, so the T-variant is correct.packages/core/src/lib/plugin/SlatePlugin.ts:341-384—render.aboveSlate/render.belowRootNodesare the slot mechanism for a chrome overlay; no portal infra to invent.packages/core/src/react/plugin/toPlatePlugin.ts:70-107— base→Plate lift preserves allextend*chains; canonical "wrap base, child plugins in wrapper" pattern.packages/footnote/src/lib/BaseFootnoteDefinitionPlugin.ts(and Reference / Input) — existing semantic base plugins; this variant composes them rather than reinventing.packages/utils/src/lib/plate-keys.ts—KEYS.paginationbelongs here, not as a string literal..claude/skills/plate-plugin-creator/SKILL.md— Slate-first hard law: pages are derived presentation, not document state, so the base plugin stays React-free.zbeyens(maintainer, 2025-07-02): "rewriting slate-react to support virtual pages without container nodes in the document structure" — explicitly his preferred direction. Same thread: closed-source predecessor was bottlenecked by container-node normalization on every keystroke.bb224e0, 2026-04-22) — pretext does NOT paginate; it is a height oracle. This variant treats it as such.Assumptions deliberately shuffled vs variant B
section/pagenodesheader/footersiblingsw:sectPr+w:hdrRef/w:ftrRefeditor.childrenshapesection[]Validate this approach — especially but not limited to
packages/core/src/lib/plugin/SlatePlugin.ts— confirmrender.aboveSlatesurvives read-only mode, plays with selection coords, and does not breakEditableevent delegation.packages/footnote/,packages/comment/,packages/core/src/internal/plugin/pluginInjectNodeProps.ts:25-122— check that the portal+overlay pattern this variant proposes is not already invalidated by an existing convention.<X>matters more than<Y>, variant<A|B>wins; otherwise the other." Specifically: which matters more, liquid editing UX + per-keystroke perf (favours A) or deterministic paged structure + DOCX section fidelity (favours B)? Do not pick a winner yet — name the axis.prepare()on the server.editor.children.Full plan
packages/pagination/(tsdown,bun test); add to rootpnpm-workspace. Runpnpm brlafter file scaffolding lands.KEYS.pagination,KEYS.header,KEYS.footer,KEYS.pageBreaktopackages/utils/src/lib/plate-keys.ts. Footnote keys reuse whatpackages/footnote/already exposes.src/lib/:base-pagination-plugin.ts,base-header-plugin.ts,base-footer-plugin.ts,base-page-break-plugin.ts,paginate.ts,allocate-footnotes.ts,internal/font-from-style.ts,internal/measure-cache.ts,types.ts. Helpers (matchers, fallbacks, fingerprint hashing) live ininternal/per repo barrel rules.src/react/:pagination-plugin.ts(toTPlatePlugin),header-plugin.ts,footer-plugin.ts,page-break-plugin.ts,page-overlay.tsx,page-frame.tsx,footnote-portal.tsx,use-pretext-measurer.ts,internal/use-page-layout.ts.packages/footnote/plugins — no re-author.PaginationPlugin = toTPlatePlugin(BasePaginationPlugin, { plugins: [HeaderPlugin, FooterPlugin, PageBreakPlugin, FootnoteDefinitionPlugin, FootnoteReferencePlugin, FootnoteInputPlugin] }).editor.api.pagination.{getPages, getPageOf, getFootnotes, measure};editor.tf.pagination.{insertFootnote, setHeader, setFooter, insertPageBreak, recomputePages}. No({ editor }: { editor: SlateEditor })annotations — inference fromcreateTSlatePlugin<Config>.PrintPaginationcomponent injecting real<section>elements +@media printrules; samepaginate()selector.packages/pagination/src/**/*.spec.tswith bun: pure paginator with fake measurer (golden cases), allocator, schema round-trip; React tests only for overlay positioning.w:hdrRef/w:ftrRef; footnotes through existingpackages/footnote/adapters..changeset/per.agents/rules/changeset.mdcbefore completing.References
packages/core/src/lib/plugin/{createSlatePlugin.ts, SlatePlugin.ts, BasePlugin.ts}packages/core/src/react/plugin/toPlatePlugin.tspackages/footnote/src/lib/BaseFootnote*Plugin.ts.claude/skills/plate-plugin-creator/SKILL.mdandrules/{creation-flow,typing,composition}.mdsrc/{layout,line-break,analysis,rich-inline}.tsat commitbb224e0