Skip to content

Paged-view production parity: margins, chrome UX, page numbers, first-page, footnotes, PlateStatic plugin coverage (variant B β€” overlay store + sub-editors)Β #385

Description

@arthrod

@coderabbitai plan

πŸ₯• multi-assumptions: variant B of 3 parallel framings of the same goal.
Sibling variant(s): #384 and #386. Please compare and tell us which assumption is load-bearing.

My goal

Bring Plate's paged view to production parity with Word/Pages: page-layout state β€” margins, header/footer chrome and content, page-number position, first-page-different rule, and footnote placement (footer or document end) β€” becomes fully user-customizable via UI; clicking into a header or footer presents a friendly chrome shell instead of raw editable text; and the paged-view PlateStatic preview renders the same plugin surface as the live editor (currently AI, comments, suggestions, drag-handle, and other chrome-only or editOnly plugins are silently dropped by getElementOnlyStaticPlugins in packages/pagination/src/react/page-frame.tsx:322-362).

To do so, do this (variant B β€” overlay store + scoped chrome sub-editors)

  1. Introduce pageLayoutStore (Zustand or React context, document-keyed by metadata) holding: margins, pageSize, headerHeight, footerHeight, footnoteWell, footnotePlacement, pageNumber: { region: 'header'|'footer', align, format, startAt, hideOnFirst }, firstPageDifferent: boolean, and IDs for chrome editors.
  2. Header and footer become separate Plate editor instances mounted by PageOverlay instead of nodes in editor.children. Same plugin set as the body editor, scoped to a one-row content. State persists in document metadata, not in editor.children.
  3. PageFrame (packages/pagination/src/react/page-frame.tsx) consumes the store; chrome regions render the live header/footer editors (paged mode) β€” not PlateStatic. The static filter now only matters for the print/PDF path, not for on-screen paged preview.
  4. Selection UX: each chrome region is its own editor, so it gets its own focus state, toolbar, and a chrome shell label ("Header", "Footer", "First-page header"). Body editor visibly defocuses while chrome is active. No selection inside the body ever lands inside chrome β†’ no "ugly text" artifact.
  5. "First page different" β†’ store flag mounts a second header editor + second footer editor; PageFrame swaps which to render based on page.pageIndex === 0.
  6. Page number β†’ independent slot inside chrome regions, positioned by store config; rendered as a non-editable React element, not a Slate void, so users cannot accidentally move/delete it via body selection.
  7. Margin customization β†’ Page Setup dialog mutates the store; PageOverlay re-renders. Same dialog UX as variant A but writes to the store, not editor options.
  8. Footnote placement β†’ store flag drives paginate.ts which path to take; allocator stays the same.
  9. Print/PDF (PlateStatic) path: wraps body + each chrome editor in PlateStatic with the existing static-safe filter. The filter still drops some plugins for print, but on-screen paged editing is no longer affected.

Acceptance: in dev-browser, paged mode shows live, fully-functional header/footer editors (typing in header works without "ugly text"); selecting a chrome region focuses it with a label; first page can have a different header; page-number position is store-driven; Page Setup dialog edits margins/heights/footnote placement; on-screen paged preview renders ALL live editor plugins (AI, comments, suggestions, drag-handle) inside body since it is a real editor, not PlateStatic.

Because the code is

  • packages/pagination/src/react/page-frame.tsx:107-158 β€” chrome regions today render via StaticPageValue (PlateStatic); replacing those with live sub-editors removes the static-filter surface for paged preview entirely.
  • packages/pagination/src/react/page-frame.tsx:322-362 β€” getElementOnlyStaticPlugins is the source of "PlateStatic page viewer doesn't render several plugins". Variant B sidesteps it for paged preview by not using PlateStatic there.
  • packages/pagination/src/lib/transforms/ensureHeader.ts / ensureFooter.ts β€” body-coupled chrome nodes; variant B stops persisting chrome inside editor.children, removing the need for these invariants.
  • packages/pagination/src/lib/types.ts β€” BasePaginationOptions becomes a thin pointer to pageLayoutStore for runtime; backward-compat shim keeps setMargins/setMode etc. as transforms that proxy into the store.
  • packages/pagination/src/react/use-pretext-measurer.ts β€” measurer is read-only over body content; unaffected by chrome sub-editors.
  • packages/pagination/src/lib/paginate.ts β€” needs a small change: skip body-internal header/footer when computing page allocation since they no longer live in the body.

Assumptions deliberately shuffled vs variant A

Axis Variant B Variant A
Where layout state lives external pageLayoutStore + scoped chrome sub-editors editor.options + editor.children (chrome nodes)
Header/footer kind separate Plate editors mounted by PageOverlay top-level Slate nodes inside the body editor
Trigger for layout changes render-time over the body editor edit-time / option-set-time on the body editor
First-page-different store flag swaps which chrome editor mounts on page 1 optional firstPageHeader/firstPageFooter node types
PlateStatic plugin coverage sub-editors render natively in paged mode β†’ ALL plugins work per-plugin staticSafe opt-in extends the static filter
Reversibility easy β€” store can be retired; chrome editors collapse back to body nodes hard β€” schema migration if first-page nodes ship and are later removed
Body-editor cleanliness body has zero chrome nodes body owns chrome nodes
Implementation surface new sub-editor mounts, store, overlay coordination, persistence shim extends existing types + adds ChromeShell + 1 dialog
DOCX round-trip adapter must merge store + chrome editor values adapter reads body chrome nodes + options

Assumptions deliberately shuffled vs variant C

Axis Variant B Variant C
Where chrome lives external store + sub-editor instances ID-keyed chrome registry referenced from Section.sectPr nodes
Granularity document-level per-section
Data model bespoke store shape OOXML-shaped (sectPr, pgNumType, titlePg, footnotePr)
First-page-different store flag titlePg + titlePgHdrRef/titlePgFtrRef
DOCX round-trip adapter on store near-direct mapping
Reversibility easy medium (sections persist)
Section model none β€” single section assumed first-class multi-section

Validate this approach β€” especially but not limited to

  • Static analysis of packages/pagination/src/react/page-frame.tsx, packages/pagination/src/react/page-overlay.tsx, packages/pagination/src/react/standard-frame.tsx, packages/pagination/src/lib/paginate.ts, packages/pagination/src/lib/transforms/, packages/pagination/src/lib/allocate-footnotes.ts.
  • Review of our own code in packages/pagination/src/react/, packages/core/src/static/components/PlateStatic.tsx, apps/www/src/registry/components/editor/plugins/pagination-kit.tsx.
  • Comparative analysis vs sibling Paged-view production parity: margins, chrome UX, page numbers, first-page, footnotes, PlateStatic plugin coverage (variant A β€” schema-first)Β #384 and Paged-view production parity: margins, chrome UX, page numbers, first-page, footnotes, PlateStatic plugin coverage (variant C β€” OOXML section model)Β #386 β€” name the load-bearing assumption as a single sentence in the form "If matters more than , variant <A|B|C> wins; otherwise the other." Do not pick a winner yet β€” name the axis. Specifically: is the load-bearing question "is mounting N+ live Plate editors per document acceptable for performance / shared selection / undo coherence" (β†’ if yes, B; if no, A), and is "do we need DOCX section-level fidelity" (β†’ if yes, C) orthogonal to that?
  • Mounting one extra Plate editor per chrome region (potentially Γ—2 with first-page-different) β€” what's the perf cost, undo-stack story, copy/paste behavior between body and chrome?
  • How do shared concerns (comments, AI suggestions, search) work across body + chrome editors? Is there a multi-editor coordination layer this assumes?
  • Does removing chrome from editor.children break existing DOCX export adapters that read header/footer from those nodes?
  • Persistence: where does pageLayoutStore actually live in user data β€” document JSON metadata, separate field, server-side?

Full plan

  1. Define pageLayoutStore shape + persistence contract (document metadata field).
  2. Build chrome sub-editor mount in PageOverlay; one editor per chrome ID, plugin set inherited from body.
  3. Refactor PageFrame chrome regions to render sub-editors instead of StaticPageValue.
  4. Update paginate.ts to ignore body-internal header/footer and consume store.
  5. Add firstPageDifferent swap logic in PageOverlay based on page.pageIndex.
  6. Wire Page Setup dialog to mutate store; keep editor.tf.pagination.* transforms as proxies for back-compat.
  7. Build chrome shell (label + focus border + exit affordance) around each chrome sub-editor.
  8. Page-number renders as non-editable React element inside chrome region per store config.
  9. Print/PDF path wraps body + chrome editors in PlateStatic; on-screen paged preview no longer touches PlateStatic.
  10. Migrate DOCX adapter to read store + chrome editor values; provide one-time migration from body chrome nodes to store.
  11. dev-browser verification of all seven user-reported issues.

References

  • packages/pagination/src/react/page-frame.tsx
  • packages/pagination/src/react/page-overlay.tsx
  • packages/pagination/src/lib/paginate.ts
  • packages/pagination/src/lib/transforms/ensureHeader.ts
  • packages/core/src/static/components/PlateStatic.tsx
  • branch codex/pagination-variant-a

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions