Skip to content

Paged-view production parity: hybrid synthesis + body-opacity-dim on chrome focus (variant D)Β #388

Description

@arthrod

@coderabbitai plan

πŸ₯• multi-assumptions: variant D (hybrid synthesis) of 4 parallel framings of the same goal.
Sibling variant(s): #384, #385, #386. This variant cherry-picks across A/B/C and adds one new piece β€” body-opacity dim on chrome focus β€” that none of the prior framings covered.

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 D β€” hybrid synthesis with body-opacity-dim UX)

This variant is B's spine (overlay store + sub-editors) with grafts from A (transform proxies, comprehensive Page Setup dialog) and C (titlePg flag instead of dedicated node types, HeaderFooterReference[]-shaped chrome IDs, four-mode footnote placement, page-number fmt), plus one novel piece:

The opacity-dim UX (new in D): when any chrome editor (header / footer / first-page-header / first-page-footer) has focus, the body editor visually dims to ~50% opacity with a 200ms transition. This is how Word and Pages signal "you are editing chrome, not body" β€” and it is the actual user fix for "header and footer when selected creates an ugly text". The chrome shell label and focus ring (B/A) are not enough on their own; the body must visually recede.

  1. State (B + C grafted): introduce pageLayoutStore (Zustand or context) with B's shape β€” margins, pageSize, headerHeight, footerHeight, footnoteWell, pageNumber, chrome editor IDs β€” but model the chrome IDs as C's HeaderFooterReference[] with type: 'default' | 'first' | 'even' (only 'default' and 'first' used in v1). Persist to a top-level pageLayout field in document metadata.
  2. First-page-different (C grafted): titlePg: boolean flag in the store. When pageIndex === 0 and titlePg === true, PageFrame resolves chrome via headerReferences.find(r => r.type === 'first') instead of 'default', falling back to default. No new node types β€” A's firstPageHeader/firstPageFooter types are discarded.
  3. Footnote placement (C grafted): widen FootnotePlacement to 'pageBottom' | 'beneathText' | 'sectEnd' | 'docEnd' (current 'footer' aliases to 'pageBottom', current 'documentEnd' aliases to 'docEnd'). v1 implements pageBottom and docEnd; beneathText and sectEnd are accepted by the type but warn-and-fall-back-to-pageBottom until phase 2.
  4. Page-number config (C grafted): PageNumberConfig carries region, align, format: 'decimal' | 'roman' | 'letter' | '1/N' | 'page-of-n', startAt, hideOnFirst. <PageNumber> is a non-editable React element rendered inside chrome regions, positioned via store config β€” never a Slate void, so body selection cannot delete or move it.
  5. Chrome sub-editors (B): header / footer / first-page-header / first-page-footer become separate Plate editor instances mounted by PageOverlay, sharing the body editor's plugin list. Single-line content constraint via schema. Cross-editor focus tracked by useChromeFocusContext (new).
  6. Body-opacity-dim (D β€” new): ChromeFocusContext exposes activeChromeId: string | null. PageOverlay wraps body editor in a div whose data-chrome-active attribute toggles when activeChromeId is set; CSS uses that attribute selector to apply opacity: 0.5 + transition: opacity 200ms ease-out to the body. Optional chromeFocusDimsBody flag in store (default true) for users who want to disable.
  7. PlateStatic gap fix (B): chrome regions in paged preview render through live sub-editors, not PlateStatic. The static-safe filter in getElementOnlyStaticPlugins (page-frame.tsx:322-362) only matters for the print/PDF path, not on-screen paged preview. AI / comments / suggestions / drag-handle "just work" inside chrome (since they are real editors).
  8. Page Setup dialog (A grafted): promote MarginsDialog to a full PageSetupDialog covering margins (per-axis numeric + cm/in/mm/px), page-size presets, chrome heights, page-number config, first-page-different toggle, footnote placement (4-mode dropdown), and chromeFocusDimsBody toggle. Wire to store via existing editor.tf.pagination.* transforms β€” keep them as proxies for back-compat (A's pattern).
  9. Auto-migration (C grafted): when an existing document with body-level header/footer nodes loads, a normalizer extracts them into the chrome registry on first edit; body header/footer nodes are removed. Single-shot, idempotent. Existing single-doc-header documents become single-section-no-titlePg documents.
  10. DOCX round-trip (B + C path): importer maps DOCX sectPr.titlePg β†’ store.titlePg, headerReference[type=first] β†’ first-page chrome ID, pgNumType β†’ store.pageNumber, footnotePr.pos β†’ store.footnotePlacement. Single-section assumption baked in for v1; multi-section is a follow-up that does not require a re-architecture (because the chrome reference shape is already C-shaped).

Acceptance: in dev-browser, paged mode shows live, fully-functional header/footer editors (typing in header works without "ugly text"); clicking a chrome region focuses it AND dims the body editor to 50% opacity with a smooth 200ms transition; first page can have a different header via titlePg; page-number position/format is store-driven (decimal vs roman vs Page 1 of N); Page Setup dialog edits margins/heights/footnote placement (with 4 modes selectable in UI even if beneathText/sectEnd warn-fallback); 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

Mix-and-match analysis (what came from where)

Concern A says B says C says D picks
Where layout state lives editor.options + editor.children chrome nodes external pageLayoutStore + chrome sub-editors chromeRegistry + Section.sectPr B (store + sub-editors) β€” but with C's reference shape
First-page rule dedicated firstPageHeader/firstPageFooter node types store flag swaps editors titlePg + titlePgHdrRef/titlePgFtrRef C (titlePg flag) β€” cleaner, opens door to multi-section later
Page-number formats '1' | '1/N' | 'Page 1 of N' not specified beyond config shape 'decimal' | 'roman' | 'letter' from pgNumType C (decimal/roman/letter) + A's vocabulary kept as aliases
Footnote placement modes 'perPage' | 'documentEnd' placement flag, modes not specified 4 modes: pageBottom | beneathText | sectEnd | docEnd C (4 modes); v1 implements 2, others warn-fallback
PlateStatic plugin coverage per-plugin staticSafe opt-in sub-editors render natively in paged mode per-section chrome editors mount with full plugin set B β€” A's staticSafe was rejected as unworkable since chrome-only plugins need live state
Page Setup dialog "create comprehensive from start" not specified not specified A (one comprehensive dialog) β€” avoids partial iterations
Persistence options + chrome nodes in editor.children document metadata field separate top-level property B's pageLayout metadata field β€” same shape as C's separate property, simpler name
Migration schema-additive not specified explicitly auto-migration in normalizer C (auto-migration) β€” applied to B's store, not C's section model
Body dim on chrome focus not covered not covered not covered D's novel piece β€” none of A/B/C addressed the visual recession that the user explicitly called out

Assumptions deliberately shuffled vs variant B

Axis Variant D Variant B
Chrome reference shape C-style HeaderFooterReference[] with type: 'default' | 'first' | 'even' flat headerEditorId / footerEditorId strings + separate firstPageHeaderEditorId / firstPageFooterEditorId
First-page rule titlePg flag (single source of truth, OOXML-aligned) firstPageDifferent flag + 4 separate ID fields
Page-number formats 'decimal' | 'roman' | 'letter' | '1/N' | 'page-of-n' not specified beyond format: 'decimal' | 'roman' | 'letter' (similar but D adds 1/N and page-of-n aliases for back-compat)
Body recede UX explicit opacity-dim with 200ms transition + opt-out flag not addressed
Footnote modes 4 (pageBottom/beneathText/sectEnd/docEnd) with v1 partial implementation 'page' | 'documentEnd' (2)
Auto-migration yes β€” extract body chrome nodes into store on first load not specified
Future multi-section path reference-based shape can extend to multi-section without API break flat IDs need restructuring to support sections

Assumptions deliberately shuffled vs variant C

Axis Variant D Variant C
Section abstraction none (single-section) first-class Section nodes carrying sectPr
Granularity document-level per-section
Implementation cost medium (B's spine + selective C grafts) high (full OOXML alignment + per-section pagination)
DOCX round-trip adapter maps store ↔ sectPr (single section) near-direct
Page-number pgNumType borrowed format/start fields full pgNumType per section
Reversibility medium (store + chrome editors) medium (sections persist)
Future-proofing reference-shape extensible to per-section later already there

Validate this approach β€” especially but not limited to

  • Static analysis of packages/pagination/src/lib/types.ts, packages/pagination/src/react/page-frame.tsx, packages/pagination/src/react/page-overlay.tsx, packages/pagination/src/react/margins-dialog.tsx, packages/pagination/src/lib/paginate.ts, packages/pagination/src/lib/allocate-footnotes.ts, packages/pagination/src/lib/transforms/.
  • Review of our own code in packages/pagination/src/, packages/core/src/static/components/PlateStatic.tsx, apps/www/src/registry/components/editor/plugins/pagination-kit.tsx.
  • Comparative analysis vs siblings Paged-view production parity: margins, chrome UX, page numbers, first-page, footnotes, PlateStatic plugin coverage (variant A β€” schema-first)Β #384, Paged-view production parity: margins, chrome UX, page numbers, first-page, footnotes, PlateStatic plugin coverage (variant B β€” overlay store + sub-editors)Β #385, 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 for variant D vs each: e.g. "If the body must remain a real editor in paged preview (sub-editor cost is tolerable), variant D wins over A; otherwise A wins." "If we don't need multi-section in the next 12 months, variant D wins over C; if we do, C wins." Don't pick a winner yet β€” name the axis per pair.
  • Body-dim correctness questions β€” does dimming the body editor to 50% interfere with focus rings of decoration plugins (cursor, comments, AI suggestions) that render inside it? Should the dim use opacity (affects child visibility) or a CSS filter: brightness(0.6) (cheaper for some browsers)? Is there an a11y concern (low-contrast text during chrome editing) we should opt-out of for users who set prefers-reduced-motion or have contrast preferences?
  • Auto-migration safety β€” for existing documents with body-level header/footer nodes, when does the normalizer fire (mount? first edit?) and is it idempotent across multiple loads on the same client? What happens if two collaborators load an unmigrated doc simultaneously and migrate independently?
  • Page-number numbering with titlePg β€” when first-page-different is on AND pageNumber.hideOnFirst === false, does the first page show "1" or skip to "2"? Word's behavior: shows 1 on title page if hideOnFirst is false. Confirm we match.
  • beneathText vs sectEnd warn-fallback semantics β€” exactly what should we do when a user picks these in v1? Silent fallback to pageBottom? Loud console warning? UI toast?
  • Single-line constraint on chrome editors β€” soft (visual clipping) or hard (schema rejects multi-block paste)? DOCX headers can have multi-line content; hard rejection breaks DOCX round-trip.

Full plan

  1. Type extensions in types.ts: widen FootnotePlacement to 4 modes (with backward-compat aliases), add PageNumberConfig, HeaderFooterReference, titlePg, chromeFocusDimsBody, pageNumber on BasePaginationOptions.
  2. Define pageLayoutStore (Zustand) shape + PageLayoutProvider + persistence to pageLayout document metadata field.
  3. Build chrome sub-editor infrastructure: createChromeEditor, ChromeEditorMount, schema constraint for single-line.
  4. useChromeFocusContext + body-opacity-dim wiring (CSS attribute selector, 200ms transition, respects prefers-reduced-motion).
  5. ChromeShell component (label + focus ring + exit affordance + dim trigger).
  6. PageNumber non-editable component reading store config; render in chrome regions.
  7. Refactor PageFrame chrome regions to mount sub-editors instead of StaticPageValue; consume store for chrome heights / margins / pageNumber.
  8. titlePg swap logic in PageFrame for pageIndex === 0 based on store + reference array.
  9. Promote MarginsDialog β†’ PageSetupDialog covering all options, wired through editor.tf.pagination.* transforms (kept as proxies into the store).
  10. Widen allocate-footnotes.ts to 4 modes: pageBottom and docEnd work; beneathText/sectEnd warn-fallback to pageBottom with TODO.
  11. Auto-migration normalizer: extract body header/footer nodes β†’ chromeRegistry β†’ remove from editor.children.
  12. Update DOCX I/O adapter (if packages/docx-io* exists) to map titlePg, headerReference[type=first], pgNumType, footnotePr.pos.
  13. dev-browser verification of all 7 user-reported issues + the body-dim transition.

References

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