Skip to content

Codex/playground pagination toggle - #438

Open
arthrod wants to merge 47 commits into
mainfrom
codex/playground-pagination-toggle
Open

Codex/playground pagination toggle#438
arthrod wants to merge 47 commits into
mainfrom
codex/playground-pagination-toggle

Conversation

@arthrod

@arthrod arthrod commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Checklist

  • pnpm typecheck
  • pnpm lint:fix
  • bun test
  • pnpm brl
  • pnpm changeset
  • ui changelog

Summary by CodeRabbit

  • New Features

    • Added continuous pagination overlay with visual page-break lines and "Page N of M" labels.
    • Pagination plugin now auto-mounts required runtime components automatically.
    • Added runtime toggle to enable/disable pagination without reloading.
    • Text measurement now uses pretext for accurate line-breaking and page-height calculation.
  • Bug Fixes

    • Fixed pagination support for editors with wrapped block UI elements.
    • Fixed cache performance for repeated block measurements at different widths.
    • Fixed page labels and break alignment in continuous view.
  • Improvements

    • Margin-aware page packing with improved spacing.
    • Blocks now place whole on pages without mid-block splitting.

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 17 commits May 22, 2026 07:00
… derived projection

Scorch the mutator path: reflowEngine, page-node wrapping (BasePaginationPlugin),
coordinator, registry, leader election, yjs bridge + the dead clone renderer
(splitClones, option C). Package is now a self-contained pure layout pipeline
(snapshot/measure/compose/geometry/align). Drop ./yjs export + dead deps.
Neutralize template pagination usage (waiver). Add pretext gate to AGENTS.md.

Plan: docs/plans/2026-05-22-pagination-rewrite-v2.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
F-006: cache was keyed by block id alone, so the same block measured at two
widths overwrote one slot — alternating widths (resize, side-by-side) defeated
the cache. Key by `${id}@${widthPx}`; drop the redundant inner key field.
TDD: alternating-width test was RED (3 measures), now GREEN (2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add @chenglou/pretext + measure/pretext.ts → measureTextLines(text, font,
widthPx, lineHeightPx): wrapped lines with text, width, and segment/grapheme
cursor range. Real word/grapheme/CJK-aware breaking — the seed for line-level
pagination + caret mapping (F-003).

pretext measures via canvas; tests inject a deterministic monospace stub so
line breaking is exact and headless. Barrels regenerated (pretext exported from
'.'); fix scratch demo to import pure pipeline from '.' and DOM helpers from
'/react' per the convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UnmeasuredBlock gains a required `text` field (concatenated leaf text), so the
measure pass can feed pretext. TDD: snapshot-text test RED→GREEN.

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

Option 3: pretext is the canonical layout measurement. measureBlockHeight(text,
font, widthPx, lineHeightPx) = wrapped line count * lineHeight (>=1 line).
createDomMeasure resolves the editor's real font + content width from the DOM
and feeds pretext, replacing offsetHeight — so layout height is line-accurate
and decoupled from box padding/margin. pretext gate satisfied (layout uses it).

TDD: measureBlockHeight RED→GREEN (canvas stub). DOM rewire verified at host (PR7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove the line-splitting + widow/orphan path. Each block is one whole fragment:
fits-or-next-page, oversized overflows its page. Matches the chosen render
(option C, no clones). Drops linesToPlace; compose is now ~20 lines simpler.
TDD: compose.spec rewritten to the place-whole contract, RED→GREEN.

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

audit P5: projection.ts rebuilt buildMappingIndex per call. Now composeLayout
builds it once and returns it on LayoutOutput.mapping; fragmentRects/
blockLinePosition consume it. buildMappingIndex now takes PageLayout[].

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

lib/registry.ts: WeakMap<editor,{output,dirty,measureCache}>, lazy rebuild via
injected compute, shouldInvalidateLayout predicate (content ops yes, selection
no). Footnote registry pattern. apply-override wiring lands with the plugin.

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

createTSlatePlugin<PaginationConfig> with page/margins/policies/viewMode/atomic/
keepWithNext options (A4, continuous default); overrideEditor wraps apply to mark
the layout registry dirty on content ops (not selection). React useHooks host
deferred to the first render PR (needs observable output to verify).

Also: salvage useful algorithms from an external DOCX paginator into the impl
plan (margin-collapsing, header/footer effective-height, footnote area, token
offset model); reject its DOM-clone architecture.

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

continuous.ts: getContinuousBreakYs(layout) → continuous-flow Y at each interior
page boundary (cumulative page heights, N pages → N-1 break Ys). Drives the
continuous view's advisory break-lines (no spacers, content stays in flow).

pnpm brl: expose lib/ (registry, BasePaginationPlugin) + layout/continuous from
the package barrels.

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

pretext owns the break decision; the continuous overlay anchors its
advisory rule to the boundary block's live DOM top instead of a
text-only pixel sum that ignores DOM margins.

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

toPlatePlugin(BasePaginationPlugin): useHooks runs snapshot→pretext-measure→compose
against the live editable when the registry is dirty (rAF-batched), stores layout
in the registry, and sets a reactive breakYs option. afterEditable overlay paints
dashed advisory page-break rules (pointer-events:none) at getContinuousBreakYs.

SCAFFOLD: typecheck 8/8 + build 8/8, but overlay DOM positioning + recompute
cadence are NOT dev-browser verified yet. Do not merge until verified in-browser.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- PaginationPlugin useHooks moved into plugin config (was .extend) so the
  React host's recompute/overlay actually run.
- Overlay consumes getContinuousBreaks: each advisory rule anchors to the
  boundary block's live DOM top (no text-only pixel sum) → lands on a real
  block edge, never mid-paragraph.
- Page N label moved to the right margin gutter (was overlapping body text);
  dashed rule bumped to visible slate-500.
- ResizeObserver recomputes + re-anchors on width change.
- Wire /dev/pagination2 demos (apps/www + template) to register the plugin;
  template gets the @chenglou/pretext runtime peer.

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

- 001: compose packs by flow height (text + DOM box spacing via flowHeightPx),
  so page count + breaks match real DOM flow (demo: 3→4 pages, correct for
  2965px content). heightPx/lineCount stay text-only for line mapping.
- 003: overlay shows 'Page N of M' + a 'Page 1 of M' marker.
- 004: labels moved to the left margin gutter so they stay on-screen when a
  narrow viewport overflows the page width.
- 002: recompute runs in a layout effect (pre-paint) instead of post-paint rAF;
  content→lines gap ~604ms→~308ms (residual is editor hydration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`enabled` (default true) on BasePaginationPlugin. When false, the React
layer skips layout recompute and renders no overlay; document never
affected. Toggle via editor.setOption(BasePaginationPlugin,'enabled',next).
TDD: base option contract (default true, configurable). Changeset: minor.

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

- PaginationKit adds @platejs/pagination (enabled:false initially) to EditorKit.
- PaginationToolbarButton ("Page breaks", SeparatorHorizontal icon) toggles
  enabled via editor.setOption(PaginationPlugin,'enabled',next); pressed state
  reflects it. Added to the lists/toggle toolbar group.
- biome: drop unused React import in pagination2-view.

Overrides the CI-controlled-templates rule per explicit user request.
Deployed via OpenNext: https://plate-playground.cicero-im.workers.dev/editor

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

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces a pure pretext-measured pagination pipeline with composition/mapping/projection, a React continuous overlay plugin, BasePaginationPlugin and registry, integrates into apps/www and template, adds E2E tests and tooling updates, documents plans/diary, and records changesets.

Changes

Pretext pagination pipeline and integrations

Layer / File(s) Summary
Policy gate and release notes
.agents/AGENTS.md, .changeset/*
Adds pretext-required policy and changesets for measurement, composition, overlay, options, mapping, and fixes.
Core contracts, snapshot, and measurement
packages/pagination/src/layout/types.ts, .../snapshot.ts, .../measure/*
Defines layout contracts, builds snapshots, measures text via pretext with caching and tests.
Layout composition, mapping, projection, continuous breaks
packages/pagination/src/layout/*
Implements composeLayout and helpers, with mapping/projection and continuous break utilities plus tests.
Base plugin and per-editor layout registry
packages/pagination/src/lib/*
Adds BasePaginationPlugin and a WeakMap-backed layout registry, tests, and refined lib barrel.
React overlay, DOM measurer, geometry, alignment, and tests
packages/pagination/src/react/*
Adds continuous-view overlay, DOM measurer, geometry/align utilities, tests, and React barrel.
Package metadata, barrels, tsconfig
packages/pagination/package.json, src/index.ts, tsconfig.json
Updates version/exports/deps, expands root exports, and tweaks TypeScript includes.
apps/www wiring and UI toggle
apps/www/...
Adds dependency, demo route, registers PaginationKit in editor kits/registry, and toolbar toggle.
Playground template wiring and vendor metadata
templates/plate-playground-template/..., vendor/...
Wires PaginationKit, updates toolbar, adds demo routes, updates vendored package.json.
Tooling configs, E2E tests, barrels script
bunfig.toml, tooling/e2e/..., tooling/config/tsdown.config.ts, tooling/scripts/brl.sh, package.json
Configures bun, adds Playwright overlay tests, tsdown yjs entry, barrels script, and deploy script.
Diary, plans, and solution docs
diary.md, docs/plans/*, docs/solutions/*, packages/pagination/failures.md
Documents rewrite rationale, plans, audits, solutions, and detailed failure assessments.

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant Plugin as BasePaginationPlugin
  participant Registry
  participant DOM as DOM Measurer
  participant Layout as Compose/Mappings
  participant Overlay as React Overlay

  Editor->>Plugin: apply(op)
  Plugin->>Registry: invalidate on content ops
  Overlay->>DOM: snapshot + pretext measure
  DOM-->>Layout: MeasuredSnapshot
  Layout-->>Registry: LayoutOutput (pages + mapping)
  Overlay->>Editor: render break lines/labels (continuous)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Possibly related PRs

  • cicero-im/plate#375 — Superseded by removing old page-cache/transform logic in favor of the new registry pipeline.
  • cicero-im/plate#357 — Rewrites/removes modules originally added there (paginate, measure-cache, overlay scaffolding).

Suggested labels

Feat2, Review effort 3/5

Poem

A rabbit paginates with care and grace,
Pretext whispers lines in measured space.
Blocks hop whole—no splitting scare,
Dashed breaks dance in gutter air.
Editor smiles, overlay light—
Pages align, just right tonight. 🐇✂️📄

✨ 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 codex/playground-pagination-toggle

@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 introduces a complete rewrite of the @platejs/pagination package, transitioning from a document-mutating architecture to a derived-overlay projection model. The new system leverages @chenglou/pretext for deterministic text measurement and includes a pure layout pipeline with a React plugin for rendering advisory page breaks. Review feedback identifies several critical improvements for the DOM measurement logic, including fixing potential index misalignment in block resolution, optimizing the performance of the measurement factory, and hardening line height and count calculations against edge cases.

Comment on lines +15 to +26
export function topLevelBlockElements(editable: HTMLElement): HTMLElement[] {
const selector = '[data-slate-node="element"]';

return Array.from(editable.children).flatMap((child) => {
if (child instanceof HTMLElement && child.matches(selector)) {
return [child];
}

const nested = child.querySelector(selector);

return nested instanceof HTMLElement ? [nested] : [];
});

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

Using flatMap here can cause index misalignment between the returned elements array and the snapshot.blocks array (which is indexed by path[0]). If a child of the editable does not match the selector and has no nested match, it is omitted from the result, shifting the indices of all subsequent elements. Using map with a null fallback is safer to maintain index stability.

Suggested change
export function topLevelBlockElements(editable: HTMLElement): HTMLElement[] {
const selector = '[data-slate-node="element"]';
return Array.from(editable.children).flatMap((child) => {
if (child instanceof HTMLElement && child.matches(selector)) {
return [child];
}
const nested = child.querySelector(selector);
return nested instanceof HTMLElement ? [nested] : [];
});
export function topLevelBlockElements(editable: HTMLElement): (HTMLElement | null)[] {
const selector = '[data-slate-node="element"]';
return Array.from(editable.children).map((child) => {
if (child instanceof HTMLElement && child.matches(selector)) {
return child;
}
const nested = child.querySelector(selector);
return nested instanceof HTMLElement ? nested : null;
});
}

Comment on lines +89 to +108
export function createDomMeasure(editable: HTMLElement): MeasureFn {
return (block) => {
const dom = topLevelBlockElements(editable)[block.path[0]];
if (!dom) return null;

const style = getComputedStyle(dom);
const lineHeightPx = resolveLineHeight(style);

return {
boxSpacingPx: verticalBoxSpacing(style),
heightPx: measureBlockHeight(
block.text,
resolveFont(style),
contentWidth(dom, style),
lineHeightPx
),
lineHeightPx,
};
};
}

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

The current implementation of createDomMeasure results in O(N^2) complexity during measureSnapshot because topLevelBlockElements(editable) is called for every block in the snapshot. Pre-calculating the elements array once in the factory function will improve performance to O(N).

export function createDomMeasure(editable: HTMLElement): MeasureFn {
  const elements = topLevelBlockElements(editable);

  return (block) => {
    const dom = elements[block.path[0]];
    if (!dom) return null;

    const style = getComputedStyle(dom);
    const lineHeightPx = resolveLineHeight(style);

    return {
      boxSpacingPx: verticalBoxSpacing(style),
      heightPx: measureBlockHeight(
        block.text,
        resolveFont(style),
        contentWidth(dom, style),
        lineHeightPx
      ),
      lineHeightPx,
    };
  };
}


function lineCountFrom(heightPx: number, lineHeightPx: number): number {
if (lineHeightPx <= 0) return 1;

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

Using Math.round for line count estimation can lead to inaccuracies where a block that slightly exceeds a line boundary is under-counted, potentially causing content overlap or incorrect page breaks. As noted in your own failures.md (F-024), Math.ceil is a safer choice for pagination to ensure all content is accounted for.

Suggested change
return Math.max(1, Math.ceil(heightPx / lineHeightPx));

Comment on lines +88 to +89
const topOf = (el: HTMLElement) =>
editable.offsetTop + (el.getBoundingClientRect().top - editableTop);

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

Update topOf to handle null elements, which may be returned by the optimized topLevelBlockElements if a child of the editable is not a Slate element.

Suggested change
const topOf = (el: HTMLElement) =>
editable.offsetTop + (el.getBoundingClientRect().top - editableTop);
const topOf = (el: HTMLElement | null) =>
el ? editable.offsetTop + (el.getBoundingClientRect().top - editableTop) : 0;

Comment on lines +109 to +110
const lineHeight =
Number.parseFloat(getComputedStyle(el).lineHeight) || 0;

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

Number.parseFloat on lineHeight will return NaN if the value is normal, resulting in a lineHeight of 0. This will cause incorrect positioning of break lines when lineStart > 0. Consider adding a fallback that uses the font size (e.g., fontSize * 1.5) when lineHeight is not a number.

Suggested change
const lineHeight =
Number.parseFloat(getComputedStyle(el).lineHeight) || 0;
const style = getComputedStyle(el);
const fontSize = Number.parseFloat(style.fontSize) || 16;
const lineHeight = Number.parseFloat(style.lineHeight) || fontSize * 1.5;

@arthrod

arthrod commented May 26, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai conejo review plz

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

@arthrod ¡Claro que sí! Iniciando la revisión ahora mismo. 🐰

✅ 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: 17

🧹 Nitpick comments (2)
docs/plans/2026-05-22-pagination-rewrite-v2.md (1)

3-5: ⚡ Quick win

Rewrite supersession text to latest-state style.

Line 3 introduces migration-style history (“Supersedes ...”), which should be avoided in docs per repo style. Keep this file framed as the authoritative current plan only.

As per coding guidelines: “No migration notes, no ‘what changed’ — just document what IS.”

🤖 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
the migration-style "Supersedes `2026-05-21-pagination-rewrite.md`" line and any
similar history phrasing in the doc so the file reads as the single
authoritative current plan; update the opening paragraph (the sentence beginning
"Supersedes ...") to a forward-looking statement that describes the current plan
(e.g., "This document defines the 2026-05-22 pagination rewrite: full pretext
(line/run) measurement and removal of the document-mutator path.") and ensure no
"what changed" or migration notes remain.
docs/plans/2026-05-21-pagination-rewrite.md (1)

45-50: ⚡ Quick win

Avoid changelog-style wording in docs.

Line 45 uses retrospective framing (“Recent fixes already landed”), which conflicts with the docs style rule for latest-state documentation. Please rewrite this section as current-state guidance only.

As per coding guidelines: “NEVER write changelog-style language in documentation … document what IS.”

🤖 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 header
"## Recent fixes already landed (PR `#405` / `#406` — folder-pick lineage)" and its
introductory phrasing should be rewritten to present-state documentation (no
changelog wording); change the heading to something like "## Fixes in place" or
"## Current fixes" and update the surrounding sentence to state that these fixes
are active now, and convert the list items (Literal 'pagination' key, Single
shared provider+coordinator subtree, Page number in bottom margin, Template:
drop TrailingBlock conflict) to present-tense descriptions of behavior (e.g.,
"The 'pagination' key is used...", "Provider and coordinator share a
subtree...", "Page number appears in the bottom margin...", "Template relies on
package auto-mount and drops TrailingBlock conflict").
🤖 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-margin-aware-packing.md:
- Around line 5-10: The document uses changelog-style narration; rewrite the
content to describe the current behavior and features as reference documentation
(no "changed/now/previously" phrasing). For the
`.changeset/pagination-margin-aware-packing.md` entry, convert each bullet into
present-tense descriptions: explain that Compose packs pages by a block's flow
height (flowHeightPx) with fallback to text height, that heightPx/lineCount
remain text-only, that overlay labels display "Page N of M" and include a static
"Page 1 of M" marker, that labels live in the left margin gutter to remain
visible on narrow viewports, and that recompute runs in a layout effect before
paint; remove any change-log language and ensure terms like flowHeightPx,
heightPx, lineCount, Compose, and layout effect are used to locate and update
the text.

In `@apps/www/src/app/dev/pagination2/pagination2-view.tsx`:
- Around line 11-13: PAGE_W and MARGIN are single-use constants; remove the
constants PAGE_W and MARGIN and replace their usages with the literal numbers
(794 and 96) directly where referenced (e.g., in the layout calculations inside
pagination2-view.tsx) so the numeric values are inlined and the unused const
declarations are deleted.

In `@apps/www/src/registry/registry-ui.ts`:
- Around line 800-803: Update the registry entry's description to accurately
reflect the component behavior: replace the current "A toolbar button for page
break markers." with wording that says the button toggles pagination on and off
(e.g., "A toolbar button that toggles pagination on/off."). Locate the object
containing description and files: the entry with files: [{ path:
'ui/pagination-toolbar-button.tsx', type: 'registry:ui' }] and update its
description string accordingly so docs/UI match implementation.

In `@docs/plans/2026-05-21-pagination-rewrite.md`:
- Around line 64-69: The fenced code block containing "Slate value →
snapshot(flat blocks + stable ids + slate paths) → measure (real DOM block
heights, cached) → MeasuredSnapshot → composeLayout(pure, deterministic) →
LayoutOutput → render overlay (page chrome + projected content) + mapping" is
missing a language tag; update the opening triple-backticks to include a
language identifier (for example, use ```text or ```diff) so Markdown renders
consistently and syntax/highlighting tools can process it.

In `@package.json`:
- Line 55: The deploy script "deploy:playground" uses ad-hoc npx for
opennextjs-cloudflare which can drift; add a pinned opennextjs-cloudflare
version to package.json devDependencies (e.g., "opennextjs-cloudflare": "x.y.z")
and update the "deploy:playground" script to invoke the local CLI (for example
replace `npx opennextjs-cloudflare build && npx opennextjs-cloudflare deploy`
with `pnpm exec opennextjs-cloudflare build && pnpm exec opennextjs-cloudflare
deploy` or the equivalent package-manager-local invocation) so the CLI version
is reproducible and installed via package.json.

In `@packages/pagination/src/layout/mapping.ts`:
- Around line 37-46: The fragment refs are using page.index which can differ
from the array position and cause lookups against
layout.pages/geometry.placements to fail; update the iteration in
buildMappingIndex to capture the array position (e.g., pages.forEach((page,
pageIndex) => { ... }) or otherwise compute the positional index) and set
ref.pageIndex to that positional pageIndex when constructing the FragmentRef
(the object created inside the pages.forEach/frames.forEach loop), instead of
using page.index, so consumers can safely use ref.pageIndex as an array offset.

In `@packages/pagination/src/layout/snapshot.ts`:
- Around line 46-75: stableId can produce duplicate fallback ids for sibling
nodes with identical type/text, corrupting downstream caches; in buildSnapshot
after creating the blocks array (built using stableId) detect duplicate ids and
disambiguate only the fallback-generated ones by appending a deterministic
suffix (e.g., "`#1`", "`#2`" or "#<index>") to make each UnmeasuredBlock.id unique
while preserving any original node.id values; implement this by scanning blocks
in order, tracking counts in a map keyed by the original id, and mutating
block.id for subsequent duplicates (but not for blocks whose node.id was an
explicit string) so grouping and (id,width) caching remain stable and
deterministic.

In `@packages/pagination/src/measure/__tests__/pretext.spec.ts`:
- Around line 5-16: The test overrides globalThis.OffscreenCanvas with
StubOffscreenCanvas and never restores it, causing pollution of other tests; fix
by saving the original globalThis.OffscreenCanvas to a local variable before
assignment and restore it in an afterAll/afterEach teardown callback (use the
same test file's test suite teardown) so globalThis.OffscreenCanvas is reset
after tests complete; reference the StubOffscreenCanvas symbol and the
globalThis.OffscreenCanvas assignment to locate where to add the
save-and-restore logic.

In `@packages/pagination/src/measure/measure.ts`:
- Around line 69-83: The measured block creation must guard against non-finite
or non-positive measurements before computing lineCount: validate
metrics?.heightPx, metrics?.lineHeightPx, and metrics?.boxSpacingPx (from the
metrics variable used to set heightPx, lineHeightPx, and boxSpacingPx) and
replace any non-finite or <=0 values with fallbackLineHeightPx (or 0 for
boxSpacingPx) before calling lineCountFrom; also ensure measured.flowHeightPx is
only set when both heightPx and boxSpacingPx are finite so lineCountFrom and
flowHeightPx cannot become NaN (adjust logic inside the block that builds
measured and where lineCountFrom is invoked).

In `@packages/pagination/src/react/alignContent.ts`:
- Around line 39-46: The computed spacer value passed to spacers.set can be
negative when contentHeight - prevBottom < 0; clamp the spacer to a non-negative
value before calling spacers.set (e.g., compute spacer = Math.max(0,
contentHeight - prevBottom + input.margins.bottomPx + gapPx +
input.margins.topPx)) so that spacers.set(first.path[0], spacer) never receives
a negative marginTop; update the logic in alignContent.ts where spacers.set is
called to use the clamped spacer variable.

In `@packages/pagination/src/react/geometry.ts`:
- Around line 78-87: The code marks startsPage using frame-local fragmentPos ===
0 which incorrectly flags blocks that start later frames as page starts; change
the condition to detect the first unique fragment seen for that page (e.g.
startsPage: byBlock.size === 0) so the first block recorded into the byBlock map
is the only one marked as starting the page; update the block creation in the
loop over page.frames / frame.fragments to use byBlock.size === 0 instead of
fragmentPos === 0 (or alternatively track a page-first-encountered boolean) so
startsPage reflects the first fragment across the whole page.

In `@plans/2026-05-16-pagination-end-to-end-fix-v1.md`:
- Around line 107-114: Update Task 9 and Task 10 to use the repo-required
command forms: replace `bun typecheck` with `bun run typecheck` in Task 9 and
replace `bun lint:fix` with `bun run lint:fix` in Task 10 (these are the entries
labeled "Task 9." and "Task 10." in the plan for the template verification
steps), ensuring the instructions read to run `cd
templates/plate-playground-template && bun run typecheck` and `cd
templates/plate-playground-template && bun run lint:fix` so they match the repo
verification policy and the coding guidelines.
- Around line 9-10: The objective and tasks use inconsistent command names (`bun
run deploy` vs `deploy:playground`); choose one canonical command (e.g.,
`deploy:playground` or `deploy`) and update all occurrences across the
document—including the root objective line, Task 7, and the verification
criteria lines referenced (including the section around lines 84-93)—so they
consistently reference the same command name (ensure any example CLI
invocations, task titles, and verification steps all use the chosen symbol).

In `@templates/plate-playground-template/package.json`:
- Line 61: Revert the manual edit adding/updating the "`@platejs/yjs`" dependency
in the generated template manifest and instead update the upstream source inputs
(the package registry or package metadata used to build templates) or the
template generation workflow; then run the template/regeneration task to produce
a new templates/plate-playground-template/package.json so the change is made by
the CI generation step rather than committing a hand-edited templates manifest.

In
`@templates/plate-playground-template/src/components/ui/pagination-toolbar-button.tsx`:
- Around line 17-20: The ToolbarButton usage in PaginationToolbarButton
overrides any caller onClick in props, so preserve and compose the original
handler: retrieve props.onClick (e.g., const { onClick: callerOnClick, ...rest }
= props) and pass a new onClick that first invokes callerOnClick(event) if
present, then performs editor.setOption(PaginationPlugin, 'enabled', !enabled);
keep pressed={enabled} and spread the remaining props (rest) into ToolbarButton
so existing consumers' callbacks are not lost.

In `@templates/plate-playground-template/vendor/platejs-pagination/package.json`:
- Around line 16-18: The package.json in
templates/plate-playground-template/vendor/platejs-pagination currently only
lists "`@chenglou/pretext`" under "dependencies" but may be missing runtime
externals declared as "peerDependencies"; regenerate or obtain the built dist/
and run the dist-imports check, and if the built JS imports external packages
such as "react", "react-dom", "platejs" (or other runtime libs), add them to
package.json's "peerDependencies" (not "dependencies") so runtime externals are
declared; ensure the CI/output that produces templates/** is updated so the
peerDependencies are present in the generated package.json whenever the dist
requires them.

In `@tooling/e2e/pagination.spec.ts`:
- Around line 93-95: boundingBox() may return fractional sub-pixel values
causing flaky assertions; update the two assertions that reference box!.x and
box!.x + box!.width (in the test using boundingBox()) to tolerate a tiny epsilon
(e.g., 0.5): check box!.x >= -epsilon and box!.x + box!.width <= 600 + epsilon
(or add/subtract epsilon in the toBeGreaterThanOrEqual/toBeLessThanOrEqual
checks) so DPR rounding won’t cause false negatives.

---

Nitpick comments:
In `@docs/plans/2026-05-21-pagination-rewrite.md`:
- Around line 45-50: The header "## Recent fixes already landed (PR `#405` / `#406`
— folder-pick lineage)" and its introductory phrasing should be rewritten to
present-state documentation (no changelog wording); change the heading to
something like "## Fixes in place" or "## Current fixes" and update the
surrounding sentence to state that these fixes are active now, and convert the
list items (Literal 'pagination' key, Single shared provider+coordinator
subtree, Page number in bottom margin, Template: drop TrailingBlock conflict) to
present-tense descriptions of behavior (e.g., "The 'pagination' key is used...",
"Provider and coordinator share a subtree...", "Page number appears in the
bottom margin...", "Template relies on package auto-mount and drops
TrailingBlock conflict").

In `@docs/plans/2026-05-22-pagination-rewrite-v2.md`:
- Around line 3-5: Remove the migration-style "Supersedes
`2026-05-21-pagination-rewrite.md`" line and any similar history phrasing in the
doc so the file reads as the single authoritative current plan; update the
opening paragraph (the sentence beginning "Supersedes ...") to a forward-looking
statement that describes the current plan (e.g., "This document defines the
2026-05-22 pagination rewrite: full pretext (line/run) measurement and removal
of the document-mutator path.") and ensure no "what changed" or migration notes
remain.
🪄 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: df1d017a-0e55-417c-9e30-7451f29cda4d

📥 Commits

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

⛔ 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 (132)
  • .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
  • .changeset/pagination-wrapped-blocks.md
  • AGENTS.md
  • apps/www/next-env.d.ts
  • apps/www/package.json
  • apps/www/src/app/dev/pagination2/page.tsx
  • apps/www/src/app/dev/pagination2/pagination2-view.tsx
  • apps/www/src/registry/blocks/editor-ai/components/editor/editor-kit.tsx
  • apps/www/src/registry/components/editor/editor-kit.tsx
  • apps/www/src/registry/components/editor/plugins/pagination-kit.tsx
  • apps/www/src/registry/registry-blocks.ts
  • apps/www/src/registry/registry-kits.ts
  • apps/www/src/registry/registry-ui.ts
  • apps/www/src/registry/ui/fixed-toolbar-buttons.tsx
  • apps/www/src/registry/ui/pagination-toolbar-button.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/2026-05-24-pagination-demo-print-regression.md
  • docs/plans/premirror-audit-findings.md
  • docs/solutions/ui-bugs/2026-05-24-pagination-must-measure-wrapped-block-elements.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.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__/domMeasure.spec.ts
  • packages/pagination/src/react/__tests__/geometry.spec.ts
  • packages/pagination/src/react/alignContent.ts
  • 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/e2e/pagination.spec.ts
  • tooling/scripts/brl.sh
💤 Files with no reviewable changes (45)
  • packages/pagination/src/static/header-element-static.tsx
  • packages/pagination/src/lib/base-pagination-plugins.spec.ts
  • packages/pagination/src/lib/transforms/insertPageBreak.ts
  • packages/pagination/src/lib/internal/keys.ts
  • packages/pagination/src/static/footer-element-static.tsx
  • packages/pagination/src/lib/internal/page-state.ts
  • packages/pagination/src/static/page-break-element-static.tsx
  • packages/pagination/src/lib/paginate.ts
  • packages/pagination/src/lib/base-page-break-plugin.ts
  • templates/plate-playground-template/vendor/.gitignore
  • packages/pagination/src/react/page-break-plugin.ts
  • packages/pagination/src/lib/queries/getPageOfPath.ts
  • packages/pagination/src/react/internal/use-page-layout.ts
  • packages/pagination/src/lib/queries/getPaginationPages.ts
  • packages/pagination/src/lib/types.ts
  • packages/pagination/src/lib/internal/font-from-style.ts
  • packages/pagination/src/lib/transforms/enforceHeaderFooterInvariants.ts
  • packages/pagination/src/static/index.ts
  • packages/pagination/src/lib/base-header-plugin.ts
  • packages/pagination/src/react/footnote-portal.tsx
  • packages/pagination/src/react/pagination-plugin.ts
  • packages/pagination/src/lib/transforms/replaceFooter.ts
  • packages/pagination/src/react/footer-plugin.ts
  • packages/pagination/src/react/page-frame.tsx
  • packages/pagination/src/lib/allocate-footnotes.ts
  • packages/pagination/src/lib/transforms/ensureHeader.ts
  • packages/pagination/src/lib/transforms/index.ts
  • packages/pagination/src/lib/transforms/removeNodesByType.ts
  • packages/pagination/src/lib/internal/measure-cache.spec.ts
  • packages/pagination/src/lib/paginate.spec.ts
  • packages/pagination/src/lib/internal/marks-fingerprint.ts
  • packages/pagination/src/react/use-pretext-measurer.ts
  • packages/pagination/src/lib/transforms/toggleHeader.ts
  • packages/pagination/src/lib/transforms/ensureFooter.ts
  • packages/pagination/src/lib/transforms/toggleFooter.ts
  • packages/pagination/src/lib/base-footer-plugin.ts
  • packages/pagination/src/lib/queries/index.ts
  • packages/pagination/src/react/page-overlay.tsx
  • packages/pagination/src/lib/base-pagination-plugin.ts
  • packages/pagination/src/lib/queries/hasChromeBlock.ts
  • packages/pagination/src/lib/transforms/replaceHeader.ts
  • packages/pagination/src/react/header-plugin.ts
  • packages/pagination/src/lib/internal/measure-cache.ts
  • packages/pagination/src/lib/internal/page-size-presets.ts
  • packages/pagination/README.md

Comment on lines +5 to +10
Margin-aware page packing + continuous-overlay polish:

- Compose now packs pages by a block's **flow height** (text height + the DOM box spacing — margins/padding/borders — supplied by the measurer as `flowHeightPx`), falling back to text height when absent. The page count and break placement now match real DOM flow instead of under-counting per-page capacity. `heightPx`/`lineCount` stay text-only so line-level mapping is unaffected.
- Overlay labels show `Page N of M` and add a `Page 1 of M` marker, so the first page and total are always visible.
- Labels moved to the left margin gutter, so they stay on-screen when a narrow viewport overflows the page width.
- The recompute runs in a layout effect (before paint) instead of a post-paint `requestAnimationFrame`, so the advisory lines appear with the content as soon as the editor hydrates.

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 doc voice instead of release-note/changelog phrasing.

This .md content is written as change narration. Reword it as current-state reference text only (no “what changed” framing).

As per coding guidelines: “NEVER write changelog-style language in documentation (‘has been removed’, ‘new feature’, ‘previously’, ‘now supports’). Docs are user-facing reference for the LATEST state only.”

🤖 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-margin-aware-packing.md around lines 5 - 10, The
document uses changelog-style narration; rewrite the content to describe the
current behavior and features as reference documentation (no
"changed/now/previously" phrasing). For the
`.changeset/pagination-margin-aware-packing.md` entry, convert each bullet into
present-tense descriptions: explain that Compose packs pages by a block's flow
height (flowHeightPx) with fallback to text height, that heightPx/lineCount
remain text-only, that overlay labels display "Page N of M" and include a static
"Page 1 of M" marker, that labels live in the left margin gutter to remain
visible on narrow viewports, and that recompute runs in a layout effect before
paint; remove any change-log language and ensure terms like flowHeightPx,
heightPx, lineCount, Compose, and layout effect are used to locate and update
the text.

Comment on lines +11 to +13
const PAGE_W = 794; // A4 @ 96dpi
const MARGIN = 96; // 1in

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n '\b(PAGE_W|MARGIN)\b' apps/www/src/app/dev/pagination2/pagination2-view.tsx

Repository: cicero-im/plate

Length of output: 183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/www/src/app/dev/pagination2/pagination2-view.tsx"
nl -ba "$FILE" | sed -n '1,25p'
echo '---'
nl -ba "$FILE" | sed -n '45,80p'

Repository: cicero-im/plate

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/www/src/app/dev/pagination2/pagination2-view.tsx"

cat -n "$FILE" | sed -n '1,25p'
echo '---'
cat -n "$FILE" | sed -n '50,75p'

Repository: cicero-im/plate

Length of output: 1741


Inline single-use layout literals (PAGE_W, MARGIN).

PAGE_W and MARGIN are single-use—inline their numeric values to match the repo style rule.

Proposed diff
-const PAGE_W = 794; // A4 @ 96dpi
-const MARGIN = 96; // 1in
-
 function makeValue(): Value {
@@
           boxShadow: '0 2px 12px rgba(15,23,42,0.12)',
           margin: '0 auto',
-          padding: MARGIN,
+          padding: 96, // 1in
           position: 'relative',
-          width: PAGE_W,
+          width: 794, // A4 @ 96dpi
         }}
       >
📝 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 PAGE_W = 794; // A4 @ 96dpi
const MARGIN = 96; // 1in
function makeValue(): Value {
🤖 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 constants; remove the constants PAGE_W and
MARGIN and replace their usages with the literal numbers (794 and 96) directly
where referenced (e.g., in the layout calculations inside pagination2-view.tsx)
so the numeric values are inlined and the unused const declarations are deleted.

Comment on lines +800 to +803
{
dependencies: ['@platejs/pagination'],
description: 'A toolbar button for page break markers.',
files: [{ path: 'ui/pagination-toolbar-button.tsx', type: 'registry:ui' }],

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

Update the component description to match behavior.

The button toggles pagination on/off rather than inserting “page break markers”; the registry description should reflect that to avoid UI/docs mismatch.

✏️ Suggested wording tweak
-    description: 'A toolbar button for page break markers.',
+    description: 'A toolbar button to toggle pagination page-break guides.',
📝 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
{
dependencies: ['@platejs/pagination'],
description: 'A toolbar button for page break markers.',
files: [{ path: 'ui/pagination-toolbar-button.tsx', type: 'registry:ui' }],
{
dependencies: ['`@platejs/pagination`'],
description: 'A toolbar button to toggle pagination page-break guides.',
files: [{ path: 'ui/pagination-toolbar-button.tsx', type: 'registry:ui' }],
🤖 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/registry/registry-ui.ts` around lines 800 - 803, Update the
registry entry's description to accurately reflect the component behavior:
replace the current "A toolbar button for page break markers." with wording that
says the button toggles pagination on and off (e.g., "A toolbar button that
toggles pagination on/off."). Locate the object containing description and
files: the entry with files: [{ path: 'ui/pagination-toolbar-button.tsx', type:
'registry:ui' }] and update its description string accordingly so docs/UI match
implementation.

Comment on lines +64 to +69
```
Slate value → snapshot(flat blocks + stable ids + slate paths)
→ measure (real DOM block heights, cached) → MeasuredSnapshot
→ composeLayout(pure, deterministic) → LayoutOutput
→ render overlay (page chrome + projected content) + mapping
```

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

Add a language tag to the fenced code block.

Line 64 starts a fenced block without a language identifier; please mark it (e.g. text) for markdown consistency.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 64-64: Fenced code blocks should have a language specified

(MD040, fenced-code-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-21-pagination-rewrite.md` around lines 64 - 69, The fenced
code block containing "Slate value → snapshot(flat blocks + stable ids + slate
paths) → measure (real DOM block heights, cached) → MeasuredSnapshot →
composeLayout(pure, deterministic) → LayoutOutput → render overlay (page chrome
+ projected content) + mapping" is missing a language tag; update the opening
triple-backticks to include a language identifier (for example, use ```text or
```diff) so Markdown renders consistently and syntax/highlighting tools can
process it.

Comment thread package.json
"reinstall": "bash tooling/scripts/reinstall.sh",
"release": "pnpm build && pnpm changeset publish",
"shadcn:build": "pnpm --filter www shadcn:build",
"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",

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether opennextjs-cloudflare is pinned anywhere in workspace manifests.
rg -n --iglob 'package.json' '"opennextjs-cloudflare"|deploy:playground'

Repository: cicero-im/plate

Length of output: 308


Pin the opennextjs-cloudflare CLI version (avoid ad-hoc npx)
In package.json line 55, deploy:playground runs npx opennextjs-cloudflare build/deploy with no opennextjs-cloudflare dependency/devDependency entry anywhere else in any package.json, so the CLI version can drift and make deploys non-reproducible.

🤖 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 55, The deploy script "deploy:playground" uses ad-hoc
npx for opennextjs-cloudflare which can drift; add a pinned
opennextjs-cloudflare version to package.json devDependencies (e.g.,
"opennextjs-cloudflare": "x.y.z") and update the "deploy:playground" script to
invoke the local CLI (for example replace `npx opennextjs-cloudflare build &&
npx opennextjs-cloudflare deploy` with `pnpm exec opennextjs-cloudflare build &&
pnpm exec opennextjs-cloudflare deploy` or the equivalent package-manager-local
invocation) so the CLI version is reproducible and installed via package.json.

Comment on lines +107 to +114
- [ ] **Task 9.** Verify typecheck passes for the template.

**Rationale**: After fixing the toolbar types and regenerating vendor copy, run `cd templates/plate-playground-template && bun typecheck` to ensure no TypeScript errors.

- [ ] **Task 10.** Verify lint passes for the template.

**Rationale**: Run `cd templates/plate-playground-template && bun lint:fix` to auto-fix and verify no lint errors.

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 the repo-required verification command forms.

The plan should use bun run typecheck and bun run lint:fix consistently (instead of bun typecheck / bun lint:fix) to match repo verification policy and avoid ambiguous execution paths.

As per coding guidelines templates/plate-playground-template/**/*.{js,ts,tsx,jsx}: ... At minimum: bun run typecheck, bun run lint:fix, and bun run build ....

🤖 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 107 - 114,
Update Task 9 and Task 10 to use the repo-required command forms: replace `bun
typecheck` with `bun run typecheck` in Task 9 and replace `bun lint:fix` with
`bun run lint:fix` in Task 10 (these are the entries labeled "Task 9." and "Task
10." in the plan for the template verification steps), ensuring the instructions
read to run `cd templates/plate-playground-template && bun run typecheck` and
`cd templates/plate-playground-template && bun run lint:fix` so they match the
repo verification policy and the coding guidelines.

"@platejs/table": "^53.0.0",
"@platejs/toc": "^53.0.0",
"@platejs/toggle": "^53.0.0",
"@platejs/yjs": "^53.0.0",

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 hand-edit template manifests; regenerate from source inputs.

templates/plate-playground-template/package.json is CI-controlled output, so these dependency edits should come from source registry/package/workflow changes and regeneration, not direct template edits.

As per coding guidelines templates/** is CI-controlled output and “Never manually edit or commit template source, manifests, or lockfiles.”

Also applies to: 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 61, Revert the
manual edit adding/updating the "`@platejs/yjs`" dependency in the generated
template manifest and instead update the upstream source inputs (the package
registry or package metadata used to build templates) or the template generation
workflow; then run the template/regeneration task to produce a new
templates/plate-playground-template/package.json so the change is made by the CI
generation step rather than committing a hand-edited templates manifest.

Comment on lines +17 to +20
<ToolbarButton
{...props}
onClick={() => editor.setOption(PaginationPlugin, 'enabled', !enabled)}
pressed={enabled}

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

Preserve caller onClick when forwarding toolbar props.

Line 19 overrides props.onClick, so any consumer callback passed through this public component is ignored. Compose handlers instead of replacing.

Proposed fix
     <ToolbarButton
       {...props}
-      onClick={() => editor.setOption(PaginationPlugin, 'enabled', !enabled)}
-      pressed={enabled}
+      onClick={(event) => {
+        props.onClick?.(event);
+        editor.setOption(PaginationPlugin, 'enabled', !enabled);
+      }}
+      pressed={Boolean(enabled)}
       tooltip="Page breaks"
     >
📝 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
<ToolbarButton
{...props}
onClick={() => editor.setOption(PaginationPlugin, 'enabled', !enabled)}
pressed={enabled}
<ToolbarButton
{...props}
onClick={(event) => {
props.onClick?.(event);
editor.setOption(PaginationPlugin, 'enabled', !enabled);
}}
pressed={Boolean(enabled)}
tooltip="Page breaks"
>
🤖 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 17 - 20, The ToolbarButton usage in PaginationToolbarButton
overrides any caller onClick in props, so preserve and compose the original
handler: retrieve props.onClick (e.g., const { onClick: callerOnClick, ...rest }
= props) and pass a new onClick that first invokes callerOnClick(event) if
present, then performs editor.setOption(PaginationPlugin, 'enabled', !enabled);
keep pressed={enabled} and spread the remaining props (rest) into ToolbarButton
so existing consumers' callbacks are not lost.

Comment on lines 16 to 18
"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

❓ Verification inconclusive

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import json, re, pathlib, sys

pkg_dir = pathlib.Path("templates/plate-playground-template/vendor/platejs-pagination")
pkg_json = pkg_dir / "package.json"
pkg = json.loads(pkg_json.read_text())

declared = set()
for key in ("dependencies", "peerDependencies", "optionalDependencies"):
    declared.update((pkg.get(key) or {}).keys())

dist_dir = pkg_dir / "dist"
if not dist_dir.exists():
    print("dist/ not found; cannot verify runtime import contract from built output.")
    sys.exit(0)

pattern = re.compile(
    r"""(?:import|export)\s+(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)"""
)

externals = set()
for file in dist_dir.rglob("*.js"):
    text = file.read_text(errors="ignore")
    for m in pattern.finditer(text):
        mod = m.group(1) or m.group(2)
        if not mod or mod.startswith(".") or mod.startswith("/") or mod.startswith("node:"):
            continue
        base = "/".join(mod.split("/")[:2]) if mod.startswith("@") else mod.split("/")[0]
        externals.add(base)

missing = sorted(x for x in externals if x not in declared)

print("Declared deps/peers:", sorted(declared))
print("Runtime externals in dist:", sorted(externals))
print("Undeclared externals:", missing)
PY

Repository: cicero-im/plate

Length of output: 133


Re-add peerDependencies for runtime externals (or validate generated dist doesn’t require them)

In templates/plate-playground-template/vendor/platejs-pagination/package.json, dependencies only include @chenglou/pretext (lines 16-18). The dist/ folder is missing in this repo snapshot, so the “runtime externals imported by built JS” check can’t be evaluated here; if the generated runtime dist still imports packages like react, react-dom, or platejs, they must be declared as peerDependencies (fix at the CI/source level since templates/** is CI-controlled output).

Run the dist-imports check once dist/ is available (from the regenerated CI output or the upstream published build). Expected: Undeclared externals is empty; otherwise add them back as peerDependencies.

🤖 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 16 - 18, The package.json in
templates/plate-playground-template/vendor/platejs-pagination currently only
lists "`@chenglou/pretext`" under "dependencies" but may be missing runtime
externals declared as "peerDependencies"; regenerate or obtain the built dist/
and run the dist-imports check, and if the built JS imports external packages
such as "react", "react-dom", "platejs" (or other runtime libs), add them to
package.json's "peerDependencies" (not "dependencies") so runtime externals are
declared; ensure the CI/output that produces templates/** is updated so the
peerDependencies are present in the generated package.json whenever the dist
requires them.

Comment on lines +93 to +95
expect(box!.x).toBeGreaterThanOrEqual(0);
expect(box!.x + box!.width).toBeLessThanOrEqual(600);
}

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

Prevent sub-pixel edge assertions from flaking.

boundingBox() can return fractional values, so exact <= 600 may fail on DPR rounding even when labels are visually on-screen. Add a small epsilon.

🔧 Suggested patch
-      expect(box!.x + box!.width).toBeLessThanOrEqual(600);
+      const viewportWidth = 600;
+      const epsilon = 1;
+      expect(box!.x + box!.width).toBeLessThanOrEqual(viewportWidth + epsilon);
📝 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
expect(box!.x).toBeGreaterThanOrEqual(0);
expect(box!.x + box!.width).toBeLessThanOrEqual(600);
}
expect(box!.x).toBeGreaterThanOrEqual(0);
const epsilon = 1;
expect(box!.x + box!.width).toBeLessThanOrEqual(600 + epsilon);
}
🤖 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 `@tooling/e2e/pagination.spec.ts` around lines 93 - 95, boundingBox() may
return fractional sub-pixel values causing flaky assertions; update the two
assertions that reference box!.x and box!.x + box!.width (in the test using
boundingBox()) to tolerate a tiny epsilon (e.g., 0.5): check box!.x >= -epsilon
and box!.x + box!.width <= 600 + epsilon (or add/subtract epsilon in the
toBeGreaterThanOrEqual/toBeLessThanOrEqual checks) so DPR rounding won’t cause
false negatives.

arthrod added a commit that referenced this pull request May 29, 2026
…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 added a commit that referenced this pull request May 29, 2026
…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>
arthrod added a commit that referenced this pull request May 29, 2026
…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>
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.

1 participant