Skip to content

Plate pagination plugin β€” consolidated plan (variant A, pretext-backed, Word-faithful)Β #380

Description

@arthrod

@coderabbitai plan

πŸ₯• Plate @platejs/pagination β€” consolidated plan (variant A, pretext-backed)

This supersedes #353 / #354 plan churn. New baseline pinned by interactive design review with the maintainer; CR is asked to plan from scratch against this brief, not to extend the older plans.

Goal

Ship a Plate plugin (@platejs/pagination) that delivers print-grade pagination β€” fixed-size pages (A4 default) with per-page headers, footers, footnotes β€” using chenglou/pretext (height oracle, MIT, ESM) for line-break + height math, with editor-faithful Word semantics (<w:sectPr> style metadata, separate header*.xml style H/F parts).

Architecture (locked)

# Topic Decision
1 Standard-mode footer Hybrid: anchored to end of content; pinned to viewport bottom when doc is shorter than viewport
2 Mode + size Single toolbar dropdown: Standard / A4 / Letter / Legal / Custom…. Custom opens dialog.
3 Margins Word-style preset submenu + Custom dialog (4 inputs, unit selector in/cm/mm/px). Paged view also exposes drag handles on margin lines.
4 H/F variation Word 6-slot model. Two toggles: Different first page, Different odd & even. Editable slots: header / header-first / header-even / footer / footer-first / footer-even.
5 Dynamic fields Plate inline-void nodes: pageNumber, pageCount, date. Inserted via "Insert field" menu in H/F editor. Resolved at paint time per page.
6 Footnotes (standard) End-of-document endnotes (renders before the hybrid footer).
6b Footnotes (paged) Per-page footer well via portal (existing variant-A spec).
7 Persistence Sibling node. Single section-properties void at end of editor.children carrying { pageSize, margins, differentFirstPage, differentOddEven }. H/F content as separate top-level sibling nodes. Body = everything else; paginate() filters non-body siblings. Word-faithful: <w:sectPr> end-of-body + separate header*.xml.
8 Font for pretext Per-block DOM scrape: editor.api.toDOMNode(block) β†’ getComputedStyle(el).font. system-ui β†’ Inter snap client-side. Cache key (node.id, marksFingerprint, font, width), rAF-coalesced.
9 Manual page break Cmd/Ctrl+Enter + /page break slash menu. Inserts { type: 'page-break' } void. Labeled rule in standard mode, hard split in paged.
10 UI distribution Plugin = base + react in @platejs/pagination. Toolbar dropdown + dialogs ship as a registry kit (Plate convention), not built into the package.
11 Print path Paged mode renders real <section class="page"> DOM (the <PrintPagination> component) for both on-screen-paged and @media print. No overlay path on screen. One render for both.

Word's data model β€” what we're matching

<w:body>
  <w:p>…paragraph…</w:p>          ← body content
  …
  <w:sectPr>                       ← section properties (NOT a page node)
    <w:pgSz w:w=… w:h=…/>          ← page size
    <w:pgMar …/>                   ← margins
    <w:hdrRef w:type="default|first|even" r:id=…/>
    <w:ftrRef w:type="default|first|even" r:id=…/>
    <w:titlePg/>                   ← differentFirstPage
  </w:sectPr>
</w:body>
+ separate header1.xml, footer1.xml, … parts (referenced by id)

Pages are never in DOCX XML β€” they're a layout-engine output. Variant A's "no page nodes ever in editor.children" matches Word exactly. section-properties and H/F siblings are metadata + separate parts, not pages.

Editor.children shape

editor.children = [
  // body content (any normal Plate blocks):
  { type: 'p', children: [...] },
  { type: 'h1', children: [...] },
  { type: 'page-break' },           // manual hard break (void)
  ...

  // β€” siblings, filtered out of paginate() body input β€”
  { type: 'header',         children: [...] },                    // default
  { type: 'header-first',   children: [...] },                    // optional
  { type: 'header-even',    children: [...] },                    // optional
  { type: 'footer',         children: [...] },
  { type: 'footer-first',   children: [...] },
  { type: 'footer-even',    children: [...] },
  { type: 'footnoteDefinition', children: [...] }, ...            // existing plugin
  { type: 'section-properties',                                   // void
    pageSize: 'A4', margins: { top, right, bottom, left },
    differentFirstPage: true, differentOddEven: false },
]

What changes on codex/pagination-variant-a (it claims pretext, ships none)

Critical fixes:

  1. Add pretext to packages/pagination/package.json dependencies.
  2. Rewrite packages/pagination/src/react/use-pretext-measurer.ts β€” replace canvas+char-width estimator with real pretext.prepare() per (node.id, marksFingerprint, font, width) and pretext.layout(width) per cycle. Use prepareRichInline for blocks with mixed marks.
  3. packages/pagination/src/react/internal/use-page-layout.ts:47 β€” replace ctx: { font: '', marksFingerprint: '', width } with per-block scraped values; build marksFingerprint per block via existing internal/marks-fingerprint.ts.
  4. Wrap recompute in a requestAnimationFrame coalescer.
  5. system-ui β†’ Inter snap inside the per-block scrape.

New surface (not yet on the branch):
6. BaseSectionPropertiesPlugin β€” void node, end of editor.children, carries pageSize/margins/toggles. paginate() skips it.
7. BaseHeaderPlugin / BaseFooterPlugin β€” variant: 'default' | 'first' | 'even'. Six possible top-level sibling nodes.
8. BasePageNumberPlugin, BasePageCountPlugin, BaseDatePlugin β€” inline-void plugins, render value from page context at paint time.
9. BasePageBreakPlugin β€” Cmd/Ctrl+Enter keybinding + slash registration.
10. <PrintPagination> (paged on-screen + print) β€” real <section class="page"> per page + @media print { .page { page-break-after: always } }. Single render path.
11. <StandardLayout> (standard mode) β€” IntersectionObserver-driven hybrid footer; renders endnotes between last block and the hybrid footer.
12. <MarginDragHandles> β€” overlay in paged view, updates section-properties options.
13. <PaginationDropdown> β€” toolbar component owning mode + size + margins UX (presets, Custom dialog, two H/F toggles). Ships in registry kit, not the npm package.

Deferred to v2

  • Per-section variation (multiple <w:sectPr> in one doc).
  • Mirrored margins / gutter / binding offset.
  • DOCX import/export adapter (data shapes are designed to round-trip; adapter is a separate ticket).
  • Yjs deterministic acceptance test (sibling-node persistence makes Yjs work for free; just needs an explicit test).

Acceptance

  • 500-block doc edits at β‰₯55fps with pagination on (rAF-coalesced pretext layout).
  • Resizing the viewport repaginates within one frame.
  • Print preview matches paged-on-screen page boundaries within Β±1 line.
  • Yjs co-edit produces identical pagination across clients (sibling-node persistence β‡’ deterministic, unlike the original variant-A "per-viewport" framing β€” see the architecture-locked persistence row).

References


@coderabbitai please plan from scratch against this brief (don't extend #353 / #354 plans). Implementation is starting in parallel β€” we're not waiting on the plan, but a fresh plan is welcome for cross-check.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions