Skip to content

chore: enhance PR template + add title validation workflow - #439

Draft
arthrod wants to merge 48 commits into
mainfrom
claude/cool-tesla-RT0Fo
Draft

chore: enhance PR template + add title validation workflow#439
arthrod wants to merge 48 commits into
mainfrom
claude/cool-tesla-RT0Fo

Conversation

@arthrod

@arthrod arthrod commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements pull request automation improvements:

  1. Enhanced PR template — adds Summary, Type of Change, and Related Issues sections before the existing checklist, so contributors provide context alongside the verification checklist.

  2. pr-validate.yml workflow — lightweight title gate that:

    • Rejects auto-generated Codex/… style titles
    • Enforces a minimum 10-character title length

Type of Change

  • Documentation / chore

Related Issues

N/A


Checklist

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

https://claude.ai/code/session_011PTTNeLZPvRTJF58SWfyU1


Generated by Claude Code

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 18 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>
- Add Summary, Type of Change, Related Issues sections to PR template
- Keep existing Plate checklist (typecheck/lint/test/brl/changeset/changelog)
- Add pr-validate.yml: rejects auto-generated Codex/ titles and enforces min 10-char title

https://claude.ai/code/session_011PTTNeLZPvRTJF58SWfyU1
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7063469e-19f9-42ca-87c9-1ad7f2e9c1f3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/cool-tesla-RT0Fo

Comment @coderabbitai help to get the list of available commands and usage tips.

@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 executes a major architectural shift for the @platejs/pagination package, replacing the previous document-mutating engine with a derived projection pipeline that leverages @chenglou/pretext for hih-fidelity text measurement. The implementation introduces a structured layout process involving snapshotting, measurement, and composition, supported by a React-based overlay for continuous-view page breaks. Review feedback focuses on enhancing the performance and reliability of the DOM measurement logic, specifically recommending the reuse of a robust line-height resolution helper to prevent NaN errors and caching top-level block elements to eliminate O(N^2) complexity during measurement passes.

});
}

function resolveLineHeight(style: CSSStyleDeclaration): number {

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

Exporting resolveLineHeight allows it to be reused in other parts of the pagination plugin, such as the overlay renderer, ensuring consistent line-height resolution across measurement and rendering.

Suggested change
function resolveLineHeight(style: CSSStyleDeclaration): number {
export function resolveLineHeight(style: CSSStyleDeclaration): number {

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.

medium

topLevelBlockElements is called for every block being measured. Since it iterates over all children of the editable, this results in O(N^2) complexity for a document with N blocks during a full measurement pass. Calling it once at the start of createDomMeasure and reusing the result improves performance significantly for large documents.

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,
    };
  };
}

import { measureSnapshot } from '../measure/measure';
import { BasePaginationPlugin } from '../lib/BasePaginationPlugin';
import { getLayoutRegistry, invalidateLayoutRegistry } from '../lib/registry';
import { createDomMeasure, topLevelBlockElements } from './domMeasure';

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

Import resolveLineHeight to use it in the overlay renderer for robust line-height resolution.

Suggested change
import { createDomMeasure, topLevelBlockElements } from './domMeasure';
import { createDomMeasure, resolveLineHeight, topLevelBlockElements } from './domMeasure';

// lineStart > 0 (future line-split mode) offsets within the block by the
// pretext line count; 0 is a clean whole-block top.
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

Using Number.parseFloat on lineHeight can return NaN if the value is "normal", which results in a 0 line height. This would cause incorrect positioning if a block is split across pages (where lineStart > 0). Using the resolveLineHeight helper provides a robust fallback.

        const lineHeight = resolveLineHeight(getComputedStyle(el));

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants