Skip to content

feat(pagination): chrome (headers/footers/page-numbers/margins) — synthesized stack - #442

Open
arthrod wants to merge 64 commits into
mainfrom
work/pagination-synthesized
Open

feat(pagination): chrome (headers/footers/page-numbers/margins) — synthesized stack#442
arthrod wants to merge 64 commits into
mainfrom
work/pagination-synthesized

Conversation

@arthrod

@arthrod arthrod commented May 29, 2026

Copy link
Copy Markdown
Collaborator

What

Lands the 10-branch pagination synthesis plus a complete chrome implementation (headers / footers / page numbers / margins). Built and live-dogfooded against the playground deploy at https://plate-playground.cicero-im.workers.dev/editor across five iterations of a self-paced /loop.

Commits

SHA Subject
`0a0872e9b` fix: cache topLevelBlockElements in createDomMeasure (PR #436 carry-over)
`6d035687f` fix: land PR #434's 3 remaining inline comments
`848539f95` feat: chrome data layer — types + composer + 7 unit tests
`7e40f9271` feat: chrome React layer + page-number convenience + playground demo
`f235ec770` fix: runtime React import + last-page footer + vendor:pagination syncs node_modules
`df863f9f9` test: 5 chrome edge-cases regression guards + cover-page convention
`69b684495` feat: visible margin rule between content and chrome bands
`ed4d253db` feat: PageNumberMinimal convenience renderer

Chrome contract

```ts
type PageChromeOption = { heightPx: number; render: (ctx: ChromeRenderContext) => unknown };

type PaginationOptions = {
// ... existing ...
chrome?: { header?: PageChromeOption; footer?: PageChromeOption };
};
```

  • PRETEXT-safe: `render` is a PURE function — no DOM access, no editor mutation, no scroll-state reads. Input is `{pageIndex, pageCount, page, margins}`; output is any ReactNode.
  • Document-anchored: the composer emits `PageLayout.chrome.{header,footer}` rects with page-LOCAL coordinates. The React overlay maps each rect to document-Y via the page's first-block top. Scroll-independent by construction.
  • Composer shrinks the content frame by header+footer height before packing fragments, so chrome can never overlap content.
  • Backward compatible: when `chrome` is absent, the existing left-margin "Page N of M" chips render unchanged.

Built-in render presets

Export Output Defaults
`PageNumber` centered "Page N of M" footer, 1-px top rule renders on every page
`PageNumberWithTitle(title)` `title` left, "Page N of M" right, 1-px top rule skips page 1 (cover-page convention)
`PageNumberMinimal({showTotal?, skipFirstPage?})` bare "N" or "N / M", tabular-nums configurable
`TextHeader(text)` uppercase title with 1-px bottom rule renders on every page

Testing

  • bun test: 65 / 65 ✓ (was 53 / 53; +12 new tests).
    • 7 composer chrome tests (`compose-chrome.spec.ts`)
    • 5 edge-case regression guards (`compose-chrome-edge-cases.spec.ts`)
  • Live dogfood across 4 redeploys with a Puppeteer agent: chrome bands render, page numbers update on edit (3 → 6 pages after inserting 80 paragraphs), scroll-anchoring confirmed mathematically (delta = scrollTop exactly), last-page footer fixed (no longer overlaps its header).

Diagnosis: "page numbers follow the screen"

Verified against the live deploy via headless probe: the existing chips and the new chrome bands are document-anchored, not screen-anchored. Page 4 footer at scrollTop=0 → rectTop=2862; at scrollTop=2785 → rectTop=77; delta=2785 (exactly the scroll distance). The original complaint was either (a) a stale-bundle cache the user hit just as the first deploy completed, or (b) a visual interpretation issue with the left-margin chips at the top of each page — now resolved by the chrome footer at the page bottom.

Live link

https://plate-playground.cicero-im.workers.dev/editor — click the page-breaks toggle in the toolbar to see the chrome.

🤖 Generated with Claude Code via /loop

Summary by CodeRabbit

  • New Features

    • Continuous, non‑mutating pagination overlay with page chrome (headers/footers, page numbers), demo page, and a simplified toolbar toggle; plugin now auto‑mounts needed runtime so registering it renders pages.
  • Improvements

    • Line‑accurate text measurement and pretext‑driven sizing for more reliable wrapping across widths.
    • Composer places whole blocks (no mid‑block splits), margin‑aware packing, and improved page↔content mapping.
  • Bug Fixes

    • Measurement cache keys now include width to prevent incorrect cache hits.
  • Documentation

    • Extensive plans, audits, and a work diary added for the pagination rewrite.

Review Change Stack

arthrod and others added 30 commits May 4, 2026 23:57
…lay) (#357)

* feat(pagination): scaffold @platejs/pagination variant A (render-overlay) — refs #353

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(template): add OpenNext deploy + pagination toolbar button placeholder

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pagination): apply inspector + CR feedback — refs #357

- move base bundle (header, footer, page break) to BasePaginationPlugin.plugins (Slate base)
- BasePaginationConfig key uses typeof KEYS.pagination
- drop degenerate <_V> generic on BasePaginationOptions
- add includeFootnoteSubPlugins option (default true) — opt-out for footnote coupling
- usePretextMeasurer uses useState so future ready flip re-renders
- drop @chenglou/pretext from dependencies until measurer lands

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(template): CR feedback — refs #365

- lint script: add explicit `eslint .` target
- lint:fix: chain `eslint . --fix` after biome
- pagination-toolbar-button: drop internal #357/#358 PR refs from user-facing toast

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pagination): implement variant A — paginate, measurer, overlay, footnotes — refs #353 #357

Replace TODO stubs with full variant A:
- paginate(): bin-pack with manual page-break, oversized-block, header/footer/footnote-def exclusion
- allocateFootnotes(): per-page allocation by reference walk
- DOM-backed measurer with bounded LRU cache keyed by (nodeId, marksFingerprint, font, width)
- usePageLayout, PageOverlay (afterEditable, pointer-events: none), PageFrame chrome
- FootnotePortal hides in-flow definitions via injected CSS
- Editor API: getPages, getPageOf, getFootnotes
- Editor transforms: insertPageBreak, setHeader, setFooter
- 9 unit tests passing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(template): wire real @platejs/pagination via vendored dist — refs #357 #365

Drop placeholder pagination kit. Vendor @platejs/pagination's built dist
under templates/plate-playground-template/vendor/platejs-pagination so the
template can consume the real plugin without npm publish:
- pagination-kit: PaginationPlugin.configure({ A4, 96px margins, 48 header/footer, 96 footnote well })
- pagination-toolbar-button: clicks editor.tf.pagination.insertPageBreak()
- vendor/.gitignore overrides repo **/dist so committed artifacts ship

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pagination): visible page panel + resilient toolbar — refs #357 #365

PageOverlay: replace overlay-on-editor (z-index conflict, white-on-white)
with a fixed top-right card showing "Pages: N" plus PageFrame thumbnails.
Always visible regardless of editor theme.

Toolbar button: try editor.tf.pagination.insertPageBreak() first; fall back
to editor.tf.insertNodes({type:'pageBreak'}) so the action works even when
the plugin's transforms aren't bound (e.g. during HMR or kit ordering).
Drop the "plugin not loaded" toast.

Refresh vendored dist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pagination): KEYS resolution + reactive overlay + toggle preview — refs #353 #357 #365

Two bugs uncovered via dogfooding the deploy:

1. Plugin silently dropped from editor: source referenced KEYS.pagination
   and KEYS.pageBreak; both undefined in published platejs@53.0.3 → key
   resolved to undefined → editor filtered the plugin out. Fixed by
   replacing with local PAGINATION_KEY / PAGE_BREAK_KEY / etc. constants
   in lib/internal/keys.ts.

2. Overlay never re-rendered on edits: useEditorRef + useMemo([editor.children])
   misses Slate in-place mutations. Switched to useEditorValue +
   usePluginOption.

Also:
- previewVisible option + togglePreview() transform; toolbar button now
  toggles the side panel instead of inserting a break.
- PageFrame now renders mini-content per block (h1-h6 / blockquote / code /
  paragraph) so the preview is content-aware, not just empty page chrome.
- Refresh vendored dist + .gitignore dogfood-output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pagination): page config + header/footer toggles + settings dropdown — refs #357

Adds the next layer per the CR plan + agreed roadmap:

Plugin (lib):
- PageSize: union of preset key | { width, height } literal — registry escape hatch
- BasePaginationOptions: + headerVisible, footerVisible (defaults false)
- BasePaginationTransforms: + setPageSize, setMargins, toggleHeader, toggleFooter
- toggleHeader/toggleFooter inserts a top-level header/footer block at index 0 / N
  with placeholder text ("Header" / "Footer") and flips the visibility option;
  removeByType dedupes if normalization produced duplicates
- resolvePageSize handles both preset and literal forms; resolvePageRect clamps
  contentHeight/contentWidth to >= 0

Overlay (react):
- usePluginOption subscribes to pageSize, margins, headerVisible, footerVisible
  so the panel re-renders when any of them changes via toggle/setMargins/etc.

Template:
- pagination-toolbar-button: replaces the single click handler with a Radix
  DropdownMenu — sections for Display (preview/header/footer toggles), Page
  size (A4/Letter/Legal radio), Margins (narrow/default/wide presets)
- vendor:pagination script — copies packages/pagination/dist into the template's
  vendored package; addresses CR's "Vendored dist refresh" follow-up

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pagination): apply CR/Gemini review — derived header/footer, normalizeNode, content cache, pt scaling — refs #357

Implements the agreed subset of the CR plan-conejo (comment 11) and the
Gemini + CR review feedback:

Architecture:
- Move page-state.ts from react/internal/ to lib/internal/ so the base
  Slate plugin's API can read pages without depending on React. Keeps
  imports flowing lib → react, never the other way.

Header/footer model:
- Drop headerVisible / footerVisible options. Derive presence from the
  doc itself (editor.children.some(n => n.type === HEADER_KEY)). Removes
  the option-vs-Slate-history asymmetry CR risk-flagged: undo of a
  toggleHeader call now correctly restores the header without leaving a
  stale option flag.
- Add api.pagination.hasHeader / hasFooter for consumers that don't want
  to walk children themselves.
- Toolbar dropdown checkboxes derive state via useEditorValue + same scan.

Invariants:
- Add overrideEditor → normalizeNode that runs at root path:
  enforceHeaderFooterInvariants drops duplicate header/footer blocks and
  re-positions the survivor (header at [0], footer at last index). Defends
  against paste/undo producing duplicates.

Measurement correctness:
- MeasureCacheKey gains a contentHash field; usePretextMeasurer hashes
  type + plain text per node and feeds it in. Cache now invalidates on
  in-place edits (Gemini + CR concern).
- collectPlainText stops inserting a space between adjacent leaves —
  bold-then-plain runs ("He" + "llo") were measured as "He llo", over-
  counting line breaks. Fixed.
- scaleFont uses /(\d+)(px|pt)/ and re-emits the matched unit so pt-sized
  fonts scale correctly for headings.
- resolvePageRect clamps contentWidth/Height to ≥ 0 (already in place,
  preserved when accepting the new PageSize union).

UI:
- PageOverlay's THUMB_SCALE is now computed via computeThumbScale(width)
  = min(0.18, 196/pageWidth) — scales custom landscape page sizes down
  to the panel width.
- React keys for the page list use `page-${pageIndex}` (stable).

Cleanup:
- marks-fingerprint: rename `sorted` → `segments` (CR nitpick — name
  reflected traversal order, not sort).

Tests: 11 pass (added contentHash + hashString cases).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 📝 CodeRabbit Chat: Implement requested code changes

* Update packages/pagination/src/static/page-break-element-static.tsx

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

* refactor(pagination): split helpers into queries/transforms; KEYS via getType; plugin spec

Apply footnote-plugin best practices to BasePaginationPlugin:
- Extract queries (lib/queries) and transforms (lib/transforms) modules.
- Move BasePaginationConfig/Api/Transforms types to lib/types.
- Use editor.getType(KEYS.x) so consumer .configure({ node: { type } }) overrides flow through.
- Fix enforceHeaderFooterInvariants stale-index bug (collect → sort desc → remove → re-scan → move).
- Add base-pagination-plugins.spec covering plugin schema, API/transforms surface, toggle/set/insert behavior, and normalize invariants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Replace packages/pagination with the version from origin/pagination.
Brings BasePaginationPlugin, PaginationCoordinator, PageElement,
YjsIntegration, leaderElection, reflowEngine, runtime, registry,
and example_visualization_with_toggle.

Amp-Thread-ID: https://ampcode.com/threads/T-019e2aaa-8068-77e8-8c96-7aed37adf1fe
Co-authored-by: Amp <amp@ampcode.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Type editor as SlateEditor; route through editor.api / editor.tf to
satisfy platejs typings without slate Editor casts. Use TextApi.isText
and ElementApi.isElement. Narrow Operation via 'path' in op /
'newPath' in op. Drop dynamic getType?/getOption?/hasEditableTarget
accesses.
Extract PaginationPlugin into its own file so src/index.ts is purely
the brl-generated barrel. Move example_visualization_with_toggle/
under internal/ so brl skips it; T8 removes it entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move YjsIntegration into src/yjs/ subfolder so the main barrel no
longer transitively depends on @platejs/yjs. Mark @platejs/yjs peer
as optional. Extend tsdown + brl tooling to discover the yjs
subpath alongside existing react/static lanes.
drop dead example dir + drop tsconfig exclude paths for it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ug logs

- runtime.notify: queueMicrotask coalesces multiple markDirty in same tick
- internal/scheduleIdle: SSR-safe ric+setTimeout fallback, drops window-as-any
- ReflowOptions.debug (default false) gates splitOversizedBlock console.error
- splitOversizedBlock: bag last two params for useMaxParams compliance
- tests: async-aware notify expectations + new microtask coalescing test
Supply-chain defense: packages must be 7+ days old before install. Adds [test] root="./packages" in monorepos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PaginationPlugin now mounts PaginationRegistryProvider (aboveEditable) and
the reflow PaginationCoordinator (afterEditable) itself, so registering the
plugin is enough to render + reflow pages.

Also repair the grafted package: fix PageElement composed-ref import
(@udecode/react-utils), stop the coordinator spec's global module mock from
stripping exports other specs need, drop vitest/stale assertions, lint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Use literal 'pagination' key, not KEYS.pagination (unreleased in published
  @platejs/utils → undefined key → content never wrapped into pages).
- Mount registry provider + reflow coordinator in one shared aboveEditable
  subtree (PaginationAboveEditable) so the coordinator reads the pages that
  PageElement registers; the split provider/coordinator gave separate registries.
- Render the page number in each page's bottom margin (paginated mode).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation of the premirror-inspired pagination rewrite (derived layout; the
document model never changes). Pure, DOM-free, deterministic:

- layout/types.ts: layout contract (PageSpec/margins/policies, Unmeasured +
  Measured snapshots, LayoutOutput = pages → frames → BlockFragments + breakReason).
- layout/compose.ts: composeLayout — block-level page fill with widow/orphan,
  keep-with-next, manual breaks, splittable-block fragmenting, oversized overflow.
- layout/snapshot.ts: buildSnapshot — Slate value → flat block snapshot with
  stable content-based ids + atomic/keepWithNext/breakBefore hints.

16 tests, typecheck + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
measure/measure.ts: measureSnapshot turns an UnmeasuredSnapshot into a
MeasuredSnapshot. The DOM read is injected (MeasureFn) so the assembly + cache
layer is pure/unit-tested; caching is keyed by stable content id + content
width (measure-once, reuse unchanged blocks). 6 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
react/domMeasure.ts: the MeasureFn that reads a top-level block's rendered
height (incl. vertical margins) + computed line height from the live editor DOM
via ReactEditor.toDOMNode. The only DOM touch in the measurement path; feeds
measureSnapshot. Resilient fallbacks for line height.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P4 of the rewrite: render derived pages without mutating the document.

- react/geometry.ts: getPageGeometry / getBlockPlacements (pure, tested) — stack
  pages + map blocks to page-frame positions.
- react/domMeasure.ts: pure-DOM MeasureFn (top-level [data-slate-node=element]
  children); no slate-react dependency.
- react/alignContent.ts: page-start CSS spacers align a single continuous
  Editable's content to page frames (no model change).
- react/index.ts: clean @platejs/pagination/react entry re-exporting the
  slate-react-free pipeline (snapshot/measure/compose + geometry/measure/align).
- apps/www dev/pagination2: demo (white A4 chrome + Editable overlay + page numbers).

Verified live in agent-browser via the playground template: 4 A4 pages, content
flows across page boxes with clean boundaries + page numbers. 149 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The data layer both P0s (split-block rendering + caret/selection mapping)
require — locate where a block/line lands and project it to screen coords.

- layout/mapping.ts: buildMappingIndex — fragmentsOfBlock / pageOfBlock /
  pageOfBlockLine / fragmentOfBlockLine / isSplit (block→page/fragment).
- layout/projection.ts: fragmentRects (absolute stack rects per fragment of a
  split block) + blockLinePosition (caret line → absolute stack position).

Pure, deterministic. 8 tests; 24 layout tests green; typecheck + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A block taller than a page now renders correctly across page boxes without
mutating the document (approach #1).

- react/splitClones.ts: computeSplitPlan (pure, layout-level) + renderSplitClones
  (DOM). One live Editable stays editable, clipped to the slice that fits its
  page; each later page gets a read-only clipped clone of the next slice.
  Slicing is real-pixel (live block's measured top/height + page geometry) with
  line-boundary snapping via Range.getClientRects() — seamless live→clone and
  clone→clone junctions (no overlap/gap/half-line).
- react/index.ts: export mapping, projection, splitClones.

Verified live (agent-browser): a block ~7× page height splits cleanly across
pages. 161 tests; typecheck + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arthrod and others added 6 commits May 29, 2026 11:14
…a layer

Implements stage 1 of the PRETEXT chrome design at
docs/plans/2026-05-29-pagination-chrome-derived-projection.md.

# What

`composeLayout` now accepts an optional `chrome: { header?, footer? }` on its
input and emits matching `chrome: { header?, footer? }` rects on every page in
its output. Each band has `heightPx` configured by the consumer; the composer
shrinks the content frame by the configured heights before packing and emits
page-local rects (x, y, widthPx, heightPx) the React overlay can use to anchor
chrome content WITHOUT any DOM measurement.

The user-visible answer to "page numbers follow the screen": chrome rects are
*pure composer output*, not DOM measurements, so they never drift with scroll.
The React overlay (next commit) reads `PageLayout.chrome.{header,footer}` and
positions absolute siblings at page-anchored Y values — same anchoring strategy
that already works correctly for break-lines (verified via headless probe on
the live deploy: marker.rectTop tracked scrollTop with delta=200 on a 200px
scroll, exactly anchored to the document).

# Type extensions

`PageChromeSpec`, `PageChromeRect`, `ChromeRenderContext` in `layout/types.ts`.
`LayoutInput` gains `chrome?: { header?: { heightPx }; footer?: { heightPx } }`.
`PageLayout` gains `chrome?: { header?: PageChromeRect; footer?: PageChromeRect }`.

The `render` function for chrome content lives on `PaginationOptions` (next
commit), NOT on `LayoutInput`, because composeLayout is pure and the React
node it would return is React-specific. Composer cares only about `heightPx`.

# Composer change

`bounds.y` += `headerHeightPx`; `bounds.height` -= `headerHeightPx + footerHeightPx`.
A `chromeRectsForPage` constant is computed once (chrome is layout-wide) and
attached to every emitted page via `flushPage`. When no chrome is configured,
the `chrome` field stays absent — existing tests unaffected (53 pre-existing
+ 7 new = 60 / 60 pass).

# Tests

`compose-chrome.spec.ts`:

  1. baseline (no chrome) — chrome absent, frame unchanged
  2. header only — frame.y bumps by header.heightPx, frame.height shrinks
  3. footer only — frame.height shrinks; footer.y == bottom of page - margin - height
  4. header + footer — both subtract from content
  5. geometry — chrome.x == margin.left, chrome.widthPx == content width
  6. per-page identical chrome rects across a multi-page layout
  7. extra packing — chrome can force MORE pages when the content frame shrinks

# Coverage

7 new tests; full suite 60 / 60 (was 53 / 53). 36 expect() calls in the new
file. No regressions in the existing composer, mapping, measure, or React
host tests.

# Next commit

React overlay rendering + `PaginationOptions.chrome.render` plumbing +
`PageNumber` convenience export + playground demo wiring + re-deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ground demo

Iteration 1 of the /loop self-paced chrome push. Completes the React side of
the design doc:

# Plugin options

`PaginationOptions.chrome?: { header?: PageChromeOption; footer?: PageChromeOption }`
where `PageChromeOption = { heightPx; render(ctx) }`. The render function is
declared PRETEXT-safe in its docstring: pure, no DOM access, no editor
mutation, no scroll-state reads. The composer cares only about `heightPx`
(piped through `LayoutInput.chrome`); the render function stays on the plugin
options so the pure layout pipeline remains React-free.

# React overlay

`PaginationBreakLines` now:

  - reads `chrome`, `page`, `margins` via `usePluginOption`
  - passes chrome heights to `composeLayout`
  - computes per-page anchor points: `headerY(i)` = top-of-page-i's-first-block
    (which is what the user sees as the visual top of each page in continuous
    view); `footerY(i)` = top of NEXT page's first block minus
    `chrome.footer.heightPx` (so the footer sits just above the next break),
    falling back to the document's bottom for the last page
  - renders one chrome `<div>` per page per band, absolute-positioned at the
    composer-computed Y, with `data-slot="pagination-chrome"`,
    `data-pagination-chrome="header" | "footer"`, and `data-page-index`
  - preserves the existing dashed break-line at each interior boundary
  - keeps the legacy "Page N of M" left-margin chip ONLY when no chrome is
    configured (backward compat)

# `PageNumber` convenience export

`packages/pagination/src/react/chrome/PageNumber.tsx` ships three pure
renderers:

  - `PageNumber` — centered `Page N of M`, the most common footer
  - `PageNumberWithTitle(title, opts?)` — `title | Page N of M`, skips
    page 1 by default (cover-page convention)
  - `TextHeader(text)` — constant uppercase + thin underline, the
    default for legal-document headers

Each renderer derives every pixel from `ChromeRenderContext.{pageIndex,
pageCount, page, margins}` — never touches the DOM, never mutates the
editor.

Added to the `react` barrel (`packages/pagination/src/react/index.ts`).

# Playground demo

`templates/plate-playground-template/src/components/editor/plugins/pagination-kit.tsx`
now configures both bands by default:

  chrome: {
    header: { heightPx: 28, render: TextHeader('Plate Playground') },
    footer: { heightPx: 32, render: PageNumber },
  }

This is what the user actually wanted: a footer carrying `Page N of M` at the
bottom of each page, and a small uppercase document-title header at the top —
both rendered at composer-computed positions, scroll-invariant.

# Tests

60/60 still pass (53 pre-existing + 7 new from the data-layer commit).
The chrome `render` functions are React; their pixel emission is verified by
the live dogfood (next step of the /loop) rather than a JSDOM snapshot, since
the bug surface lives in DOM anchoring under live scroll.

# Next /loop iteration

Build + deploy + run the agent browser against the live playground to confirm
headers/footers/page-numbers render and stay anchored as the user scrolls and
edits. Fix any drift found.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dor sync

Three fixes from iter-1 dogfood against the live deploy:

# 1. `React is not defined` after toggling pagination ON

`packages/pagination/src/react/chrome/PageNumber.tsx` declared
`import type * as React from 'react'` — a type-only import that
elides at runtime. The JSX in every render function compiles to
`React.createElement(...)`, so the moment a consumer invoked
`PageNumber({...})` the page error'd out and the editor
unmounted. Reproduced live via headless probe; switched to
`import * as React from 'react'` (runtime), confirmed bundle
emits `React$1.createElement` and the editor stays mounted.

# 2. Last-page footer overlapping its header

In continuous view a footer naturally lives one chrome.heightPx
above the NEXT page's first block. The previous code anchored
the LAST page's footer to "end of last block - chrome.heightPx",
which on a sparse last page (just one short block after the
final break) made the footer land at the same Y as the header.

Fix: anchor the last page's footer to the page's *geometric*
bottom (`startBlock + (page.heightPx - margins.top -
margins.bottom)`), falling back to the actual content end IF
the last block's height exceeds the geometric page height (so
the footer never lands above content).

# 3. `bun run vendor:pagination` left node_modules stale

bun's `file:` dep resolution caches the installed copy in
`node_modules/@platejs/pagination/dist/`. The previous script
copied the freshly-built dist into `vendor/platejs-pagination/
dist/` but did NOT refresh node_modules — so Next.js still
bundled the OLD dist on every redeploy. Diagnosed via sha256:
node_modules and vendor hashes diverged.

Fix: extend the script to ALSO `rm node_modules/...dist && cp
vendor/...dist node_modules/...dist`. Hashes now match after
every `bun run vendor:pagination`.

# Verification

- `bun test` 60/60 ✓
- Headless probe of live deploy (Worker version f6907805): no
  `React is not defined` after toggle; 7 headers, 7 footers,
  6 break-lines rendered; mathematical scroll-anchoring
  confirmed (Page 3 footer rectTop delta = 2785 = scrollTop).
- Last-page footer overlap reproduced before fix
  (Page 7 footer rectTop == Page 7 header rectTop == 5250) and
  resolved after this commit (verified next iter).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nvention

Iter-3 of the /loop chrome push.

# Regression guards (compose-chrome-edge-cases.spec.ts)

5 tests, all green, pinning behaviors that broke (or were at risk of
breaking) during the iter-1/iter-2 live dogfood:

  1. last-page chrome rect present when the page's content is much
     shorter than a full page — guards the iter-2 fix
     (footerY for last page = page-geometric bottom, not last-block
     bottom). Verifies chrome.footer is emitted on the short page
     and matches every other page's footer.y identically.

  2. content frame height is the SAME on every page — confirms the
     chrome subtraction is constant (1123 - 96 - 96 - 28 - 32 = 871),
     no page-by-page drift.

  3. composer is pure — same input ⇒ identical pages JSON.

  4. zero-height chrome is valid — heightPx=0 doesn't change frame
     height; chrome rects exist but are zero-band.

  5. multi-page packing under chrome subtraction — math:
     content/page = 871 px, blocks @ 200 px = 4 blocks/page,
     10 blocks → 3 pages (4+4+2).

Brings the pagination suite to 65 / 65 (was 60 / 60).

# Cover-page convention in the playground

`templates/plate-playground-template/src/components/editor/plugins/
pagination-kit.tsx` now uses `PageNumberWithTitle('Plate Playground')`
for the footer (defaults `skipFirstPage: true` so page 1 stays blank).
A one-line render-wrapper does the same for the header. This matches
the printed-document convention where page 1 is the cover.

# Verification

- bun test: 65 pass / 0 fail / 158 expect() calls.
- bun run typecheck: passes after `vendor:pagination` (which now
  syncs node_modules too — iter-1 commit).
- Live dogfood pending (next /loop iteration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…me bands

Iter-4 visual polish. Each chrome band now carries a single 1-px
`rgb(226 232 240)` rule on its inside edge:

  - footer chip / footer-with-title: `border-top` (= top of bottom margin)
  - header text: `border-bottom` (= bottom of top margin)

Together those two rules visually demarcate the page as a chrome→
margin→content→margin→chrome stack at a glance, addressing the
"margins" half of the original mandate without requiring a heavier
paged-view treatment.

Also extracted three shared constants (`CHROME_FONT`, `CHROME_INK`,
`CHROME_RULE`) so consumers can build sibling renderers that match
without copying typography strings.

# Verification

`bun test` still 65 / 65. Live-deploy dogfood in flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third built-in chrome renderer alongside PageNumber and
PageNumberWithTitle. Bare "N" or "N / M" centered in the band, with
tabular-nums so the digits don't shift width between pages 9 and 10.

Use case: book templates where a running header carries the title and
all that's needed in the footer is the page number itself.

Options:
  - showTotal: false (default) → "1", "2", "3"
  - showTotal: true  → "1 / 7", "2 / 7", "3 / 7"
  - skipFirstPage: true → render null on page 1 (cover-page convention)

bun test: 65 / 65 ✓.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a pretext-driven, non-mutating pagination pipeline: snapshot → measure → compose plus mapping/projection, DOM-backed measurement, per-editor registry, React continuous overlay, many tests, changesets/docs, playground wiring, and removal/simplified legacy pagination exports.

Changes

Pagination v2 pipeline and integration

Layer / File(s) Summary
Layout contract and composer
packages/pagination/src/layout/*
Adds pagination layout types, composeLayout implementing chrome-aware whole-block placement, and returns LayoutOutput with mapping and metrics.
Snapshot and stable ids
packages/pagination/src/layout/snapshot.ts, tests
Adds buildSnapshot to emit deterministic block ids/text and collision-safe fallback ids with unit tests.
Pretext measurement & measureSnapshot
packages/pagination/src/measure/*
Adds measureTextLines, measureBlockHeight, and measureSnapshot with per-(id,width) caching, flowHeight/renderedHeight rules, rounding/fallback handling, and atomic packing semantics; includes tests.
Mapping & projection helpers
packages/pagination/src/layout/mapping.ts, projection.ts, packages/pagination/src/react/geometry.ts
Implements buildMappingIndex, fragmentRects, blockLinePosition, getPageGeometry, and getBlockPlacements for absolute projection and caret/selection mapping.
React host & DOM measure
packages/pagination/src/react/*
Adds PaginationPlugin overlay, createDomMeasure, topLevelBlockElements, alignContentToLayout, chrome/PageNumber render helpers, geometry utilities, and ResizeObserver-driven recompute; overlay is advisory and non-mutating.
Registry & Base plugin
packages/pagination/src/lib/registry.ts, BasePaginationPlugin.ts
Introduces per-editor WeakMap layout registry (get/invalidate/ensure/shouldInvalidateLayout) and BasePaginationPlugin that marks registry dirty on content operations.
Tests: regressions & integration
packages/pagination/src/**/__tests__/*
Adds many tests covering compose (chrome/edge cases), mapping/projection, snapshot stable-id behavior, measure/pretext, registry, and plugin behaviors including PR-specific regressions.
Removals and barrel changes
packages/pagination/src/lib/*, packages/pagination/src/react/*, packages/pagination/src/static/*
Removes legacy mutator-era modules and transforms (paginate, allocate-footnotes, page-frame, many transforms/queries/static renderers), simplifies barrels and package exports.
Docs, changesets, templates, config
.changeset/*, docs/plans/*, diary.md, AGENTS.md, templates/*, bunfig.toml, package.json, tooling/*`
Adds changesets documenting multiple releases/features/fixes, extensive planning/audit docs and diary, AGENTS pretext gate, playground demo pages and kit/toolbar changes, test/install config, and new npm scripts.

Sequence Diagram

sequenceDiagram
  participant Editor as Slate Editor
  participant Snapshot as buildSnapshot
  participant Measure as measureSnapshot (pretext)
  participant Compose as composeLayout
  participant Registry as LayoutRegistry
  participant Overlay as PaginationOverlay

  Editor->>Snapshot: buildSnapshot(editor.value)
  Snapshot->>Measure: measureSnapshot(unmeasured, width)
  Measure->>Compose: MeasuredSnapshot
  Compose->>Registry: LayoutOutput (pages, mapping, breaks)
  Registry->>Overlay: ensureLayout() → breaks/mapping
  Overlay->>Editor: render advisory rules & chrome (no mutation)
Loading

Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels
Feat2, Review effort 3/5

"I nibble lines where pretext grows,
Counted hops where paragraph flows.
Whole blocks stitched, no seams to tear,
Registry hums — overlay's air.
Rabbit stamps pages with a code-y paw."

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch work/pagination-synthesized

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request rewrites the @platejs/pagination package, replacing the document-mutating engine with a pure, derived projection pipeline driven by @chenglou/pretext for line-accurate, deterministic layout. It introduces a continuous-view React overlay that paints advisory page-break lines and page chrome without modifying the Slate document. The review feedback identifies a critical bug in resolveLineHeight where unitless line-height multipliers are not scaled by font size, a geometry calculation error in PaginationBreakLines causing footer overflow on the last page, and an issue in stableId that excludes numeric IDs from caching. Additionally, a simplification for the spacer height formula in computePageStartSpacers is suggested.

Comment on lines +34 to +42
function resolveLineHeight(style: CSSStyleDeclaration): number {
const lh = Number.parseFloat(style.lineHeight);
if (Number.isFinite(lh) && lh > 0) return lh;

const fontSize = Number.parseFloat(style.fontSize);
if (Number.isFinite(fontSize) && fontSize > 0) return fontSize * 1.5;

return 20;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

In resolveLineHeight, if style.lineHeight is a unitless multiplier (e.g., '1.5'), Number.parseFloat will return 1.5. Since 1.5 > 0, the function will return 1.5 pixels as the line height instead of scaling it by the font size. This will cause measured block heights to be extremely small and break pagination packing. Scaling unitless multipliers by the font size resolves this.

Suggested change
function resolveLineHeight(style: CSSStyleDeclaration): number {
const lh = Number.parseFloat(style.lineHeight);
if (Number.isFinite(lh) && lh > 0) return lh;
const fontSize = Number.parseFloat(style.fontSize);
if (Number.isFinite(fontSize) && fontSize > 0) return fontSize * 1.5;
return 20;
}
function resolveLineHeight(style: CSSStyleDeclaration): number {
const fontSize = Number.parseFloat(style.fontSize) || 16;
const lh = Number.parseFloat(style.lineHeight);
if (Number.isFinite(lh) && lh > 0) {
return lh < 5 ? lh * fontSize : lh;
}
return fontSize * 1.5;
}

Comment on lines +120 to +121
const pageContentHeightPx =
page.heightPx - margins.topPx - margins.bottomPx;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In PaginationBreakLines, pageContentHeightPx is defined as page.heightPx - margins.topPx - margins.bottomPx without subtracting the header height. Since startY (the top of the start block) already includes the header height, startY + pageContentHeightPx will be shifted down by headerHeightPx too low, causing the footer on the last page to overflow the page boundary. Subtracting the header height resolves this.

Suggested change
const pageContentHeightPx =
page.heightPx - margins.topPx - margins.bottomPx;
const pageContentHeightPx =
page.heightPx - margins.topPx - margins.bottomPx - (chrome?.header?.heightPx ?? 0);

Comment on lines +46 to +50
function stableId(node: SlateNode): string {
if (typeof node.id === 'string' && node.id.length > 0) return node.id;

return `${node.type ?? 'node'}#${hash(nodeText(node))}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In stableId, the check typeof node.id === 'string' excludes numeric IDs. If a consumer uses numeric IDs, they will fall back to content hashes, causing unnecessary cache misses/thrashing on edits. Converting node.id to a string if it is present resolves this.

Suggested change
function stableId(node: SlateNode): string {
if (typeof node.id === 'string' && node.id.length > 0) return node.id;
return `${node.type ?? 'node'}#${hash(nodeText(node))}`;
}
function stableId(node: SlateNode): string {
if (node.id != null && String(node.id).length > 0) return String(node.id);
return `${node.type ?? 'node'}#${hash(nodeText(node))}`;
}

Comment on lines +18 to +47
export function computePageStartSpacers(
layout: LayoutOutput,
input: LayoutInput,
gapPx: number = PAGE_STACK_GAP_PX
): Map<number, number> {
const contentHeight =
input.page.heightPx - input.margins.topPx - input.margins.bottomPx;
const spacers = new Map<number, number>();

for (const page of layout.pages) {
if (page.index === 0) continue;

const first = page.frames[0].fragments[0];
if (!first) continue;

const prev = layout.pages[page.index - 1].frames[0];
const prevBottom = prev.fragments.reduce(
(max, f) => Math.max(max, f.y + f.heightPx),
0
);

spacers.set(
first.path[0],
contentHeight -
prevBottom +
input.margins.bottomPx +
gapPx +
input.margins.topPx
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In computePageStartSpacers, the formula for computing the spacer height can be simplified. Since margins and chrome heights are constant across pages, they cancel out perfectly, leaving the spacer height as exactly input.page.heightPx - prevBottom + gapPx. Simplifying this removes the misleading contentHeight variable and makes the math much clearer.

export function computePageStartSpacers(
  layout: LayoutOutput,
  input: LayoutInput,
  gapPx: number = PAGE_STACK_GAP_PX
): Map<number, number> {
  const spacers = new Map<number, number>();

  for (const page of layout.pages) {
    if (page.index === 0) continue;

    const first = page.frames[0].fragments[0];
    if (!first) continue;

    const prev = layout.pages[page.index - 1].frames[0];
    const prevBottom = prev.fragments.reduce(
      (max, f) => Math.max(max, f.y + f.heightPx),
      0
    );

    spacers.set(
      first.path[0],
      input.page.heightPx - prevBottom + gapPx
    );
  }

  return spacers;
}

…ision-safe fallback ids

# 1. mapping.ts — positional page index

`buildMappingIndex` stored `page.index` in each `FragmentRef.pageIndex`.
Consumers downstream dereference `layout.pages[ref.pageIndex]` and
`geometry.placements[ref.pageIndex]` AS ARRAY OFFSETS — so any future
composer that emits non-contiguous `page.index` values (skipped covers,
re-numbering after a deletion) silently breaks every projection.

Fix: use the `forEach` positional index instead. `page.index` stays
unchanged on the PageLayout for consumers that want the composer's
emitted number; the mapping invariant is now purely positional.

# 2. snapshot.ts — collision-safe fallback ids

`stableId` falls back to `${type}#${hash(text)}` for nodes lacking an
author-supplied id. Two empty paragraphs (or any two siblings with the
same type and text) produced the SAME fallback id, corrupting the
`(id, width)` measure cache (two blocks share one cached height) and
confusing fragment grouping.

Fix: track ids seen within a single `buildSnapshot` pass and disambiguate
duplicates by appending the positional index (`...@2`). Real author-
supplied ids stay untouched (we register the raw input value the moment
it first appears, so the second occurrence — if a consumer ever uses the
same explicit id twice — would dedupe; but that's a consumer bug, not a
silent shadow).

# Tests

4 new tests in `coderabbit-pr438-fixes.spec.ts` pin both behaviors:

  positional mapping:
    - real-composer path: every block's ref.pageIndex matches the page's
      array position; pages[ref.pageIndex] contains the block's fragment
    - synthetic non-contiguous page.index test: mapping still resolves
      to the correct array position even when page.index is 10, 25, ...

  collision-safe ids:
    - two empty paragraphs → distinct ids
    - three identical paragraphs → 3 distinct ids
    - author-supplied real ids → untouched
    - mix of explicit + fallback → explicit untouched, fallback dedupes

Suite: 71 / 71 (was 65).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: CodeRabbitAI <noreply@github.com>

@arthrod arthrod left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review — pretext gate + code audit

Pretext gate: ✓ satisfied

domMeasure.ts derives block height from measureBlockHeight@chenglou/pretext (line count × lineHeight), not offsetHeight. DOM rect (getBoundingClientRect().height) is used only for splittable === false atomic blocks (images, tables, embeds) where pretext has no text to shape — correct exemption per the spec.

Other notes

  • bounds.height = page.heightPx − margins − headerHeightPx − footerHeightPx — chrome correctly shrinks the content frame before block packing.
  • All four chrome render presets (PageNumber, PageNumberWithTitle, PageNumberMinimal, TextHeader) are pure functions — no DOM access, no editor mutation.
  • Legacy left-margin chips preserved unchanged when chrome is absent.
  • PageChromeOption (public) / PageChromeSpec (internal layout) separation is clean.
  • 65 / 65 tests pass (verified locally after pnpm install).

No blocking issues found.


Generated by Claude Code

…oseLayout JSDoc

# 1. composeLayout: clamp non-positive frame bounds

If margins (or margins + chrome) sum to >= page dimension, the content
frame went negative. Downstream packing then produced pathological
output. `Math.max(0, …)` on both width and height keeps composition
stable through transient invalid input (e.g. the user typing margin
values that briefly exceed the page).

JSDoc added on the function explaining the contract, the chrome
extension's effect, and the clamp's rationale.

# 2. alignContentToLayout: clamp page-start spacer to non-negative

When `prevBottom` exceeded the content height (oversized last
fragment), the spacer expression went negative and the CSS
`margin-top` it set as a spacer would pull the next page-start block
UPWARD across the boundary, visually breaking pagination.

# 3. geometry.fragmentRects: startsPage page-local, not frame-local

Multi-frame pages marked the first fragment of EVERY frame as a page
starter. A page with two frames produced TWO `startsPage: true`
placements, falsely promoting later-frame blocks across the page
boundary. Tracked `pageFragmentSeen` per page so only the very first
fragment of the page gets the flag.

# Tests

4 new tests in `coderabbit-pr433-fixes.spec.ts` pin the bounds-guard
behavior:

  - margins consuming entire page height → bounds.height clamps to 0
  - margins consuming entire page width → bounds.width clamps to 0
  - margins + chrome both consuming → both clamp to 0
  - happy path with valid margins unchanged

Suite: 75 / 75 (was 71). The alignContent + geometry fixes are
exercised by the live dogfood path; their unit-test coverage requires
mocking the DOM-anchored geometry layer, which is exercised in the
existing `geometry.spec` indirectly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: CodeRabbitAI <noreply@github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plans/2026-05-16-pagination-end-to-end-fix-v1.md (1)

1-149: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move this plan to docs/plans/ and rewrite it as latest-state guidance only.

This file is placed under plans/ and includes changelog-style/history sections (for example PR closure/background and “stale” comparisons). The repo rules require plan files under docs/plans/ and markdown content written as current-state reference only.

As per coding guidelines: "docs/plans/**: Planning files should be located under docs/plans/" and "**/*.md: Write documentation as user-facing reference for the LATEST state only. Never use changelog-style language."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plans/2026-05-16-pagination-end-to-end-fix-v1.md` around lines 1 - 149, Move
and rewrite the plan as a current-state guidance document: relocate the content
from plans/2026-05-16-pagination-end-to-end-fix-v1.md into docs/plans/ (e.g.,
docs/plans/pagination-end-to-end-fix.md) and remove changelog/history phrasing
(PR `#4830` context, Sage Research Findings Summary as comparative "found vs.
vendor" narrative); convert sections like "Objective", "Implementation Plan",
"Phase 1/2/3/4", and "Verification Criteria" into present-tense actionable
guidance only (describe the required fixes such as updating
pagination-toolbar-button.tsx to read documentSettings.sizes, header/footer
detection inside page children, removing the WIP comment in editor-kit.tsx,
fixing toolbar cast to use PaginationTransforms, and vendor/regeneration/deploy
steps) and delete retrospective items (e.g., "Key findings" table or PR closure
notes) so the file contains only the latest-state instructions and verification
checklist.
🧹 Nitpick comments (7)
docs/plans/premirror-audit-findings.md (1)

3-7: ⚡ Quick win

The intro should be converted to latest-state reference wording.

This opening frames the doc as a historical translation delta. Please rewrite to describe the current architecture/contracts directly, without “what premirror did vs what we did” changelog framing.

As per coding guidelines "**/*.md: Write documentation as user-facing reference for the LATEST state only. Never use changelog-style language..."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/premirror-audit-findings.md` around lines 3 - 7, Rewrite the "##
Summary" intro to be a current-state reference: remove comparative/changelog
language about "Premirror vs we" and instead state the present architecture and
contracts directly (e.g., describe that the system operates at top-level block
granularity, explain that text extraction and line-breaking are done per-block
with estimated line counts rather than per-character/styled-segment
measurement). Explicitly list current missing features as present-state
limitations — lack of bidirectional position mapping, lack of dirty-range
incremental invalidation, and absence of decoration-projection rendering — and
phrase them as requirements or fidelity gaps to address, not as contrasts to
Premirror.
docs/plans/2026-05-23-pagination-impl-plan.md (1)

13-35: ⚡ Quick win

Rewrite this plan as current-state guidance, not retrospective triage.

This section is written as historical delta tracking (“already fixed vs remains”), which conflicts with the docs rule for latest-state-only reference style. Please restate as present-tense target architecture/requirements only.

As per coding guidelines "**/*.md: Write documentation as user-facing reference for the LATEST state only. Never use changelog-style language..."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/2026-05-23-pagination-impl-plan.md` around lines 13 - 35, Replace
the historical "0. Audit triage — what the scorch already fixed vs what remains"
section with a present-tense, state-first architecture/requirements summary:
remove delta/changelog phrasing and keep only current issues and targets (e.g.,
P0–P10 titles as current problems), describing each finding as the present-state
gap and the desired requirement or acceptance criteria; update evidence pointers
to the current code symbols (snapshot.ts:84 → reference snapshot.ts and
nodeText, pretext.ts:34, mapping.ts:18, measure.ts:55, types.ts,
projection.ts:36/66, domMeasure.ts:32/46/69, alignContent.ts:18, compose.ts:63)
and drop any "fixed vs remains" language so the table/list reads as current
deficiencies and next-step goals for the architecture.
packages/pagination/failures.md (1)

3-7: ⚡ Quick win

This should be reformatted from append-only audit log to current-state reference.

The “appended over time / never rewrites prior findings” framing is changelog-style and will age badly. Please convert this to a latest-state failures/spec reference.

As per coding guidelines "**/*.md: Write documentation as user-facing reference for the LATEST state only. Never use changelog-style language..."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pagination/failures.md` around lines 3 - 7, The failures.md document
currently uses an "append-only audit log" voice and must be rewritten as a
current-state reference: remove phrases like "appended", "never rewrites prior
findings", and any changelog-style narrative; replace with a clear "Current
known failures / limitations" heading and a succinct, up-to-date list of issues
with status, impact, and recommended remediation for each item; ensure
compliance with the repository guideline for "*.md" docs to present the LATEST
state only and keep historical changelog entries out of this file (move any
chronological audit notes to a separate CHANGELOG or audit-log file if needed).
package.json (1)

56-56: ⚡ Quick win

Add local verification gates before deploy:playground.

This deploy path skips template verification (typecheck, lint:fix, and build when build-sensitive), so it can publish a broken playground artifact. Consider prepending the checks to this script.

Suggested update
- "deploy:playground": "pnpm turbo build --filter=./packages/pagination && cd templates/plate-playground-template && bun run vendor:pagination && bun install && npx opennextjs-cloudflare build && npx opennextjs-cloudflare deploy",
+ "deploy:playground": "pnpm turbo build --filter=./packages/pagination && cd templates/plate-playground-template && bun run vendor:pagination && bun install && bun run typecheck && bun run lint:fix && bun run build && npx opennextjs-cloudflare build && npx opennextjs-cloudflare deploy",

As per coding guidelines "templates/plate-playground-template/**/*.{js,ts,tsx,jsx}: ... run bun run typecheck, bun run lint:fix, and bun run build if task touched app behavior or build config."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 56, The deploy:playground npm script currently skips
local verification; update the "deploy:playground" script so it first runs the
template checks by changing it to run typecheck, lint:fix and build for the
playground template before the existing steps (e.g. prepend: cd
templates/plate-playground-template && bun run typecheck && bun run lint:fix &&
bun run build && cd -), then continue with the existing
vendor/install/build/deploy sequence; target the "deploy:playground" script name
so the template is verified locally before packaging and deployment.
packages/pagination/src/layout/mapping.ts (1)

18-20: ⚡ Quick win

Avoid exposing mutable internal mapping arrays.

Line [54] returns the live array stored in byBlock. External mutation can silently corrupt index behavior for later calls.

♻️ Suggested hardening
 export type MappingIndex = {
   /** All fragments of a top-level block (path[0]), in document/page order. */
-  fragmentsOfBlock: (blockIndex: number) => FragmentRef[];
+  fragmentsOfBlock: (blockIndex: number) => readonly FragmentRef[];
@@
-  const fragmentsOfBlock = (blockIndex: number): FragmentRef[] =>
-    byBlock.get(blockIndex) ?? [];
+  const fragmentsOfBlock = (blockIndex: number): readonly FragmentRef[] =>
+    [...(byBlock.get(blockIndex) ?? [])];

Also applies to: 53-54

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pagination/src/layout/mapping.ts` around lines 18 - 20, The
fragmentsOfBlock accessor currently returns the live array stored in byBlock,
allowing external code to mutate internal state; change fragmentsOfBlock (and
any similar accessors) to return an immutable copy or readonly view
instead—e.g., return a shallow copy of byBlock[blockIndex] or change the return
type to readonly FragmentRef[]—so callers cannot modify the internal byBlock
array (refer to MappingIndex and the fragmentsOfBlock implementation and any
other accessors that expose byBlock).
packages/pagination/src/react/PaginationPlugin.tsx (1)

265-268: ⚡ Quick win

Document the exported PaginationPlugin API surface with JSDoc.

Please add a top-level JSDoc block describing key options (enabled, page, margins, chrome, breaks) and lifecycle behavior.

As per coding guidelines: **/*.{ts,tsx,js,jsx}: “JSDoc must be first-class for agents. Every API surface should be intuitive for both humans and AI agents.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pagination/src/react/PaginationPlugin.tsx` around lines 265 - 268,
Add a top-level JSDoc block immediately above the exported PaginationPlugin
constant that documents the API surface and lifecycle: describe the plugin
itself (PaginationPlugin), its key options object properties (enabled: boolean
to toggle plugin, page: current page number, margins: page margin spec, chrome:
header/footer/chrome rendering toggle, breaks: ContinuousBreak[] array
controlling pagination break points), and lifecycle/behavior notes (when options
are read/updated, how setOption/useHooks affect pagination state and render via
render.afterEditable PaginationBreakLines). Reference the exported symbol
PaginationPlugin and mention interaction with BasePaginationPlugin,
render.afterEditable (PaginationBreakLines), and useHooks for updating options
so callers know when to call setOption or reinitialize.
packages/pagination/src/measure/measure.ts (1)

60-64: ⚡ Quick win

Add function-level JSDoc for measureSnapshot contract.

This is a public API and should explicitly document cache semantics, fallback behavior, and flow-height rules at the function boundary.

As per coding guidelines: **/*.{ts,tsx,js,jsx}: “JSDoc must be first-class for agents. Every API surface should be intuitive for both humans and AI agents.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pagination/src/measure/measure.ts` around lines 60 - 64, Add a
function-level JSDoc comment immediately above the measureSnapshot declaration
that documents the contract: describe the parameters (snapshot:
UnmeasuredSnapshot, measure: MeasureFn, options: MeasureOptions) and return
(MeasuredSnapshot), explain cache semantics (when results may be cached, cache
keys/ttl if applicable, and whether callers must treat returned snapshots as
immutable), specify fallback behavior (what happens if measure throws or returns
partial data and how errors are surfaced or defaulted), and enumerate
flow-height rules (how height/flow values are derived, validated, and adjusted,
including any invariants callers can rely on). Mention any side-effects or async
behaviour and any preconditions/postconditions callers must satisfy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/pagination-automount-runtime.md:
- Line 5: Rewrite the sentence to describe current behavior (present-state)
instead of a change: replace "Mount the registry provider and reflow coordinator
automatically from `PaginationPlugin`, so registering the plugin is all that is
needed for pages to render and reflow" with a present-tense reference like
"`PaginationPlugin` mounts the registry provider and reflow coordinator
automatically, so registering the plugin is sufficient for pages to render and
reflow." Update the line in .changeset/pagination-automount-runtime.md to use
that phrasing and remove any change-log framing or past-tense wording.

In @.changeset/pagination-cache-key.md:
- Line 5: Rewrite the changelog entry to describe the current cache-key
contract: state that measureSnapshot caches measurements keyed by the tuple
(block id, width) so measurements for the same block at different widths are
stored separately and do not overwrite each other; mention measureSnapshot and
the cache key format "(block id, width)" and avoid any before/after or migration
language.

In @.changeset/pagination-continuous-breaks.md:
- Line 5: Rewrite the sentence to describe the current
getContinuousBreaks(layout) behavior in present tense (no "Add ..."): state that
getContinuousBreaks(layout) returns each interior page boundary identified by
the block and line that begin the next page, and that the continuous overlay
anchors its advisory rule to that boundary block’s live DOM top so the advisory
line aligns with a real block edge instead of relying on a pixel-only text sum
that ignores DOM margins; remove changelog phrasing and ensure the description
reads as the current API behavior.

In @.changeset/pagination-mapping-in-output.md:
- Line 5: Update the changelog entry to state only the current contract: say
that composeLayout builds a MappingIndex and exposes it as LayoutOutput.mapping,
and that projection reads LayoutOutput.mapping as the prebuilt MappingIndex;
remove any comparative or "instead of…" wording describing prior behavior.
Mention the symbols composeLayout, MappingIndex, LayoutOutput.mapping, and
projection to make the contract clear.

In @.changeset/pagination-margin-aware-packing.md:
- Around line 5-10: Convert the changelog-style bullets into present-tense
reference statements describing current pagination/overlay behavior: state that
Compose packs pages using a block’s flow height (flowHeightPx) with a fallback
to text height, and that heightPx/lineCount remain text-only; state that overlay
labels display "Page N of M" and include a persistent "Page 1 of M" marker;
state that labels are placed in the left margin gutter to remain visible when
the viewport narrows; and state that recompute runs in a layout effect (before
paint) so advisory lines render with the editor on hydrate—remove words like
"now", "moved", "instead of", and any changelog framing so the bullets read as
current-reference documentation.

In @.changeset/pagination-page-fixes.md:
- Line 5: Rewrite the sentence to describe the current behavior only (no
"Fix/previously/instead of" framing): state that the package now uses a literal
'pagination' key (rather than an unreleased KEYS.pagination), mounts the
registry provider and reflow coordinator in a shared subtree so reflow can read
registered pages, and renders the page number in each page's bottom margin —
phrased as present-state behavior and not as a fix or comparison.

In `@apps/www/next-env.d.ts`:
- Line 4: Replace the dev-only routes import in next-env.d.ts: change the import
string "./.next/dev/types/routes.d.ts" to the stable generated path
"./.next/types/routes.d.ts" so type resolution works outside `next dev`; update
the import statement in next-env.d.ts accordingly and run typecheck to verify no
other references to the dev path remain.

In `@docs/plans/2026-05-21-pagination-rewrite.md`:
- Around line 64-69: The fenced code block in the docs is unlabeled and triggers
markdownlint; update the fence by adding a language tag (e.g., "text") after the
opening backticks for the block containing the "Slate value → snapshot..."
pipeline so the block is fenced as ```text (or another appropriate language) to
satisfy the linter and keep the docs lint-clean.

In `@packages/pagination/src/layout/__tests__/compose-chrome.spec.ts`:
- Around line 126-132: The test currently allows undefined chrome to pass
because it compares values with optional chaining and non-null assertions;
before the for-loop that iterates out.pages and checks p.chrome.header/footer
against first, add explicit assertions that the initial chrome exists (const
first = out.pages[0]!.chrome) and that first.header and first.footer are defined
(e.g., expect(first).toBeDefined(); expect(first.header).toBeDefined();
expect(first.footer).toBeDefined()) so the loop cannot silently compare
undefined values for chrome/header/footer across pages (references: variable
first, out.pages, chrome, header, footer in compose-chrome.spec.ts).

In `@packages/pagination/src/layout/compose.ts`:
- Around line 28-31: Add a first-class JSDoc block directly above the exported
function declaration composeLayout describing its purpose (what layout it
composes), its parameters (snapshot: MeasuredSnapshot — what snapshot contains
and expectations; input: LayoutInput — describe key fields used), and its return
value (LayoutOutput — what it represents). Include `@param` tags for snapshot and
input, an `@returns` tag describing the LayoutOutput shape/meaning, and a short
example or usage note if helpful; keep wording concise and agent-friendly so the
signature and intent are immediately discoverable.
- Around line 38-50: The computed content rect can be non-positive when
margins/chrome consume the whole page (variables contentWidthPx, bounds,
frameHeight computed from page, margins, headerHeightPx, footerHeightPx), so
validate contentWidthPx > 0 and bounds.height > 0 before proceeding; if either
is non-positive, return/abort composition for that page (or throw a clear error)
instead of continuing to build frames with invalid geometry, and add a unit test
for compose behavior when margins + chrome exceed page size to cover this
branch.

In `@packages/pagination/src/layout/continuous.ts`:
- Around line 69-71: The loop that builds next-page anchors uses
layout.pages[i].frames[0]?.fragments[0] which misses cases where frames[0] is
empty; update the logic in continuous.ts to scan frames for the first frame with
fragments (e.g., find the first frame in layout.pages[i].frames where
frame.fragments.length>0) and use its fragments[0] as the anchor instead of
assuming frames[0]; adjust the const first assignment and retain the existing
continue behavior when no non-empty frame is found so downstream code using
first still works.

In `@packages/pagination/src/layout/snapshot.ts`:
- Around line 46-50: stableId currently returns the same fallback for distinct
nodes with identical type and text, causing ID collisions; update stableId to
include a per-node unique component (e.g., use an existing unique property such
as node.key/_key if present, or generate and persist a stable id on the node
object) in addition to `${node.type ?? 'node'}#${hash(nodeText(node))}` so two
different nodes with the same content still get distinct IDs; apply the same
change to the other fallback usage around the 59-63 block and ensure you use
nodeText and hash together with the per-node unique suffix to maintain
determinism and cacheability.

In `@packages/pagination/src/measure/measure.ts`:
- Around line 68-76: The cache key only uses block.id and widthPx which allows
stale metrics when a block's text changes; update the key generation (the
cacheKey used with cache.get/cache.set) to incorporate a content fingerprint
from the block (e.g., block.text, block.content, or a short hash of the block's
relevant layout-affecting fields) so any edit changes the cache key; keep using
measure(block) and cache.get/cache.set but compute cacheKey =
`${block.id}@${widthPx}@${contentFingerprint}` (or similar) before the lookup to
ensure edited text never returns stale BlockMetrics.

In `@packages/pagination/src/react/alignContent.ts`:
- Around line 30-46: The current code sets a page-start spacer using
first.path[0] for every page, which incorrectly applies spacing when the page's
first fragment is a continuation of a block from the previous page; change the
logic in the loop that computes prevBottom and calls spacers.set so it first
checks whether the current first fragment is a continuation (compare
first.path[0] to the previous page's last fragment path, e.g.
prev.fragments[prev.fragments.length - 1].path[0]) and skip setting a spacer if
they match, and also clamp the computed spacer value to be non-negative (use
Math.max(0, computedValue)) before calling spacers.set (references: first, prev,
prevBottom, spacers.set, first.path[0], contentHeight, input.margins, gapPx).

In `@packages/pagination/src/react/PaginationPlugin.tsx`:
- Around line 349-356: The ResizeObserver callback currently triggers
invalidateLayoutRegistry(editor) and forceRecompute on any size change, causing
redundant recomputes for height-only edits; update the useEffect containing
useEffect, ResizeObserver, invalidateLayoutRegistry, and forceRecompute to
compare the observed width before invoking work: track the previous width (e.g.,
in a ref or closure), read entries[0].contentRect.width inside the observer, and
only call invalidateLayoutRegistry(editor) and forceRecompute when the new width
differs from the previous width (then update the stored width); leave the
existing editor.api.toDOMNode(editor) and observer lifecycle unchanged.

In `@plans/2026-05-15-fix-pagination-plugin-tdd-v2.md`:
- Line 1: This planning doc file is in the wrong directory; move the file named
plans/2026-05-15-fix-pagination-plugin-tdd-v2.md into the docs/plans/ directory
so it follows the repository guideline that planning files live under
docs/plans/ (keep the existing filename/date-based naming intact). Ensure any
references or links to plans/2026-05-15-fix-pagination-plugin-tdd-v2.md in
README, index, or other docs are updated to point to
docs/plans/2026-05-15-fix-pagination-plugin-tdd-v2.md.

In `@plans/2026-05-15-fix-pagination-plugin-v1.md`:
- Line 1: This planning doc is in the wrong folder; move the file
"2026-05-15-fix-pagination-plugin-v1.md" from plans/ into docs/plans/ and rename
it to follow the repo convention (either prefix with the issue/ticket number if
this work is issue-backed or keep the date-first name under docs/plans/ if it’s
non-ticket work); after moving, update any references or links in the repo that
point to plans/2026-05-15-fix-pagination-plugin-v1.md so they now point to
docs/plans/<new-name>.md.

In `@templates/plate-playground-template/package.json`:
- Line 11: The change is a manual edit to a CI-controlled template manifest (the
npm script "vendor:pagination" added to the template package.json); revert this
manual edit in the template and instead update the upstream source that
generates templates (the original package/workflow/registry that supplies this
script) so CI can regenerate the template; search for and avoid direct edits of
the same script entries in other template locations referenced (the other
template package.json script entries corresponding to lines 61, 101, 108-109)
and make the fix in the source package or workflow inputs that produce the
template so the CI regeneration will include the intended change.

In `@templates/plate-playground-template/vendor/platejs-pagination/package.json`:
- Around line 3-18: This change incorrectly edits a CI-controlled template
manifest
(templates/plate-playground-template/vendor/platejs-pagination/package.json); do
not modify package.json fields like "version", "exports", "main", or
"dependencies" directly in the template. Instead, update the upstream source
(registry/package or workflow inputs) that generates this template, then trigger
the CI regeneration pipeline so the manifest is rebuilt; revert this manual edit
and rely on the CI output to apply any intended package/version changes.

---

Outside diff comments:
In `@plans/2026-05-16-pagination-end-to-end-fix-v1.md`:
- Around line 1-149: Move and rewrite the plan as a current-state guidance
document: relocate the content from
plans/2026-05-16-pagination-end-to-end-fix-v1.md into docs/plans/ (e.g.,
docs/plans/pagination-end-to-end-fix.md) and remove changelog/history phrasing
(PR `#4830` context, Sage Research Findings Summary as comparative "found vs.
vendor" narrative); convert sections like "Objective", "Implementation Plan",
"Phase 1/2/3/4", and "Verification Criteria" into present-tense actionable
guidance only (describe the required fixes such as updating
pagination-toolbar-button.tsx to read documentSettings.sizes, header/footer
detection inside page children, removing the WIP comment in editor-kit.tsx,
fixing toolbar cast to use PaginationTransforms, and vendor/regeneration/deploy
steps) and delete retrospective items (e.g., "Key findings" table or PR closure
notes) so the file contains only the latest-state instructions and verification
checklist.

---

Nitpick comments:
In `@docs/plans/2026-05-23-pagination-impl-plan.md`:
- Around line 13-35: Replace the historical "0. Audit triage — what the scorch
already fixed vs what remains" section with a present-tense, state-first
architecture/requirements summary: remove delta/changelog phrasing and keep only
current issues and targets (e.g., P0–P10 titles as current problems), describing
each finding as the present-state gap and the desired requirement or acceptance
criteria; update evidence pointers to the current code symbols (snapshot.ts:84 →
reference snapshot.ts and nodeText, pretext.ts:34, mapping.ts:18, measure.ts:55,
types.ts, projection.ts:36/66, domMeasure.ts:32/46/69, alignContent.ts:18,
compose.ts:63) and drop any "fixed vs remains" language so the table/list reads
as current deficiencies and next-step goals for the architecture.

In `@docs/plans/premirror-audit-findings.md`:
- Around line 3-7: Rewrite the "## Summary" intro to be a current-state
reference: remove comparative/changelog language about "Premirror vs we" and
instead state the present architecture and contracts directly (e.g., describe
that the system operates at top-level block granularity, explain that text
extraction and line-breaking are done per-block with estimated line counts
rather than per-character/styled-segment measurement). Explicitly list current
missing features as present-state limitations — lack of bidirectional position
mapping, lack of dirty-range incremental invalidation, and absence of
decoration-projection rendering — and phrase them as requirements or fidelity
gaps to address, not as contrasts to Premirror.

In `@package.json`:
- Line 56: The deploy:playground npm script currently skips local verification;
update the "deploy:playground" script so it first runs the template checks by
changing it to run typecheck, lint:fix and build for the playground template
before the existing steps (e.g. prepend: cd templates/plate-playground-template
&& bun run typecheck && bun run lint:fix && bun run build && cd -), then
continue with the existing vendor/install/build/deploy sequence; target the
"deploy:playground" script name so the template is verified locally before
packaging and deployment.

In `@packages/pagination/failures.md`:
- Around line 3-7: The failures.md document currently uses an "append-only audit
log" voice and must be rewritten as a current-state reference: remove phrases
like "appended", "never rewrites prior findings", and any changelog-style
narrative; replace with a clear "Current known failures / limitations" heading
and a succinct, up-to-date list of issues with status, impact, and recommended
remediation for each item; ensure compliance with the repository guideline for
"*.md" docs to present the LATEST state only and keep historical changelog
entries out of this file (move any chronological audit notes to a separate
CHANGELOG or audit-log file if needed).

In `@packages/pagination/src/layout/mapping.ts`:
- Around line 18-20: The fragmentsOfBlock accessor currently returns the live
array stored in byBlock, allowing external code to mutate internal state; change
fragmentsOfBlock (and any similar accessors) to return an immutable copy or
readonly view instead—e.g., return a shallow copy of byBlock[blockIndex] or
change the return type to readonly FragmentRef[]—so callers cannot modify the
internal byBlock array (refer to MappingIndex and the fragmentsOfBlock
implementation and any other accessors that expose byBlock).

In `@packages/pagination/src/measure/measure.ts`:
- Around line 60-64: Add a function-level JSDoc comment immediately above the
measureSnapshot declaration that documents the contract: describe the parameters
(snapshot: UnmeasuredSnapshot, measure: MeasureFn, options: MeasureOptions) and
return (MeasuredSnapshot), explain cache semantics (when results may be cached,
cache keys/ttl if applicable, and whether callers must treat returned snapshots
as immutable), specify fallback behavior (what happens if measure throws or
returns partial data and how errors are surfaced or defaulted), and enumerate
flow-height rules (how height/flow values are derived, validated, and adjusted,
including any invariants callers can rely on). Mention any side-effects or async
behaviour and any preconditions/postconditions callers must satisfy.

In `@packages/pagination/src/react/PaginationPlugin.tsx`:
- Around line 265-268: Add a top-level JSDoc block immediately above the
exported PaginationPlugin constant that documents the API surface and lifecycle:
describe the plugin itself (PaginationPlugin), its key options object properties
(enabled: boolean to toggle plugin, page: current page number, margins: page
margin spec, chrome: header/footer/chrome rendering toggle, breaks:
ContinuousBreak[] array controlling pagination break points), and
lifecycle/behavior notes (when options are read/updated, how setOption/useHooks
affect pagination state and render via render.afterEditable
PaginationBreakLines). Reference the exported symbol PaginationPlugin and
mention interaction with BasePaginationPlugin, render.afterEditable
(PaginationBreakLines), and useHooks for updating options so callers know when
to call setOption or reinitialize.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc21322a-b15f-4156-968b-e6c5776da84f

📥 Commits

Reviewing files that changed from the base of the PR and between 8a712e1 and ed4d253.

⛔ Files ignored due to path filters (12)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • templates/plate-playground-template/bun.lock is excluded by !**/*.lock
  • templates/plate-playground-template/vendor/platejs-pagination/dist/index-BmXRyAOt.d.ts is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/index-BmXRyAOt.d.ts.map is excluded by !**/dist/**, !**/*.map
  • templates/plate-playground-template/vendor/platejs-pagination/dist/index.d.ts is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/index.js is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/paginate-c73WStbw.js is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/paginate-c73WStbw.js.map is excluded by !**/dist/**, !**/*.map
  • templates/plate-playground-template/vendor/platejs-pagination/dist/react/index.d.ts is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/react/index.d.ts.map is excluded by !**/dist/**, !**/*.map
  • templates/plate-playground-template/vendor/platejs-pagination/dist/react/index.js is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/react/index.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (121)
  • .agents/AGENTS.md
  • .changeset/pagination-automount-runtime.md
  • .changeset/pagination-cache-key.md
  • .changeset/pagination-compose-place-whole.md
  • .changeset/pagination-continuous-breaks.md
  • .changeset/pagination-enabled-option.md
  • .changeset/pagination-mapping-in-output.md
  • .changeset/pagination-margin-aware-packing.md
  • .changeset/pagination-page-fixes.md
  • .changeset/pagination-pretext-measure-block.md
  • .changeset/pagination-pretext-measure.md
  • .changeset/pagination-react-continuous-overlay.md
  • .changeset/pagination-scorch-mutator.md
  • .changeset/pagination-snapshot-text.md
  • AGENTS.md
  • apps/www/next-env.d.ts
  • apps/www/src/app/dev/pagination2/page.tsx
  • apps/www/src/app/dev/pagination2/pagination2-view.tsx
  • bunfig.toml
  • diary.md
  • docs/plans/2026-05-15-pagination-plugin-refactor.md
  • docs/plans/2026-05-20-pagination-wiring.md
  • docs/plans/2026-05-21-pagination-rewrite.md
  • docs/plans/2026-05-22-pagination-rewrite-v2.md
  • docs/plans/2026-05-23-pagination-impl-plan.md
  • docs/plans/premirror-audit-findings.md
  • package.json
  • packages/pagination/README.md
  • packages/pagination/failures.md
  • packages/pagination/package.json
  • packages/pagination/src/index.ts
  • packages/pagination/src/layout/__tests__/compose-chrome-edge-cases.spec.ts
  • packages/pagination/src/layout/__tests__/compose-chrome.spec.ts
  • packages/pagination/src/layout/__tests__/compose.spec.ts
  • packages/pagination/src/layout/__tests__/continuous.spec.ts
  • packages/pagination/src/layout/__tests__/mapping.spec.ts
  • packages/pagination/src/layout/__tests__/projection.spec.ts
  • packages/pagination/src/layout/__tests__/snapshot.spec.ts
  • packages/pagination/src/layout/compose.ts
  • packages/pagination/src/layout/continuous.ts
  • packages/pagination/src/layout/index.ts
  • packages/pagination/src/layout/mapping.ts
  • packages/pagination/src/layout/projection.ts
  • packages/pagination/src/layout/snapshot.ts
  • packages/pagination/src/layout/types.ts
  • packages/pagination/src/lib/BasePaginationPlugin.ts
  • packages/pagination/src/lib/__tests__/BasePaginationPlugin.spec.ts
  • packages/pagination/src/lib/__tests__/registry.spec.ts
  • packages/pagination/src/lib/allocate-footnotes.ts
  • packages/pagination/src/lib/base-footer-plugin.ts
  • packages/pagination/src/lib/base-header-plugin.ts
  • packages/pagination/src/lib/base-page-break-plugin.ts
  • packages/pagination/src/lib/base-pagination-plugin.ts
  • packages/pagination/src/lib/base-pagination-plugins.spec.ts
  • packages/pagination/src/lib/index.ts
  • packages/pagination/src/lib/internal/font-from-style.ts
  • packages/pagination/src/lib/internal/keys.ts
  • packages/pagination/src/lib/internal/marks-fingerprint.ts
  • packages/pagination/src/lib/internal/measure-cache.spec.ts
  • packages/pagination/src/lib/internal/measure-cache.ts
  • packages/pagination/src/lib/internal/page-size-presets.ts
  • packages/pagination/src/lib/internal/page-state.ts
  • packages/pagination/src/lib/paginate.spec.ts
  • packages/pagination/src/lib/paginate.ts
  • packages/pagination/src/lib/queries/getPageOfPath.ts
  • packages/pagination/src/lib/queries/getPaginationPages.ts
  • packages/pagination/src/lib/queries/hasChromeBlock.ts
  • packages/pagination/src/lib/queries/index.ts
  • packages/pagination/src/lib/registry.ts
  • packages/pagination/src/lib/transforms/enforceHeaderFooterInvariants.ts
  • packages/pagination/src/lib/transforms/ensureFooter.ts
  • packages/pagination/src/lib/transforms/ensureHeader.ts
  • packages/pagination/src/lib/transforms/index.ts
  • packages/pagination/src/lib/transforms/insertPageBreak.ts
  • packages/pagination/src/lib/transforms/removeNodesByType.ts
  • packages/pagination/src/lib/transforms/replaceFooter.ts
  • packages/pagination/src/lib/transforms/replaceHeader.ts
  • packages/pagination/src/lib/transforms/toggleFooter.ts
  • packages/pagination/src/lib/transforms/toggleHeader.ts
  • packages/pagination/src/lib/types.ts
  • packages/pagination/src/measure/__tests__/measure.spec.ts
  • packages/pagination/src/measure/__tests__/pretext.spec.ts
  • packages/pagination/src/measure/index.ts
  • packages/pagination/src/measure/measure.ts
  • packages/pagination/src/measure/pretext.ts
  • packages/pagination/src/react/PaginationPlugin.tsx
  • packages/pagination/src/react/__tests__/geometry.spec.ts
  • packages/pagination/src/react/alignContent.ts
  • packages/pagination/src/react/chrome/PageNumber.tsx
  • packages/pagination/src/react/domMeasure.ts
  • packages/pagination/src/react/footer-plugin.ts
  • packages/pagination/src/react/footnote-portal.tsx
  • packages/pagination/src/react/geometry.ts
  • packages/pagination/src/react/header-plugin.ts
  • packages/pagination/src/react/index.ts
  • packages/pagination/src/react/internal/use-page-layout.ts
  • packages/pagination/src/react/page-break-plugin.ts
  • packages/pagination/src/react/page-frame.tsx
  • packages/pagination/src/react/page-overlay.tsx
  • packages/pagination/src/react/pagination-plugin.ts
  • packages/pagination/src/react/use-pretext-measurer.ts
  • packages/pagination/src/static/footer-element-static.tsx
  • packages/pagination/src/static/header-element-static.tsx
  • packages/pagination/src/static/index.ts
  • packages/pagination/src/static/page-break-element-static.tsx
  • packages/pagination/tsconfig.json
  • plans/2026-05-15-fix-pagination-plugin-tdd-v2.md
  • plans/2026-05-15-fix-pagination-plugin-v1.md
  • plans/2026-05-16-pagination-end-to-end-fix-v1.md
  • templates/plate-playground-template/package.json
  • templates/plate-playground-template/src/app/dev/pagination2/page.tsx
  • templates/plate-playground-template/src/app/dev/pagination2/pagination2-view.tsx
  • templates/plate-playground-template/src/app/editor/page.tsx
  • templates/plate-playground-template/src/components/editor/editor-kit.tsx
  • templates/plate-playground-template/src/components/editor/plugins/pagination-kit.tsx
  • templates/plate-playground-template/src/components/ui/fixed-toolbar-buttons.tsx
  • templates/plate-playground-template/src/components/ui/pagination-toolbar-button.tsx
  • templates/plate-playground-template/vendor/.gitignore
  • templates/plate-playground-template/vendor/platejs-pagination/package.json
  • tooling/config/tsdown.config.ts
  • tooling/scripts/brl.sh
💤 Files with no reviewable changes (45)
  • packages/pagination/src/react/page-break-plugin.ts
  • packages/pagination/src/lib/transforms/insertPageBreak.ts
  • packages/pagination/src/lib/allocate-footnotes.ts
  • packages/pagination/src/lib/transforms/ensureFooter.ts
  • packages/pagination/src/lib/queries/hasChromeBlock.ts
  • packages/pagination/src/lib/base-header-plugin.ts
  • packages/pagination/src/react/page-frame.tsx
  • packages/pagination/src/lib/paginate.spec.ts
  • packages/pagination/src/lib/base-pagination-plugins.spec.ts
  • packages/pagination/src/react/use-pretext-measurer.ts
  • packages/pagination/src/react/internal/use-page-layout.ts
  • packages/pagination/src/lib/internal/font-from-style.ts
  • packages/pagination/src/lib/base-page-break-plugin.ts
  • packages/pagination/src/lib/transforms/replaceHeader.ts
  • packages/pagination/src/lib/internal/page-state.ts
  • packages/pagination/src/lib/paginate.ts
  • packages/pagination/src/lib/internal/measure-cache.ts
  • packages/pagination/src/react/pagination-plugin.ts
  • templates/plate-playground-template/vendor/.gitignore
  • packages/pagination/src/static/header-element-static.tsx
  • packages/pagination/src/lib/transforms/removeNodesByType.ts
  • packages/pagination/src/react/footnote-portal.tsx
  • packages/pagination/README.md
  • packages/pagination/src/static/footer-element-static.tsx
  • packages/pagination/src/lib/transforms/replaceFooter.ts
  • packages/pagination/src/lib/base-footer-plugin.ts
  • packages/pagination/src/react/page-overlay.tsx
  • packages/pagination/src/lib/transforms/ensureHeader.ts
  • packages/pagination/src/lib/types.ts
  • packages/pagination/src/react/footer-plugin.ts
  • packages/pagination/src/static/page-break-element-static.tsx
  • packages/pagination/src/lib/queries/getPaginationPages.ts
  • packages/pagination/src/lib/internal/marks-fingerprint.ts
  • packages/pagination/src/lib/queries/index.ts
  • packages/pagination/src/lib/internal/keys.ts
  • packages/pagination/src/lib/internal/measure-cache.spec.ts
  • packages/pagination/src/lib/transforms/enforceHeaderFooterInvariants.ts
  • packages/pagination/src/lib/internal/page-size-presets.ts
  • packages/pagination/src/lib/queries/getPageOfPath.ts
  • packages/pagination/src/lib/transforms/toggleFooter.ts
  • packages/pagination/src/lib/base-pagination-plugin.ts
  • packages/pagination/src/react/header-plugin.ts
  • packages/pagination/src/lib/transforms/index.ts
  • packages/pagination/src/lib/transforms/toggleHeader.ts
  • packages/pagination/src/static/index.ts

Comment thread .changeset/pagination-automount-runtime.md Outdated
Comment thread .changeset/pagination-cache-key.md Outdated
Comment thread .changeset/pagination-continuous-breaks.md Outdated
Comment thread .changeset/pagination-mapping-in-output.md Outdated
Comment thread .changeset/pagination-margin-aware-packing.md Outdated
Comment thread packages/pagination/src/react/PaginationPlugin.tsx
@@ -0,0 +1,345 @@
# Fix Pagination Plugin — TDD Implementation Plan (100% Coverage)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Move this planning doc under docs/plans/.

Planning files must live in docs/plans/ (date-based naming is already good; directory is the issue).

As per coding guidelines: docs/plans/**: “Planning files should be located under docs/plans/ with issue-backed work using ticket-number-first naming ... and non-ticket work using date-based naming ...”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plans/2026-05-15-fix-pagination-plugin-tdd-v2.md` at line 1, This planning
doc file is in the wrong directory; move the file named
plans/2026-05-15-fix-pagination-plugin-tdd-v2.md into the docs/plans/ directory
so it follows the repository guideline that planning files live under
docs/plans/ (keep the existing filename/date-based naming intact). Ensure any
references or links to plans/2026-05-15-fix-pagination-plugin-tdd-v2.md in
README, index, or other docs are updated to point to
docs/plans/2026-05-15-fix-pagination-plugin-tdd-v2.md.

@@ -0,0 +1,114 @@
# Fix Pagination Plugin — Make It Usable End-to-End

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Move this planning doc under docs/plans/.

This is a planning file but it’s currently stored under plans/ instead of docs/plans/.

As per coding guidelines: docs/plans/**: “Planning files should be located under docs/plans/ with issue-backed work using ticket-number-first naming ... and non-ticket work using date-based naming ...”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plans/2026-05-15-fix-pagination-plugin-v1.md` at line 1, This planning doc is
in the wrong folder; move the file "2026-05-15-fix-pagination-plugin-v1.md" from
plans/ into docs/plans/ and rename it to follow the repo convention (either
prefix with the issue/ticket number if this work is issue-backed or keep the
date-first name under docs/plans/ if it’s non-ticket work); after moving, update
any references or links in the repo that point to
plans/2026-05-15-fix-pagination-plugin-v1.md so they now point to
docs/plans/<new-name>.md.

"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"dev": "next dev",
"vendor:pagination": "rm -rf vendor/platejs-pagination/dist && cp -r ../../packages/pagination/dist vendor/platejs-pagination/dist",
"vendor:pagination": "rm -rf vendor/platejs-pagination/dist node_modules/@platejs/pagination/dist && cp -r ../../packages/pagination/dist vendor/platejs-pagination/dist && cp -r vendor/platejs-pagination/dist node_modules/@platejs/pagination/dist",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Do not commit direct edits to template manifests.

templates/** is CI-controlled output; templates/plate-playground-template/package.json changes should come from source registry/package/workflow inputs and be regenerated by CI to avoid template drift.

As per coding guidelines: "templates/**: Templates are CI-controlled output. Never manually edit or commit template source, manifests, or lockfiles. Fix the source registry, package, or workflow inputs and let CI regenerate templates."

Also applies to: 61-61, 101-101, 108-109

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@templates/plate-playground-template/package.json` at line 11, The change is a
manual edit to a CI-controlled template manifest (the npm script
"vendor:pagination" added to the template package.json); revert this manual edit
in the template and instead update the upstream source that generates templates
(the original package/workflow/registry that supplies this script) so CI can
regenerate the template; search for and avoid direct edits of the same script
entries in other template locations referenced (the other template package.json
script entries corresponding to lines 61, 101, 108-109) and make the fix in the
source package or workflow inputs that produce the template so the CI
regeneration will include the intended change.

Comment on lines +3 to 18
"version": "52.2.0",
"description": "Pagination plugin for Plate - page-based document layout",
"license": "MIT",
"sideEffects": false,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./react": "./dist/react/index.js",
"./package.json": "./package.json"
"./package.json": "./package.json",
"./react": "./dist/react/index.js"
},
"files": ["dist/**/*"],
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"dependencies": {
"@platejs/footnote": "^53.0.0",
"react-compiler-runtime": "^1.0.0"
},
"peerDependencies": {
"platejs": ">=53.0.0",
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
"@chenglou/pretext": "^0.0.6"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not edit template manifests directly.

This manifest is under templates/**; update the source registry/package/workflow inputs and regenerate via CI rather than committing direct template-manifest edits.

As per coding guidelines, "Templates are CI-controlled output. Never manually edit or commit template source, manifests, or lockfiles. Fix the source registry, package, or workflow inputs and let CI regenerate templates."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@templates/plate-playground-template/vendor/platejs-pagination/package.json`
around lines 3 - 18, This change incorrectly edits a CI-controlled template
manifest
(templates/plate-playground-template/vendor/platejs-pagination/package.json); do
not modify package.json fields like "version", "exports", "main", or
"dependencies" directly in the template. Instead, update the upstream source
(registry/package or workflow inputs) that generates this template, then trigger
the CI regeneration pipeline so the manifest is rebuilt; revert this manual edit
and rely on the CI output to apply any intended package/version changes.

arthrod and others added 2 commits May 29, 2026 13:31
# 1. resolveLineHeight: handle unitless multipliers (CRITICAL)

`getComputedStyle().lineHeight` returns the unitless multiplier when CSS
declares `line-height: 1.5` (very common — Tailwind preflight,
reset.css, base browser styles). `parseFloat('1.5')` returned `1.5`,
which the code then treated as PIXELS — collapsing every block to a
~1.5px line height and breaking every downstream packing decision.

Heuristic: < 5 is a multiplier (scale by font-size), >= 5 is already
in pixels.

# 2. PaginationBreakLines: last-page footer geometry (HIGH)

`pageContentHeightPx` didn't subtract the header height. Since the
page-start-block's top already includes the header offset, the result
was `header.heightPx` too low — the last-page footer rendered past the
page's geometric bottom.

# 3. snapshot.stableId: accept non-string ids (MEDIUM)

A strict `typeof === 'string'` check forced every numerically-ided
block onto the content-hash fallback path. Plate consumers using
numeric ids would thrash the (id, width) measure cache on every edit
even though they ALREADY had a stable identity.

# 4. computePageStartSpacers: algebraic simplification (MEDIUM)

The previous expression was
  (page.heightPx - margin.top - margin.bottom - prevBottom)
    + margin.bottom + gap + margin.top
Margins cancel exactly. Simplified to:
  page.heightPx - prevBottom + gap
Still clamped to non-negative (PR #433 fix).

# Tests

`gemini-pr442-fixes.spec.ts` adds 5 unit tests for the snapshot
numeric-id path (which is testable without a DOM). The other three
fixes are exercised by the live dogfood overlay path; their proper
unit tests need a mock DOM and are deferred to a follow-up.

Suite: 80 / 80 ✓ (was 75).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Gemini Code Assist <noreply@github.com>
…ni/CR PR #442)

Plate's markdown coding guideline forbids changelog-style phrasing in
docs (`Add …`, `Fix …`, `now …`, `instead of …`, `moved …`). The 11
pagination changesets all leaned on that voice. CodeRabbit flagged 7
explicitly on PR #442; rewriting all 11 consistently keeps the corpus
uniform.

No content drift — each note still names the same API/behavior, just
described as current state rather than as a transition from a prior
state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: CodeRabbitAI <noreply@github.com>
@arthrod

arthrod commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@arthrod

arthrod commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/pagination/src/layout/snapshot.ts (1)

57-60: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add first-class JSDoc for exported API.

buildSnapshot is exported but lacks a function-level JSDoc contract (inputs/behavior/flags). Please add it for agent/human API discoverability.

As per coding guidelines: "**/*.{js,ts,tsx,jsx,mjs,mts,mtsx}: JSDoc must be first-class for agents in all API surfaces".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pagination/src/layout/snapshot.ts` around lines 57 - 60, Add a
first-class JSDoc block for the exported function buildSnapshot(value:
SlateNode[], options: SnapshotOptions): UnmeasuredSnapshot that documents the
parameters (describe shape/expectations of value and each option field/flags),
the returned UnmeasuredSnapshot structure, side effects/behavior (immutable
copy, measurement deferred, error conditions), and any defaults or important
invariants; place the JSDoc immediately above the buildSnapshot declaration so
tools/agents can ingest it as the public API contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/pagination/src/layout/snapshot.ts`:
- Around line 72-87: The code currently dedupes every ID by calling
uniqueId(stableId(node), index), which rewrites author-supplied IDs; change this
so deduplication only runs for generated/fallback IDs: compute const rawId =
stableId(node); if the node carries an explicit ID (e.g. node.id or whatever
property your nodes use for user-provided ids), use rawId as-is and add it to
seenIds without calling uniqueId; otherwise call uniqueId(rawId, index) to
produce a deduped fallback id. Update the block creation (where id is assigned)
to reflect this and ensure seenIds is still populated for both explicit and
generated ids.

---

Outside diff comments:
In `@packages/pagination/src/layout/snapshot.ts`:
- Around line 57-60: Add a first-class JSDoc block for the exported function
buildSnapshot(value: SlateNode[], options: SnapshotOptions): UnmeasuredSnapshot
that documents the parameters (describe shape/expectations of value and each
option field/flags), the returned UnmeasuredSnapshot structure, side
effects/behavior (immutable copy, measurement deferred, error conditions), and
any defaults or important invariants; place the JSDoc immediately above the
buildSnapshot declaration so tools/agents can ingest it as the public API
contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 08e66829-c121-449a-96cf-576c98b50b75

📥 Commits

Reviewing files that changed from the base of the PR and between ed4d253 and e807d0e.

📒 Files selected for processing (10)
  • packages/pagination/src/layout/__tests__/coderabbit-pr433-fixes.spec.ts
  • packages/pagination/src/layout/__tests__/coderabbit-pr438-fixes.spec.ts
  • packages/pagination/src/layout/__tests__/gemini-pr442-fixes.spec.ts
  • packages/pagination/src/layout/compose.ts
  • packages/pagination/src/layout/mapping.ts
  • packages/pagination/src/layout/snapshot.ts
  • packages/pagination/src/react/PaginationPlugin.tsx
  • packages/pagination/src/react/alignContent.ts
  • packages/pagination/src/react/domMeasure.ts
  • packages/pagination/src/react/geometry.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/pagination/src/react/geometry.ts
  • packages/pagination/src/react/alignContent.ts
  • packages/pagination/src/react/PaginationPlugin.tsx
  • packages/pagination/src/layout/mapping.ts
  • packages/pagination/src/react/domMeasure.ts
  • packages/pagination/src/layout/compose.ts

Comment thread packages/pagination/src/layout/snapshot.ts Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a complete rewrite of the @platejs/pagination package, transitioning it from a document-mutating model to a pure, derived-overlay projection driven by @chenglou/pretext for deterministic, line-accurate text measurement. The document model is never mutated or wrapped in page nodes. The rewrite introduces a clean pipeline: Slate value → snapshot → pretext measure → compose layout → geometry projection → React continuous/paged overlay rendering. It also updates the playground template to wire this new pagination plugin, including a simplified toolbar button to toggle page breaks, and adds comprehensive unit tests covering the pure layout, measurement, and mapping layers. No review comments were provided, so there is no additional feedback to address.

arthrod and others added 2 commits May 29, 2026 13:50
…JSDoc on buildSnapshot

# 1. Explicit-id pass-through (major)

The PR #438 dedupe naively ran every id (explicit + fallback) through
the collision-disambiguation path. Two blocks with the same
consumer-supplied `id: 'foo'` would get rewritten to `'foo'` and
`'foo@1'` — violating the contract that explicit ids stay untouched.

The fix: dedupe ONLY fallback ids. Explicit ids pass through verbatim
but ARE added to `seenIds` so a later fallback can't collide with
them (e.g. an explicit `'p#abc'` poisoning a fallback's namespace).

# 2. JSDoc for buildSnapshot

First-class JSDoc contract on the exported API per Plate coding
guidelines: inputs, outputs, the explicit-vs-fallback id behavior, the
keep-with-next / atomic / break-before flag wiring, all named with the
PR refs that gave them their current shape.

# Tests

`coderabbit-pr442-fixes.spec.ts` — 5 new tests:
  - duplicate explicit string ids remain untouched
  - duplicate explicit numeric ids remain untouched
  - explicit id reserves its name against later fallback collisions
  - three fallback duplicates still dedupe (PR #438 unchanged)
  - mix: explicit duplicates pass through, fallback duplicates dedupe

Suite: 85 / 85 ✓ (was 80).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: CodeRabbitAI <noreply@github.com>
…433 backlog)

Plate's markdown/TS guideline: "JSDoc must be first-class for agents in
all API surfaces". This sweep upgrades the five remaining exports that
either lacked JSDoc or carried a one-line summary insufficient for
agent + human discoverability:

- mapping.buildMappingIndex      — new JSDoc; documents the positional
                                   pageIndex invariant (PR #438) as the
                                   reason it's the only safe projection
- projection.fragmentRects       — multi-line; documents fragment-by-
                                   fragment iteration semantics, the
                                   mapping dependency, and the empty-
                                   result conditions
- projection.blockLinePosition   — multi-line; documents line-within-
                                   fragment math and the null contract
- alignContent.computePageStart  — new JSDoc; documents the simplified
   Spacers                         formula (Gemini PR #442) and what
                                   keys appear in the returned map
- geometry.getPageGeometry       — upgraded from one-liner; documents
                                   placement shape, gap semantics, and
                                   the total-{width,height} convention

No behavior change. Suite: 85/85 ✓.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: CodeRabbitAI <noreply@github.com>
@arthrod

arthrod commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

Iter-11 lands the JSDoc backlog you flagged on PR #433: buildMappingIndex, fragmentRects, blockLinePosition, computePageStartSpacers, getPageGeometry all now carry first-class JSDoc. buildSnapshot (iter-10) was the previous batch. composeLayout (iter-7) too.

Combined with iter-10's explicit-id pass-through fix, the open snapshot/dedupe contract should now be closed.

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

@arthrod Thanks for the summary! Triggering a review of Iter-11 now — will cover the JSDoc additions across buildMappingIndex, fragmentRects, blockLinePosition, computePageStartSpacers, and getPageGeometry, and verify the snapshot/dedupe contract is fully closed with the explicit-id pass-through fix from iter-10.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

arthrod and others added 3 commits May 29, 2026 14:05
…cklog)

`measureSnapshot` was the last pure-pipeline export without JSDoc.
Adding it documents the (id, width) cache invariant the prior cache-key
fix already introduced, the fallback contract when `measure` returns
null, and the flow-height composition formula
(`renderedHeightPx ?? heightPx + boxSpacingPx`).

`measureTextLines` and `measureBlockHeight` (pretext.ts) already carry
multi-line JSDoc — leaving as-is.

No behavior change. Suite: 85/85 ✓.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: CodeRabbitAI <noreply@github.com>
Two fixes wrapped in one script change:

1. `bun install --frozen-lockfile` — the prior script let `bun install`
   silently update `bun.lock`, drifting `@opennextjs/cloudflare` away
   from the committed `1.19.6` pin between deploys.

2. `bun x --no-install opennextjs-cloudflare …` — `npx`/`bunx` was
   allowed to fetch a newer (or older) version when the local binary
   was missing. `--no-install` requires the binary to exist in the
   project, which the prior `bun install --frozen-lockfile` step now
   guarantees.

Net: two consecutive deploys from the same commit produce the same
deploy artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: CodeRabbitAI <noreply@github.com>
# 1. measure.ts cache key includes content signature (major)

Cache was keyed by `id@width`. An EXPLICIT consumer-supplied id is
stable across edits, but the block's measured height isn't — the same
id re-measured after the text changed returned the OLD cached metrics,
producing wrong page breaks. Added a `type|text|keepWithNext|breakBefore|splittable`
signature to the key; the same edit changes the signature and evicts
the stale slot. Fallback-id consumers already got this for free (the
fallback hashes the text); now explicit-id consumers do too.

# 2. alignContent.ts skips continuation fragments (major)

A page that opens with a CONTINUATION fragment (fragmentIndex > 0) of
a top-level block whose earlier fragments live on the prior page
shares its block's DOM element with those earlier fragments. Applying
margin-top to that element would push the entire block — including
its prior-page slice — downward. Guard: skip the spacer when
`first.fragmentIndex > 0`.

# 3. PaginationPlugin.tsx ResizeObserver width-only (major)

Height-only changes (the natural by-product of every edit) were
triggering full recompute via ResizeObserver — a 60Hz pipeline cycle.
Track `lastWidth`, short-circuit when nextWidth === lastWidth. Reads
`ResizeObserverEntry.contentBoxSize[0].inlineSize` first for
precision, falls back to `clientWidth`.

# Tests

`coderabbit-pr442-batch2.spec.ts` — 7 new tests:
  - same id + different text re-measures (no stale cache hit)
  - same id + same text + same width stays cached (1 measure call)
  - same id + same text + different widths produces 2 cache entries
  - keepWithNext flag flip invalidates the cache slot
  - continuation fragment on page 2 gets no spacer
  - new block on page 2 still gets its spacer
  - happy-path composer output: every page starts a NEW block

Suite: 92 / 92 ✓ (was 85).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: CodeRabbitAI <noreply@github.com>
@arthrod

arthrod commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

Iter-13 lands three batch-2 fixes you raised at 11:27 — cache key includes content signature, page-start spacer skips continuation fragments, ResizeObserver short-circuits height-only changes. Test suite 92/92 ✓. Live deploy validated: 4 pages, headers + footers at expected geometry, no legacy chips, no console errors.

Stack now 9 commits (chrome → PR #438 → PR #433 → Gemini → changesets → CR PR #442 explicit-ids → JSDoc → measure + reproducibility → CR batch 2). Would appreciate a fresh pass on the synthesis.

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

@arthrod Thanks for the detailed Iter-13 summary! Triggering a full fresh review now — will cover all 9 commits in the stack with particular attention to:

  1. Cache key content signature — verifying the (id, widthPx, contentHash) triplet prevents stale hits across mutations
  2. Page-start spacer continuation skip — ensuring spacers are only applied to the first fragment of a page, not continuation fragments
  3. ResizeObserver height-only short-circuit — confirming the width-change guard avoids spurious recomputes

Will also give a holistic look at the chrome system, the MappingIndex / projection pipeline, and the snapshot/dedupe contract closure from iter-10/11.

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (1)
.changeset/pagination-page-fixes.md (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use pure current-state wording without release-comparison framing.

This sentence still reads partially as rollout context (“published …”, “unreleased …”). Prefer direct contract wording only.

Suggested rewrite
-Pagination works for consumers on published `platejs`: registration uses a literal `'pagination'` key (the `KEYS.pagination` constant is unreleased in `@platejs/utils`), the registry provider and reflow coordinator share one mounted subtree so reflow can read registered pages, and each page renders its page number in the bottom margin.
+Pagination registration uses the literal `'pagination'` key, the registry provider and reflow coordinator share a mounted subtree so reflow can read registered pages, and each page renders its page number in the bottom margin.
As per coding guidelines: “Docs are user-facing reference for the LATEST state only… no ‘what changed’ framing.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/pagination-page-fixes.md at line 5, The sentence mixes
release-comparison framing; rewrite it to describe the current contract only (no
"published" or "unreleased" language). Replace the phrase that says registration
uses a literal 'pagination' key and references KEYS.pagination with a neutral
statement such as: registration uses the 'pagination' key, the registry provider
and reflow coordinator share a mounted subtree so reflow can read registered
pages, and each page renders its page number in the bottom margin; ensure
KEYS.pagination and package-release status are not mentioned.
🧹 Nitpick comments (3)
apps/www/src/app/dev/pagination2/pagination2-view.tsx (1)

11-13: ⚡ Quick win

Inline single-use layout constants for consistency.

PAGE_W and MARGIN are used once each; inline them to match repo convention.

Proposed diff
-const PAGE_W = 794; // A4 @ 96dpi
-const MARGIN = 96; // 1in
@@
           margin: '0 auto',
-          padding: MARGIN,
+          padding: 96, // 1in
           position: 'relative',
-          width: PAGE_W,
+          width: 794, // A4 @ 96dpi
         }}
       >
As per coding guidelines: "`**/*.{ts,tsx,js,jsx}`: Prefer inline when used once; extract constants only when reused."

Also applies to: 60-63

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/www/src/app/dev/pagination2/pagination2-view.tsx` around lines 11 - 13,
PAGE_W and MARGIN are single-use layout constants; inline them where they are
referenced instead of declaring top-level constants. Replace usages of PAGE_W
with the literal 794 and MARGIN with the literal 96 in the component(s) that
reference them (also apply the same change for the similar constants at lines
60-63), remove the now-unused PAGE_W and MARGIN declarations, and run a quick
lint/type check to ensure no unused symbol warnings remain.
templates/plate-playground-template/src/components/ui/pagination-toolbar-button.tsx (1)

10-12: ⚡ Quick win

Add JSDoc for the exported toolbar API surface.

This exported component should include a short JSDoc block so intent/behavior is explicit for both humans and tooling.

💡 Suggested update
+/**
+ * Toolbar control that toggles pagination on the current editor instance.
+ * Composes consumer `onClick` and skips toggle when `event.defaultPrevented`.
+ */
 export function PaginationToolbarButton(
   props: React.ComponentProps<typeof ToolbarButton>
 ) {
As per coding guidelines, "`**/*.{ts,tsx,js,jsx}`: Optimize for the absolute best developer experience. JSDoc must be first-class for agents. Every API surface should be intuitive for both humans and AI agents."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@templates/plate-playground-template/src/components/ui/pagination-toolbar-button.tsx`
around lines 10 - 12, Add a JSDoc block immediately above the exported
PaginationToolbarButton function that documents the component's purpose, its
props (React.ComponentProps<typeof ToolbarButton>), the return value
(React.ReactElement | null), and any important behavior/usage notes (e.g., that
it proxies ToolbarButton props and is part of the pagination toolbar API).
Ensure the JSDoc uses `@param` for props and `@returns`, mentions that this is an
exported component, and keeps the description concise to help humans and tooling
locate intent.
packages/pagination/failures.md (1)

1-2097: 💤 Low value

Optional: Consider moving audit to docs/plans/ for discoverability.

This is well-structured audit documentation. The package-local placement is acceptable, but per repo learnings, findings/audit documents typically land in docs/plans/ for cross-team visibility and to avoid package-root clutter.

Based on learnings: "Do not create task_plan.md, findings.md, or progress.md at repo root... Merge that content into one file under docs/plans/."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pagination/failures.md` around lines 1 - 2097, Move the audit file
failures.md out of the package root into the shared docs folder (create
docs/plans/ if missing) and update any references/imports/README links that
point to packages/pagination/failures.md to the new
docs/plans/pagination-failures.md location; ensure repository index files
(README, docs sidebar, or any CI docs checks) reference the new path and add a
short note in the package README pointing to docs/plans/pagination-failures.md
so discoverability is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/plans/2026-05-21-pagination-rewrite.md`:
- Around line 45-50: The "Recent fixes already landed (PR `#405` / `#406` —
folder-pick lineage)" header and its bullet list use changelog/timeline
language; rewrite them into present‑state documentation voice by describing
current behavior/state (e.g., "KEYS.pagination is available",
"provider+coordinator share a subtree", "page number appears in bottom margin",
"Template relies on package auto-mount and drops TrailingBlock conflict") and
remove temporal verbs/phrases like "recent", "has been", "now", "previously";
apply the same present‑state rewrite to the other nearby bullet block in the
same section so the docs follow the docs/**/*.md guideline against
changelog-style language.

In `@docs/plans/2026-05-22-pagination-rewrite-v2.md`:
- Around line 3-5: Remove all changelog-style framing and transitional status
language from the plan (e.g., the opening "Supersedes
`2026-05-21-pagination-rewrite.md`" sentence and any lines containing words like
"current", "DONE & green", "Awaiting" referenced in the comment), and rewrite
those sentences so the document reads as a single authoritative specification
describing the target/current behavior and decisions (no history or
prior-version notes). Update the affected sections (around the top block, lines
noted near 41-45, line ~77, and lines ~138-141) so each paragraph states the
intended design, measurement approach (full pretext line/run measurement), and
the removed document-mutator path as facts rather than a change log or
transitional status.

In `@docs/plans/2026-05-23-pagination-impl-plan.md`:
- Around line 13-35: Rewrite the "Audit triage" section (the table and
surrounding text between the header and conclusion) into a present-state
reference: replace changelog phrases like "predates", "re-scored", "still",
"deleted", "gone", "has been removed", "new feature", "previously", "now
supports" with neutral, present-tense descriptions of current status for each
finding (e.g., "Status: OPEN — mapping is block/line only" instead of "OPEN —
mapping is block/line only."). Keep the same findings and evidence pointers
(e.g., references to premirror-audit-findings.md, snapshot.ts:84, pretext.ts:34,
mapping.ts:18, measure.ts:55, types.ts, projection.ts:36,66,
domMeasure.ts:32,46,69, alignContent.ts:18) but present them as current-state
facts; remove any timeline/changelog language in lines 15–34 and ensure the
conclusion remains a present-state summary.

In `@packages/pagination/src/layout/__tests__/compose.spec.ts`:
- Around line 17-21: The test block factory is creating path using the
incremented nextId, producing path: [1] for the first block; change the factory
so path reflects the block's actual numeric id (e.g., capture the current id
before incrementing or compute path from id) — use the same base value used to
form id (`nextId`) rather than the post-incremented value (for example, compute
a local currentId = nextId, then set id = `b${currentId}`, increment nextId, and
set path: [currentId] or otherwise set path: [nextId - 1]) so path aligns with
id; adjust references in the factory that use nextId, id, and path accordingly.

In `@packages/pagination/src/layout/mapping.ts`:
- Around line 50-104: The pagination code in buildMappingIndex currently maps
blockIndex→fragments (functions: buildMappingIndex, fragmentsOfBlock,
fragmentOfBlockLine, isSplit, pageOfBlockLine), which violates the requirement
to either use pretext-driven run/leaf measurement or to provide a formal
attestation/plan. Fix by doing one of two clear actions: either add a solemn
attestation (documented at the top of this file and in the PR description) that
pretext is intentionally not useful here with concrete technical reasons (why
pretext cannot be applied to this layout, what measurements are impossible, and
why block-level fragments are sufficient), or replace the block-granular
approach with a pretext roadmap and timeline plus scoped implementation plan to
move mapping from blockIndex→fragments to pmPos↔layout (update buildMappingIndex
to produce per-run/pmPos refs and change
fragmentsOfBlock/fragmentOfBlockLine/isSplit/pageOfBlockLine semantics
accordingly). Ensure the chosen option is explicit in code comments and the PR
so reviewers can verify compliance.

In `@packages/pagination/src/layout/snapshot.ts`:
- Around line 80-85: The function buildSnapshot currently dereferences
options.atomicTypes and options.keepWithNextTypes which will throw if callers
omit the options argument; update buildSnapshot to default its options parameter
to an empty object (e.g., options: SnapshotOptions = {} or assign a local const
opts = options ?? {}) and then use opts.atomicTypes and opts.keepWithNextTypes
when constructing the atomic and keepWithNext sets so buildSnapshot,
SnapshotOptions, atomicTypes, keepWithNextTypes and UnmeasuredSnapshot are safe
for JS callers who omit the second argument.

In `@packages/pagination/src/measure/__tests__/pretext.spec.ts`:
- Around line 13-16: Save the original globalThis.OffscreenCanvas before
assigning the test stub (e.g. const _origOffscreenCanvas =
globalThis.OffscreenCanvas), then set globalThis.OffscreenCanvas =
StubOffscreenCanvas for the suite, and restore the original in teardown (use
afterAll or afterEach) by reassigning globalThis.OffscreenCanvas =
_origOffscreenCanvas or deleting it when _origOffscreenCanvas is undefined;
reference the StubOffscreenCanvas assignment near the
measureBlockHeight/measureTextLines tests and ensure restoration happens even on
failures.

---

Duplicate comments:
In @.changeset/pagination-page-fixes.md:
- Line 5: The sentence mixes release-comparison framing; rewrite it to describe
the current contract only (no "published" or "unreleased" language). Replace the
phrase that says registration uses a literal 'pagination' key and references
KEYS.pagination with a neutral statement such as: registration uses the
'pagination' key, the registry provider and reflow coordinator share a mounted
subtree so reflow can read registered pages, and each page renders its page
number in the bottom margin; ensure KEYS.pagination and package-release status
are not mentioned.

---

Nitpick comments:
In `@apps/www/src/app/dev/pagination2/pagination2-view.tsx`:
- Around line 11-13: PAGE_W and MARGIN are single-use layout constants; inline
them where they are referenced instead of declaring top-level constants. Replace
usages of PAGE_W with the literal 794 and MARGIN with the literal 96 in the
component(s) that reference them (also apply the same change for the similar
constants at lines 60-63), remove the now-unused PAGE_W and MARGIN declarations,
and run a quick lint/type check to ensure no unused symbol warnings remain.

In `@packages/pagination/failures.md`:
- Around line 1-2097: Move the audit file failures.md out of the package root
into the shared docs folder (create docs/plans/ if missing) and update any
references/imports/README links that point to packages/pagination/failures.md to
the new docs/plans/pagination-failures.md location; ensure repository index
files (README, docs sidebar, or any CI docs checks) reference the new path and
add a short note in the package README pointing to
docs/plans/pagination-failures.md so discoverability is preserved.

In
`@templates/plate-playground-template/src/components/ui/pagination-toolbar-button.tsx`:
- Around line 10-12: Add a JSDoc block immediately above the exported
PaginationToolbarButton function that documents the component's purpose, its
props (React.ComponentProps<typeof ToolbarButton>), the return value
(React.ReactElement | null), and any important behavior/usage notes (e.g., that
it proxies ToolbarButton props and is part of the pagination toolbar API).
Ensure the JSDoc uses `@param` for props and `@returns`, mentions that this is an
exported component, and keeps the description concise to help humans and tooling
locate intent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8cde710a-6be4-45b8-a550-bb0381553dbf

📥 Commits

Reviewing files that changed from the base of the PR and between 8a712e1 and ea13503.

⛔ Files ignored due to path filters (12)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • templates/plate-playground-template/bun.lock is excluded by !**/*.lock
  • templates/plate-playground-template/vendor/platejs-pagination/dist/index-BmXRyAOt.d.ts is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/index-BmXRyAOt.d.ts.map is excluded by !**/dist/**, !**/*.map
  • templates/plate-playground-template/vendor/platejs-pagination/dist/index.d.ts is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/index.js is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/paginate-c73WStbw.js is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/paginate-c73WStbw.js.map is excluded by !**/dist/**, !**/*.map
  • templates/plate-playground-template/vendor/platejs-pagination/dist/react/index.d.ts is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/react/index.d.ts.map is excluded by !**/dist/**, !**/*.map
  • templates/plate-playground-template/vendor/platejs-pagination/dist/react/index.js is excluded by !**/dist/**
  • templates/plate-playground-template/vendor/platejs-pagination/dist/react/index.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (127)
  • .agents/AGENTS.md
  • .changeset/pagination-automount-runtime.md
  • .changeset/pagination-cache-key.md
  • .changeset/pagination-compose-place-whole.md
  • .changeset/pagination-continuous-breaks.md
  • .changeset/pagination-enabled-option.md
  • .changeset/pagination-mapping-in-output.md
  • .changeset/pagination-margin-aware-packing.md
  • .changeset/pagination-page-fixes.md
  • .changeset/pagination-pretext-measure-block.md
  • .changeset/pagination-pretext-measure.md
  • .changeset/pagination-react-continuous-overlay.md
  • .changeset/pagination-scaffold.md
  • .changeset/pagination-scorch-mutator.md
  • .changeset/pagination-snapshot-text.md
  • AGENTS.md
  • apps/www/next-env.d.ts
  • apps/www/src/app/dev/pagination2/page.tsx
  • apps/www/src/app/dev/pagination2/pagination2-view.tsx
  • bunfig.toml
  • diary.md
  • docs/plans/2026-05-15-pagination-plugin-refactor.md
  • docs/plans/2026-05-20-pagination-wiring.md
  • docs/plans/2026-05-21-pagination-rewrite.md
  • docs/plans/2026-05-22-pagination-rewrite-v2.md
  • docs/plans/2026-05-23-pagination-impl-plan.md
  • docs/plans/premirror-audit-findings.md
  • package.json
  • packages/pagination/README.md
  • packages/pagination/failures.md
  • packages/pagination/package.json
  • packages/pagination/src/index.ts
  • packages/pagination/src/layout/__tests__/coderabbit-pr433-fixes.spec.ts
  • packages/pagination/src/layout/__tests__/coderabbit-pr438-fixes.spec.ts
  • packages/pagination/src/layout/__tests__/coderabbit-pr442-batch2.spec.ts
  • packages/pagination/src/layout/__tests__/coderabbit-pr442-fixes.spec.ts
  • packages/pagination/src/layout/__tests__/compose-chrome-edge-cases.spec.ts
  • packages/pagination/src/layout/__tests__/compose-chrome.spec.ts
  • packages/pagination/src/layout/__tests__/compose.spec.ts
  • packages/pagination/src/layout/__tests__/continuous.spec.ts
  • packages/pagination/src/layout/__tests__/gemini-pr442-fixes.spec.ts
  • packages/pagination/src/layout/__tests__/mapping.spec.ts
  • packages/pagination/src/layout/__tests__/projection.spec.ts
  • packages/pagination/src/layout/__tests__/snapshot.spec.ts
  • packages/pagination/src/layout/compose.ts
  • packages/pagination/src/layout/continuous.ts
  • packages/pagination/src/layout/index.ts
  • packages/pagination/src/layout/mapping.ts
  • packages/pagination/src/layout/projection.ts
  • packages/pagination/src/layout/snapshot.ts
  • packages/pagination/src/layout/types.ts
  • packages/pagination/src/lib/BasePaginationPlugin.ts
  • packages/pagination/src/lib/__tests__/BasePaginationPlugin.spec.ts
  • packages/pagination/src/lib/__tests__/registry.spec.ts
  • packages/pagination/src/lib/allocate-footnotes.ts
  • packages/pagination/src/lib/base-footer-plugin.ts
  • packages/pagination/src/lib/base-header-plugin.ts
  • packages/pagination/src/lib/base-page-break-plugin.ts
  • packages/pagination/src/lib/base-pagination-plugin.ts
  • packages/pagination/src/lib/base-pagination-plugins.spec.ts
  • packages/pagination/src/lib/index.ts
  • packages/pagination/src/lib/internal/font-from-style.ts
  • packages/pagination/src/lib/internal/keys.ts
  • packages/pagination/src/lib/internal/marks-fingerprint.ts
  • packages/pagination/src/lib/internal/measure-cache.spec.ts
  • packages/pagination/src/lib/internal/measure-cache.ts
  • packages/pagination/src/lib/internal/page-size-presets.ts
  • packages/pagination/src/lib/internal/page-state.ts
  • packages/pagination/src/lib/paginate.spec.ts
  • packages/pagination/src/lib/paginate.ts
  • packages/pagination/src/lib/queries/getPageOfPath.ts
  • packages/pagination/src/lib/queries/getPaginationPages.ts
  • packages/pagination/src/lib/queries/hasChromeBlock.ts
  • packages/pagination/src/lib/queries/index.ts
  • packages/pagination/src/lib/registry.ts
  • packages/pagination/src/lib/transforms/enforceHeaderFooterInvariants.ts
  • packages/pagination/src/lib/transforms/ensureFooter.ts
  • packages/pagination/src/lib/transforms/ensureHeader.ts
  • packages/pagination/src/lib/transforms/index.ts
  • packages/pagination/src/lib/transforms/insertPageBreak.ts
  • packages/pagination/src/lib/transforms/removeNodesByType.ts
  • packages/pagination/src/lib/transforms/replaceFooter.ts
  • packages/pagination/src/lib/transforms/replaceHeader.ts
  • packages/pagination/src/lib/transforms/toggleFooter.ts
  • packages/pagination/src/lib/transforms/toggleHeader.ts
  • packages/pagination/src/lib/types.ts
  • packages/pagination/src/measure/__tests__/measure.spec.ts
  • packages/pagination/src/measure/__tests__/pretext.spec.ts
  • packages/pagination/src/measure/index.ts
  • packages/pagination/src/measure/measure.ts
  • packages/pagination/src/measure/pretext.ts
  • packages/pagination/src/react/PaginationPlugin.tsx
  • packages/pagination/src/react/__tests__/geometry.spec.ts
  • packages/pagination/src/react/alignContent.ts
  • packages/pagination/src/react/chrome/PageNumber.tsx
  • packages/pagination/src/react/domMeasure.ts
  • packages/pagination/src/react/footer-plugin.ts
  • packages/pagination/src/react/footnote-portal.tsx
  • packages/pagination/src/react/geometry.ts
  • packages/pagination/src/react/header-plugin.ts
  • packages/pagination/src/react/index.ts
  • packages/pagination/src/react/internal/use-page-layout.ts
  • packages/pagination/src/react/page-break-plugin.ts
  • packages/pagination/src/react/page-frame.tsx
  • packages/pagination/src/react/page-overlay.tsx
  • packages/pagination/src/react/pagination-plugin.ts
  • packages/pagination/src/react/use-pretext-measurer.ts
  • packages/pagination/src/static/footer-element-static.tsx
  • packages/pagination/src/static/header-element-static.tsx
  • packages/pagination/src/static/index.ts
  • packages/pagination/src/static/page-break-element-static.tsx
  • packages/pagination/tsconfig.json
  • plans/2026-05-15-fix-pagination-plugin-tdd-v2.md
  • plans/2026-05-15-fix-pagination-plugin-v1.md
  • plans/2026-05-16-pagination-end-to-end-fix-v1.md
  • templates/plate-playground-template/package.json
  • templates/plate-playground-template/src/app/dev/pagination2/page.tsx
  • templates/plate-playground-template/src/app/dev/pagination2/pagination2-view.tsx
  • templates/plate-playground-template/src/app/editor/page.tsx
  • templates/plate-playground-template/src/components/editor/editor-kit.tsx
  • templates/plate-playground-template/src/components/editor/plugins/pagination-kit.tsx
  • templates/plate-playground-template/src/components/ui/fixed-toolbar-buttons.tsx
  • templates/plate-playground-template/src/components/ui/pagination-toolbar-button.tsx
  • templates/plate-playground-template/vendor/.gitignore
  • templates/plate-playground-template/vendor/platejs-pagination/package.json
  • tooling/config/tsdown.config.ts
  • tooling/scripts/brl.sh
💤 Files with no reviewable changes (45)
  • packages/pagination/src/lib/queries/index.ts
  • packages/pagination/README.md
  • packages/pagination/src/lib/transforms/ensureHeader.ts
  • packages/pagination/src/lib/base-footer-plugin.ts
  • packages/pagination/src/lib/base-page-break-plugin.ts
  • packages/pagination/src/lib/transforms/ensureFooter.ts
  • packages/pagination/src/lib/internal/page-state.ts
  • packages/pagination/src/lib/transforms/toggleHeader.ts
  • packages/pagination/src/lib/transforms/replaceHeader.ts
  • packages/pagination/src/lib/queries/hasChromeBlock.ts
  • packages/pagination/src/lib/internal/measure-cache.ts
  • packages/pagination/src/react/internal/use-page-layout.ts
  • packages/pagination/src/lib/paginate.spec.ts
  • packages/pagination/src/react/footnote-portal.tsx
  • packages/pagination/src/react/page-break-plugin.ts
  • packages/pagination/src/lib/internal/page-size-presets.ts
  • templates/plate-playground-template/vendor/.gitignore
  • packages/pagination/src/react/footer-plugin.ts
  • packages/pagination/src/lib/transforms/index.ts
  • packages/pagination/src/lib/queries/getPaginationPages.ts
  • packages/pagination/src/lib/internal/measure-cache.spec.ts
  • packages/pagination/src/lib/allocate-footnotes.ts
  • packages/pagination/src/lib/base-header-plugin.ts
  • packages/pagination/src/lib/base-pagination-plugins.spec.ts
  • packages/pagination/src/lib/internal/font-from-style.ts
  • packages/pagination/src/react/pagination-plugin.ts
  • packages/pagination/src/lib/types.ts
  • packages/pagination/src/static/index.ts
  • packages/pagination/src/lib/transforms/toggleFooter.ts
  • packages/pagination/src/react/page-frame.tsx
  • packages/pagination/src/lib/paginate.ts
  • packages/pagination/src/lib/transforms/enforceHeaderFooterInvariants.ts
  • packages/pagination/src/react/use-pretext-measurer.ts
  • packages/pagination/src/static/footer-element-static.tsx
  • packages/pagination/src/react/header-plugin.ts
  • packages/pagination/src/static/page-break-element-static.tsx
  • packages/pagination/src/lib/transforms/removeNodesByType.ts
  • packages/pagination/src/static/header-element-static.tsx
  • packages/pagination/src/lib/base-pagination-plugin.ts
  • packages/pagination/src/lib/internal/marks-fingerprint.ts
  • packages/pagination/src/lib/transforms/insertPageBreak.ts
  • packages/pagination/src/lib/transforms/replaceFooter.ts
  • packages/pagination/src/lib/queries/getPageOfPath.ts
  • packages/pagination/src/react/page-overlay.tsx
  • packages/pagination/src/lib/internal/keys.ts

Comment on lines +45 to +50
## Recent fixes already landed (PR #405 / #406 — folder-pick lineage)
- Literal `'pagination'` key (KEYS.pagination unreleased in published utils).
- Single shared provider+coordinator subtree (reflow reads registry).
- Page number in bottom margin.
- Template: drop TrailingBlock conflict; rely on package auto-mount.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use latest-state documentation voice instead of timeline/status language.

This section reads like release notes (“recent fixes”, phased done/status updates). Convert to present-state reference wording.

As per coding guidelines, docs/**/*.md: NEVER write changelog-style language ("has been removed", "new feature", "previously", "now supports") in documentation.

Also applies to: 146-150

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/2026-05-21-pagination-rewrite.md` around lines 45 - 50, The
"Recent fixes already landed (PR `#405` / `#406` — folder-pick lineage)" header and
its bullet list use changelog/timeline language; rewrite them into present‑state
documentation voice by describing current behavior/state (e.g., "KEYS.pagination
is available", "provider+coordinator share a subtree", "page number appears in
bottom margin", "Template relies on package auto-mount and drops TrailingBlock
conflict") and remove temporal verbs/phrases like "recent", "has been", "now",
"previously"; apply the same present‑state rewrite to the other nearby bullet
block in the same section so the docs follow the docs/**/*.md guideline against
changelog-style language.

Comment on lines +3 to +5
Supersedes `2026-05-21-pagination-rewrite.md`. That doc planned a block-level
overlay; this one commits to **full pretext (line/run) measurement** and the
**deletion of the document-mutator path**.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove changelog framing from this plan doc.

Several lines still read as transition history/status instead of latest-state reference (e.g., “Supersedes…”, “current…”, “DONE & green”, “Awaiting…”). Please rewrite these to describe only the target/current state.

As per coding guidelines: “docs/**/*.md: NEVER write changelog-style language … write as if no prior version exists.”

Also applies to: 41-45, 77-77, 138-141

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/2026-05-22-pagination-rewrite-v2.md` around lines 3 - 5, Remove
all changelog-style framing and transitional status language from the plan
(e.g., the opening "Supersedes `2026-05-21-pagination-rewrite.md`" sentence and
any lines containing words like "current", "DONE & green", "Awaiting" referenced
in the comment), and rewrite those sentences so the document reads as a single
authoritative specification describing the target/current behavior and decisions
(no history or prior-version notes). Update the affected sections (around the
top block, lines noted near 41-45, line ~77, and lines ~138-141) so each
paragraph states the intended design, measurement approach (full pretext
line/run measurement), and the removed document-mutator path as facts rather
than a change log or transitional status.

Comment on lines +13 to +35
## 0. Audit triage — what the scorch already fixed vs what remains

The audit `premirror-audit-findings.md` predates the #408–#413 rewrite. Re-scored
against the current tree:

| # | Finding | Status now | Evidence |
|---|---------|-----------|----------|
| P0 | Run/text fidelity lost | **Partly fixed** — snapshot extracts block `text`; pretext measures real wrapped lines. Still block-granularity (no per-leaf runs/marks). | `snapshot.ts:84` (`text: nodeText`), `pretext.ts:34` |
| P1 | No `Point`↔`LayoutPoint` mapping | **OPEN** — mapping is block/line only. | `mapping.ts:18` |
| P2 | Clone-based split rendering | **STALE** — `splitClones.ts` deleted in scorch. | (file gone) |
| P3 | No dirty-range invalidation | **OPEN** — cache by `id@width`, but full pipeline each edit. | `measure.ts:55` |
| P4 | Line-breaking estimated not measured | **Largely fixed** — `measureBlockHeight` counts real pretext lines; `linesToPlace` deleted. `measure.ts` still has a `height/lineHeight` cache fallback. | `pretext.ts:57`, `compose.ts:63` |
| P5 | `MappingIndex` not in `LayoutOutput`, rebuilt per-call | **OPEN — cheapest win.** | `types.ts` (no `mapping`), `projection.ts:36,66` |
| P6 | No font model | **Partly fixed** — `domMeasure` resolves font + content width. | `domMeasure.ts:32,46` |
| P7 | No obstacle/slot (float, multi-column) | **OPEN (future).** | (none) |
| P8 | Spacer self-referential w/ offsetHeight | **Improved** — measure is pretext line-count, not `offsetHeight`, so the circularity is broken. Spacer still depends on prior layout. | `domMeasure.ts:69`, `alignContent.ts:18` |
| P9 | Determinism gap (clone DOM reads) | **STALE** — clones gone; compose is pure. | (file gone) |
| P10 | No previous-layout seam | **OPEN** — no incremental compose. | `measure.ts:45` |

**Conclusion:** the rendering hazards (P2/P9) are gone; the remaining debt is
**contract/correctness + scaling** (P5, P1, P3, P10) plus the missing plugin
host + render layers. This plan targets those in cost/value order.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rewrite this section in present-state reference style.

Lines 15–34 use changelog framing (“predates”, “fixed”, “remains”). Keep this doc as current-state guidance only.

As per coding guidelines, docs/**/*.md: NEVER write changelog-style language ("has been removed", "new feature", "previously", "now supports") in documentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/2026-05-23-pagination-impl-plan.md` around lines 13 - 35, Rewrite
the "Audit triage" section (the table and surrounding text between the header
and conclusion) into a present-state reference: replace changelog phrases like
"predates", "re-scored", "still", "deleted", "gone", "has been removed", "new
feature", "previously", "now supports" with neutral, present-tense descriptions
of current status for each finding (e.g., "Status: OPEN — mapping is block/line
only" instead of "OPEN — mapping is block/line only."). Keep the same findings
and evidence pointers (e.g., references to premirror-audit-findings.md,
snapshot.ts:84, pretext.ts:34, mapping.ts:18, measure.ts:55, types.ts,
projection.ts:36,66, domMeasure.ts:32,46,69, alignContent.ts:18) but present
them as current-state facts; remove any timeline/changelog language in lines
15–34 and ensure the conclusion remains a present-state summary.

Comment on lines +17 to +21
const id = `b${nextId++}`;
return {
id,
path: [nextId],
heightPx,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

path is off-by-one in the test block factory.

Line 20 uses incremented nextId, so the first block gets path: [1] instead of [0]. This can mask path/mapping regressions.

Suggested fix
 function block(
   heightPx: number,
   extra: Partial<MeasuredBlock> = {}
 ): MeasuredBlock {
-  const id = `b${nextId++}`;
+  const index = nextId++;
+  const id = `b${index}`;
   return {
     id,
-    path: [nextId],
+    path: [index],
     heightPx,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const id = `b${nextId++}`;
return {
id,
path: [nextId],
heightPx,
const index = nextId++;
const id = `b${index}`;
return {
id,
path: [index],
heightPx,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pagination/src/layout/__tests__/compose.spec.ts` around lines 17 -
21, The test block factory is creating path using the incremented nextId,
producing path: [1] for the first block; change the factory so path reflects the
block's actual numeric id (e.g., capture the current id before incrementing or
compute path from id) — use the same base value used to form id (`nextId`)
rather than the post-incremented value (for example, compute a local currentId =
nextId, then set id = `b${currentId}`, increment nextId, and set path:
[currentId] or otherwise set path: [nextId - 1]) so path aligns with id; adjust
references in the factory that use nextId, id, and path accordingly.

Comment on lines +50 to +104
export function buildMappingIndex(pages: PageLayout[]): MappingIndex {
const byBlock = new Map<number, FragmentRef[]>();

// CodeRabbit PR #438: store the POSITIONAL pages-array index, not
// `page.index`. Consumers downstream dereference `layout.pages[ref.pageIndex]`
// and `geometry.placements[ref.pageIndex]` as array offsets. If a future
// composer ever emits non-contiguous `page.index` values (skipped covers,
// re-numbering), the two diverge and projections silently drop.
pages.forEach((page, positionalIndex) => {
page.frames.forEach((frame, frameIndex) => {
for (const fragment of frame.fragments) {
const blockIndex = fragment.path[0];
const refs = byBlock.get(blockIndex);
const ref: FragmentRef = {
fragment,
frameIndex,
pageIndex: positionalIndex,
};
if (refs) refs.push(ref);
else byBlock.set(blockIndex, [ref]);
}
});
});

const fragmentsOfBlock = (blockIndex: number): FragmentRef[] =>
byBlock.get(blockIndex) ?? [];

const fragmentOfBlockLine = (
blockIndex: number,
lineIndex: number
): FragmentRef | null => {
for (const ref of fragmentsOfBlock(blockIndex)) {
const { lineCount, lineStart } = ref.fragment;
if (lineIndex >= lineStart && lineIndex < lineStart + lineCount) {
return ref;
}
}

return null;
};

return {
fragmentOfBlockLine,
fragmentsOfBlock,
isSplit: (blockIndex) => {
const refs = fragmentsOfBlock(blockIndex);

return new Set(refs.map((r) => r.pageIndex)).size > 1;
},
pageOfBlock: (blockIndex) =>
fragmentsOfBlock(blockIndex)[0]?.pageIndex ?? null,
pageOfBlockLine: (blockIndex, lineIndex) =>
fragmentOfBlockLine(blockIndex, lineIndex)?.pageIndex ?? null,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: Pagination implementation violates pretext requirement.

Per coding guidelines: "Pretext is mandatory for any pagination plugin. Faithful pagination (page counting, widow/orphan, split points) requires real text measurement/shaping via pretext; block-level DOM offsetHeight estimates are a downgrade, not a substitute."

Per retrieved learning: "If a pagination plugin does NOT use pretext, or uses it incompletely, you MUST NOT proceed silently. First make a solemn attestation stating plainly that pretext is not useful here and the concrete technical reason why."

Evidence from docs/plans/premirror-audit-findings.md:

  • P0 finding: "Run-level fidelity entirely lost" — "NO text content, NO runs, NO per-character measurement"
  • P4 finding: "Line-breaking is estimated, not measured" — uses lineCount = Math.round(heightPx / lineHeightPx) instead of real line-filling

This mapping index operates at block granularity (blockIndex → fragments) instead of document-position granularity (pmPos ↔ layout), which is a direct consequence of the missing pretext layer.

Required action: Either provide the solemn attestation that pretext is not useful for this pagination system with concrete technical reasoning, OR add a plan/timeline to implement pretext-driven text measurement at the run/leaf level.

As per coding guidelines: Pagination files must use pretext for faithful text measurement; the current block-level approach is documented as "model collapse" in the audit findings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pagination/src/layout/mapping.ts` around lines 50 - 104, The
pagination code in buildMappingIndex currently maps blockIndex→fragments
(functions: buildMappingIndex, fragmentsOfBlock, fragmentOfBlockLine, isSplit,
pageOfBlockLine), which violates the requirement to either use pretext-driven
run/leaf measurement or to provide a formal attestation/plan. Fix by doing one
of two clear actions: either add a solemn attestation (documented at the top of
this file and in the PR description) that pretext is intentionally not useful
here with concrete technical reasons (why pretext cannot be applied to this
layout, what measurements are impossible, and why block-level fragments are
sufficient), or replace the block-granular approach with a pretext roadmap and
timeline plus scoped implementation plan to move mapping from
blockIndex→fragments to pmPos↔layout (update buildMappingIndex to produce
per-run/pmPos refs and change
fragmentsOfBlock/fragmentOfBlockLine/isSplit/pageOfBlockLine semantics
accordingly). Ensure the chosen option is explicit in code comments and the PR
so reviewers can verify compliance.

Comment on lines +80 to +85
export function buildSnapshot(
value: SlateNode[],
options: SnapshotOptions
): UnmeasuredSnapshot {
const atomic = new Set(options.atomicTypes ?? []);
const keepWithNext = new Set(options.keepWithNextTypes ?? []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Default options to prevent runtime crashes.

Line 84 dereferences options.atomicTypes; if a JS caller omits the second arg, this throws. Defaulting options keeps this API safe-by-default.

Suggested fix
 export function buildSnapshot(
   value: SlateNode[],
-  options: SnapshotOptions
+  options: SnapshotOptions = {}
 ): UnmeasuredSnapshot {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function buildSnapshot(
value: SlateNode[],
options: SnapshotOptions
): UnmeasuredSnapshot {
const atomic = new Set(options.atomicTypes ?? []);
const keepWithNext = new Set(options.keepWithNextTypes ?? []);
export function buildSnapshot(
value: SlateNode[],
options: SnapshotOptions = {}
): UnmeasuredSnapshot {
const atomic = new Set(options.atomicTypes ?? []);
const keepWithNext = new Set(options.keepWithNextTypes ?? []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pagination/src/layout/snapshot.ts` around lines 80 - 85, The
function buildSnapshot currently dereferences options.atomicTypes and
options.keepWithNextTypes which will throw if callers omit the options argument;
update buildSnapshot to default its options parameter to an empty object (e.g.,
options: SnapshotOptions = {} or assign a local const opts = options ?? {}) and
then use opts.atomicTypes and opts.keepWithNextTypes when constructing the
atomic and keepWithNext sets so buildSnapshot, SnapshotOptions, atomicTypes,
keepWithNextTypes and UnmeasuredSnapshot are safe for JS callers who omit the
second argument.

Comment on lines +13 to +16
// @ts-expect-error - test-only canvas stub
globalThis.OffscreenCanvas = StubOffscreenCanvas;

import { measureBlockHeight, measureTextLines } from '../pretext';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore globalThis.OffscreenCanvas after this suite.

The global override is permanent for the process and can leak into unrelated tests. Save the previous value and restore it in teardown.

Suggested fix
+const prevOffscreenCanvas = globalThis.OffscreenCanvas;
 // `@ts-expect-error` - test-only canvas stub
 globalThis.OffscreenCanvas = StubOffscreenCanvas;
 
 import { measureBlockHeight, measureTextLines } from '../pretext';
+afterAll(() => {
+  globalThis.OffscreenCanvas = prevOffscreenCanvas;
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @ts-expect-error - test-only canvas stub
globalThis.OffscreenCanvas = StubOffscreenCanvas;
import { measureBlockHeight, measureTextLines } from '../pretext';
const prevOffscreenCanvas = globalThis.OffscreenCanvas;
// `@ts-expect-error` - test-only canvas stub
globalThis.OffscreenCanvas = StubOffscreenCanvas;
import { measureBlockHeight, measureTextLines } from '../pretext';
afterAll(() => {
globalThis.OffscreenCanvas = prevOffscreenCanvas;
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/pagination/src/measure/__tests__/pretext.spec.ts` around lines 13 -
16, Save the original globalThis.OffscreenCanvas before assigning the test stub
(e.g. const _origOffscreenCanvas = globalThis.OffscreenCanvas), then set
globalThis.OffscreenCanvas = StubOffscreenCanvas for the suite, and restore the
original in teardown (use afterAll or afterEach) by reassigning
globalThis.OffscreenCanvas = _origOffscreenCanvas or deleting it when
_origOffscreenCanvas is undefined; reference the StubOffscreenCanvas assignment
near the measureBlockHeight/measureTextLines tests and ensure restoration
happens even on failures.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants