Codex/playground pagination toggle - #438
Conversation
…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>
… 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>
📝 WalkthroughWalkthroughIntroduces 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. ChangesPretext pagination pipeline and integrations
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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.
| 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] : []; | ||
| }); |
There was a problem hiding this comment.
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.
| 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; | |
| }); | |
| } |
| 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, | ||
| }; | ||
| }; | ||
| } |
There was a problem hiding this comment.
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; | ||
|
|
There was a problem hiding this comment.
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.
| return Math.max(1, Math.ceil(heightPx / lineHeightPx)); |
| const topOf = (el: HTMLElement) => | ||
| editable.offsetTop + (el.getBoundingClientRect().top - editableTop); |
There was a problem hiding this comment.
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.
| const topOf = (el: HTMLElement) => | |
| editable.offsetTop + (el.getBoundingClientRect().top - editableTop); | |
| const topOf = (el: HTMLElement | null) => | |
| el ? editable.offsetTop + (el.getBoundingClientRect().top - editableTop) : 0; |
| const lineHeight = | ||
| Number.parseFloat(getComputedStyle(el).lineHeight) || 0; |
There was a problem hiding this comment.
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.
| 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; |
|
@coderabbitai conejo review plz |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (2)
docs/plans/2026-05-22-pagination-rewrite-v2.md (1)
3-5: ⚡ Quick winRewrite 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 winAvoid 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
⛔ Files ignored due to path filters (12)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamltemplates/plate-playground-template/bun.lockis excluded by!**/*.locktemplates/plate-playground-template/vendor/platejs-pagination/dist/index-BmXRyAOt.d.tsis excluded by!**/dist/**templates/plate-playground-template/vendor/platejs-pagination/dist/index-BmXRyAOt.d.ts.mapis excluded by!**/dist/**,!**/*.maptemplates/plate-playground-template/vendor/platejs-pagination/dist/index.d.tsis excluded by!**/dist/**templates/plate-playground-template/vendor/platejs-pagination/dist/index.jsis excluded by!**/dist/**templates/plate-playground-template/vendor/platejs-pagination/dist/paginate-c73WStbw.jsis excluded by!**/dist/**templates/plate-playground-template/vendor/platejs-pagination/dist/paginate-c73WStbw.js.mapis excluded by!**/dist/**,!**/*.maptemplates/plate-playground-template/vendor/platejs-pagination/dist/react/index.d.tsis excluded by!**/dist/**templates/plate-playground-template/vendor/platejs-pagination/dist/react/index.d.ts.mapis excluded by!**/dist/**,!**/*.maptemplates/plate-playground-template/vendor/platejs-pagination/dist/react/index.jsis excluded by!**/dist/**templates/plate-playground-template/vendor/platejs-pagination/dist/react/index.js.mapis 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.mdAGENTS.mdapps/www/next-env.d.tsapps/www/package.jsonapps/www/src/app/dev/pagination2/page.tsxapps/www/src/app/dev/pagination2/pagination2-view.tsxapps/www/src/registry/blocks/editor-ai/components/editor/editor-kit.tsxapps/www/src/registry/components/editor/editor-kit.tsxapps/www/src/registry/components/editor/plugins/pagination-kit.tsxapps/www/src/registry/registry-blocks.tsapps/www/src/registry/registry-kits.tsapps/www/src/registry/registry-ui.tsapps/www/src/registry/ui/fixed-toolbar-buttons.tsxapps/www/src/registry/ui/pagination-toolbar-button.tsxbunfig.tomldiary.mddocs/plans/2026-05-15-pagination-plugin-refactor.mddocs/plans/2026-05-20-pagination-wiring.mddocs/plans/2026-05-21-pagination-rewrite.mddocs/plans/2026-05-22-pagination-rewrite-v2.mddocs/plans/2026-05-23-pagination-impl-plan.mddocs/plans/2026-05-24-pagination-demo-print-regression.mddocs/plans/premirror-audit-findings.mddocs/solutions/ui-bugs/2026-05-24-pagination-must-measure-wrapped-block-elements.mdpackage.jsonpackages/pagination/README.mdpackages/pagination/failures.mdpackages/pagination/package.jsonpackages/pagination/src/index.tspackages/pagination/src/layout/__tests__/compose.spec.tspackages/pagination/src/layout/__tests__/continuous.spec.tspackages/pagination/src/layout/__tests__/mapping.spec.tspackages/pagination/src/layout/__tests__/projection.spec.tspackages/pagination/src/layout/__tests__/snapshot.spec.tspackages/pagination/src/layout/compose.tspackages/pagination/src/layout/continuous.tspackages/pagination/src/layout/index.tspackages/pagination/src/layout/mapping.tspackages/pagination/src/layout/projection.tspackages/pagination/src/layout/snapshot.tspackages/pagination/src/layout/types.tspackages/pagination/src/lib/BasePaginationPlugin.tspackages/pagination/src/lib/__tests__/BasePaginationPlugin.spec.tspackages/pagination/src/lib/__tests__/registry.spec.tspackages/pagination/src/lib/allocate-footnotes.tspackages/pagination/src/lib/base-footer-plugin.tspackages/pagination/src/lib/base-header-plugin.tspackages/pagination/src/lib/base-page-break-plugin.tspackages/pagination/src/lib/base-pagination-plugin.tspackages/pagination/src/lib/base-pagination-plugins.spec.tspackages/pagination/src/lib/index.tspackages/pagination/src/lib/internal/font-from-style.tspackages/pagination/src/lib/internal/keys.tspackages/pagination/src/lib/internal/marks-fingerprint.tspackages/pagination/src/lib/internal/measure-cache.spec.tspackages/pagination/src/lib/internal/measure-cache.tspackages/pagination/src/lib/internal/page-size-presets.tspackages/pagination/src/lib/internal/page-state.tspackages/pagination/src/lib/paginate.spec.tspackages/pagination/src/lib/paginate.tspackages/pagination/src/lib/queries/getPageOfPath.tspackages/pagination/src/lib/queries/getPaginationPages.tspackages/pagination/src/lib/queries/hasChromeBlock.tspackages/pagination/src/lib/queries/index.tspackages/pagination/src/lib/registry.tspackages/pagination/src/lib/transforms/enforceHeaderFooterInvariants.tspackages/pagination/src/lib/transforms/ensureFooter.tspackages/pagination/src/lib/transforms/ensureHeader.tspackages/pagination/src/lib/transforms/index.tspackages/pagination/src/lib/transforms/insertPageBreak.tspackages/pagination/src/lib/transforms/removeNodesByType.tspackages/pagination/src/lib/transforms/replaceFooter.tspackages/pagination/src/lib/transforms/replaceHeader.tspackages/pagination/src/lib/transforms/toggleFooter.tspackages/pagination/src/lib/transforms/toggleHeader.tspackages/pagination/src/lib/types.tspackages/pagination/src/measure/__tests__/measure.spec.tspackages/pagination/src/measure/__tests__/pretext.spec.tspackages/pagination/src/measure/index.tspackages/pagination/src/measure/measure.tspackages/pagination/src/measure/pretext.tspackages/pagination/src/react/PaginationPlugin.tsxpackages/pagination/src/react/__tests__/domMeasure.spec.tspackages/pagination/src/react/__tests__/geometry.spec.tspackages/pagination/src/react/alignContent.tspackages/pagination/src/react/domMeasure.tspackages/pagination/src/react/footer-plugin.tspackages/pagination/src/react/footnote-portal.tsxpackages/pagination/src/react/geometry.tspackages/pagination/src/react/header-plugin.tspackages/pagination/src/react/index.tspackages/pagination/src/react/internal/use-page-layout.tspackages/pagination/src/react/page-break-plugin.tspackages/pagination/src/react/page-frame.tsxpackages/pagination/src/react/page-overlay.tsxpackages/pagination/src/react/pagination-plugin.tspackages/pagination/src/react/use-pretext-measurer.tspackages/pagination/src/static/footer-element-static.tsxpackages/pagination/src/static/header-element-static.tsxpackages/pagination/src/static/index.tspackages/pagination/src/static/page-break-element-static.tsxpackages/pagination/tsconfig.jsonplans/2026-05-15-fix-pagination-plugin-tdd-v2.mdplans/2026-05-15-fix-pagination-plugin-v1.mdplans/2026-05-16-pagination-end-to-end-fix-v1.mdtemplates/plate-playground-template/package.jsontemplates/plate-playground-template/src/app/dev/pagination2/page.tsxtemplates/plate-playground-template/src/app/dev/pagination2/pagination2-view.tsxtemplates/plate-playground-template/src/app/editor/page.tsxtemplates/plate-playground-template/src/components/editor/editor-kit.tsxtemplates/plate-playground-template/src/components/editor/plugins/pagination-kit.tsxtemplates/plate-playground-template/src/components/ui/fixed-toolbar-buttons.tsxtemplates/plate-playground-template/src/components/ui/pagination-toolbar-button.tsxtemplates/plate-playground-template/vendor/.gitignoretemplates/plate-playground-template/vendor/platejs-pagination/package.jsontooling/config/tsdown.config.tstooling/e2e/pagination.spec.tstooling/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
| 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. |
There was a problem hiding this comment.
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.
| const PAGE_W = 794; // A4 @ 96dpi | ||
| const MARGIN = 96; // 1in | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n '\b(PAGE_W|MARGIN)\b' apps/www/src/app/dev/pagination2/pagination2-view.tsxRepository: 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.
| 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.
| { | ||
| dependencies: ['@platejs/pagination'], | ||
| description: 'A toolbar button for page break markers.', | ||
| files: [{ path: 'ui/pagination-toolbar-button.tsx', type: 'registry:ui' }], |
There was a problem hiding this comment.
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.
| { | |
| 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.
| ``` | ||
| 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 | ||
| ``` |
There was a problem hiding this comment.
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.
| "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", |
There was a problem hiding this comment.
🧩 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.
| - [ ] **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. | ||
|
|
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
| <ToolbarButton | ||
| {...props} | ||
| onClick={() => editor.setOption(PaginationPlugin, 'enabled', !enabled)} | ||
| pressed={enabled} |
There was a problem hiding this comment.
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.
| <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.
| "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" | ||
| } |
There was a problem hiding this comment.
❓ 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)
PYRepository: 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.
| expect(box!.x).toBeGreaterThanOrEqual(0); | ||
| expect(box!.x + box!.width).toBeLessThanOrEqual(600); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
…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>
…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>
Checklist
pnpm typecheckpnpm lint:fixbun testpnpm brlpnpm changesetSummary by CodeRabbit
New Features
Bug Fixes
Improvements