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 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.
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.
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.
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.
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.
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).
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.
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).
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).
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.
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
packages/pagination/src/lib/types.ts β BasePaginationOptions already has margins, mode, footnotePlacement (only 2 modes), headerHeight, footerHeight, footnoteWell. Variant D widens FootnotePlacement, adds pageNumber, firstPageDifferent/titlePg, chromeFocusDimsBody, and a chromeReferences shape. All additive.
packages/pagination/src/react/page-frame.tsx:107-158 β chrome regions today render via StaticPageValue (PlateStatic). Replacing with live sub-editors removes the static-filter surface for paged preview entirely (variant B's fix).
packages/pagination/src/react/page-frame.tsx:156 β <span style={{ float: 'right' }}>{pageIndex + 1}</span> is the entire page-number implementation. Hardcoded.
packages/pagination/src/react/page-frame.tsx:322-362 β getElementOnlyStaticPlugins is the source of "PlateStatic page viewer doesn't render several plugins". Variant D matches B's framing: only the print/PDF path needs this filter.
packages/pagination/src/react/margins-dialog.tsx β minimal v1 native <dialog>; only edits the four margin sides. Variant D promotes it to comprehensive PageSetupDialog.
packages/pagination/src/lib/transforms/ensureHeader.ts / ensureFooter.ts β body-coupled chrome nodes; variant D's auto-migration turns them into chromeRegistry entries on first load.
packages/pagination/src/lib/allocate-footnotes.ts β currently switches on 'footer' | 'documentEnd'; variant D widens to 4 modes (with backward-compat aliases for the two existing strings).
packages/pagination/src/lib/base-pagination-plugin.ts β defines transforms; variant D adds setPageNumber, setFirstPageDifferent, setChromeFocusDimsBody, and proxies them through to the store.
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.
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
Type extensions in types.ts: widen FootnotePlacement to 4 modes (with backward-compat aliases), add PageNumberConfig, HeaderFooterReference, titlePg, chromeFocusDimsBody, pageNumber on BasePaginationOptions.
@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
editOnlyplugins are silently dropped bygetElementOnlyStaticPluginsinpackages/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 (
titlePgflag instead of dedicated node types,HeaderFooterReference[]-shaped chrome IDs, four-mode footnote placement, page-numberfmt), 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.
pageLayoutStore(Zustand or context) with B's shape βmargins,pageSize,headerHeight,footerHeight,footnoteWell,pageNumber, chrome editor IDs β but model the chrome IDs as C'sHeaderFooterReference[]withtype: 'default' | 'first' | 'even'(only'default'and'first'used in v1). Persist to a top-levelpageLayoutfield in document metadata.titlePg: booleanflag in the store. WhenpageIndex === 0andtitlePg === true,PageFrameresolves chrome viaheaderReferences.find(r => r.type === 'first')instead of'default', falling back to default. No new node types β A'sfirstPageHeader/firstPageFootertypes are discarded.FootnotePlacementto'pageBottom' | 'beneathText' | 'sectEnd' | 'docEnd'(current'footer'aliases to'pageBottom', current'documentEnd'aliases to'docEnd'). v1 implementspageBottomanddocEnd;beneathTextandsectEndare accepted by the type but warn-and-fall-back-to-pageBottomuntil phase 2.PageNumberConfigcarriesregion,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.PageOverlay, sharing the body editor's plugin list. Single-line content constraint via schema. Cross-editor focus tracked byuseChromeFocusContext(new).ChromeFocusContextexposesactiveChromeId: string | null.PageOverlaywraps body editor in a div whosedata-chrome-activeattribute toggles when activeChromeId is set; CSS uses that attribute selector to applyopacity: 0.5+transition: opacity 200ms ease-outto the body. OptionalchromeFocusDimsBodyflag in store (defaulttrue) for users who want to disable.PlateStatic. The static-safe filter ingetElementOnlyStaticPlugins(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).MarginsDialogto a fullPageSetupDialogcovering 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 existingeditor.tf.pagination.*transforms β keep them as proxies for back-compat (A's pattern).header/footernodes 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.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 viatitlePg; page-number position/format is store-driven (decimal vs roman vsPage 1 of N); Page Setup dialog edits margins/heights/footnote placement (with 4 modes selectable in UI even ifbeneathText/sectEndwarn-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
packages/pagination/src/lib/types.tsβBasePaginationOptionsalready hasmargins,mode,footnotePlacement(only 2 modes),headerHeight,footerHeight,footnoteWell. Variant D widensFootnotePlacement, addspageNumber,firstPageDifferent/titlePg,chromeFocusDimsBody, and achromeReferencesshape. All additive.packages/pagination/src/react/page-frame.tsx:107-158β chrome regions today render viaStaticPageValue(PlateStatic). Replacing with live sub-editors removes the static-filter surface for paged preview entirely (variant B's fix).packages/pagination/src/react/page-frame.tsx:156β<span style={{ float: 'right' }}>{pageIndex + 1}</span>is the entire page-number implementation. Hardcoded.packages/pagination/src/react/page-frame.tsx:322-362βgetElementOnlyStaticPluginsis the source of "PlateStatic page viewer doesn't render several plugins". Variant D matches B's framing: only the print/PDF path needs this filter.packages/pagination/src/react/margins-dialog.tsxβ minimal v1 native<dialog>; only edits the four margin sides. Variant D promotes it to comprehensivePageSetupDialog.packages/pagination/src/lib/transforms/ensureHeader.ts/ensureFooter.tsβ body-coupled chrome nodes; variant D's auto-migration turns them into chromeRegistry entries on first load.packages/pagination/src/lib/allocate-footnotes.tsβ currently switches on'footer' | 'documentEnd'; variant D widens to 4 modes (with backward-compat aliases for the two existing strings).packages/pagination/src/lib/base-pagination-plugin.tsβ defines transforms; variant D addssetPageNumber,setFirstPageDifferent,setChromeFocusDimsBody, and proxies them through to the store.@coderabbitai plan. Variant D explicitly synthesizes across them per the Mix-and-match analysis below.Mix-and-match analysis (what came from where)
editor.options+editor.childrenchrome nodespageLayoutStore+ chrome sub-editorschromeRegistry+Section.sectPrfirstPageHeader/firstPageFooternode typestitlePg+titlePgHdrRef/titlePgFtrReftitlePgflag) β cleaner, opens door to multi-section later'1' | '1/N' | 'Page 1 of N''decimal' | 'roman' | 'letter'frompgNumType'perPage' | 'documentEnd'pageBottom | beneathText | sectEnd | docEndstaticSafeopt-instaticSafewas rejected as unworkable since chrome-only plugins need live stateeditor.childrenpageLayoutmetadata field β same shape as C's separate property, simpler nameAssumptions deliberately shuffled vs variant B
HeaderFooterReference[]withtype: 'default' | 'first' | 'even'headerEditorId/footerEditorIdstrings + separatefirstPageHeaderEditorId/firstPageFooterEditorIdtitlePgflag (single source of truth, OOXML-aligned)firstPageDifferentflag + 4 separate ID fields'decimal' | 'roman' | 'letter' | '1/N' | 'page-of-n'format: 'decimal' | 'roman' | 'letter'(similar but D adds1/Nandpage-of-naliases for back-compat)pageBottom/beneathText/sectEnd/docEnd) with v1 partial implementation'page' | 'documentEnd'(2)Assumptions deliberately shuffled vs variant C
Sectionnodes carryingsectPrpgNumTypepgNumTypeper sectionValidate this approach β especially but not limited to
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/.packages/pagination/src/,packages/core/src/static/components/PlateStatic.tsx,apps/www/src/registry/components/editor/plugins/pagination-kit.tsx.opacity(affects child visibility) or a CSSfilter: 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 setprefers-reduced-motionor have contrast preferences?titlePgβ when first-page-different is on ANDpageNumber.hideOnFirst === false, does the first page show "1" or skip to "2"? Word's behavior: shows 1 on title page ifhideOnFirstis false. Confirm we match.beneathTextvssectEndwarn-fallback semantics β exactly what should we do when a user picks these in v1? Silent fallback topageBottom? Loud console warning? UI toast?Full plan
types.ts: widenFootnotePlacementto 4 modes (with backward-compat aliases), addPageNumberConfig,HeaderFooterReference,titlePg,chromeFocusDimsBody,pageNumberonBasePaginationOptions.pageLayoutStore(Zustand) shape +PageLayoutProvider+ persistence topageLayoutdocument metadata field.createChromeEditor,ChromeEditorMount, schema constraint for single-line.useChromeFocusContext+ body-opacity-dim wiring (CSS attribute selector, 200ms transition, respectsprefers-reduced-motion).ChromeShellcomponent (label + focus ring + exit affordance + dim trigger).PageNumbernon-editable component reading store config; render in chrome regions.PageFramechrome regions to mount sub-editors instead ofStaticPageValue; consume store for chrome heights / margins / pageNumber.titlePgswap logic inPageFrameforpageIndex === 0based on store + reference array.MarginsDialogβPageSetupDialogcovering all options, wired througheditor.tf.pagination.*transforms (kept as proxies into the store).allocate-footnotes.tsto 4 modes:pageBottomanddocEndwork;beneathText/sectEndwarn-fallback topageBottomwith TODO.editor.children.packages/docx-io*exists) to maptitlePg,headerReference[type=first],pgNumType,footnotePr.pos.dev-browserverification of all 7 user-reported issues + the body-dim transition.References
packages/pagination/src/codex/pagination-variant-a