Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pagination-wrapped-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@platejs/pagination": patch
---

Fix page-break markers for editors whose block UI wraps Slate elements.
1 change: 1 addition & 0 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"@platejs/math": "workspace:^",
"@platejs/media": "workspace:^",
"@platejs/mention": "workspace:^",
"@platejs/pagination": "workspace:^",
"@platejs/playwright": "workspace:^",
"@platejs/resizable": "workspace:^",
"@platejs/selection": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { MarkdownKit } from '@/registry/components/editor/plugins/markdown-kit';
import { MathKit } from '@/registry/components/editor/plugins/math-kit';
import { MediaKit } from '@/registry/components/editor/plugins/media-kit';
import { MentionKit } from '@/registry/components/editor/plugins/mention-kit';
import { PaginationKit } from '@/registry/components/editor/plugins/pagination-kit';
import { SlashKit } from '@/registry/components/editor/plugins/slash-kit';
import { SuggestionKit } from '@/registry/components/editor/plugins/suggestion-kit';
import { TableKit } from '@/registry/components/editor/plugins/table-kit';
Expand Down Expand Up @@ -65,6 +66,9 @@ export const EditorKit = [
...AlignKit,
...LineHeightKit,

// Layout
...PaginationKit,

// Collaboration
...DiscussionKit,
...CommentKit,
Expand Down
4 changes: 4 additions & 0 deletions apps/www/src/registry/components/editor/editor-kit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { MarkdownKit } from './plugins/markdown-kit';
import { MathKit } from './plugins/math-kit';
import { MediaKit } from './plugins/media-kit';
import { MentionKit } from './plugins/mention-kit';
import { PaginationKit } from './plugins/pagination-kit';
import { SlashKit } from './plugins/slash-kit';
import { SuggestionKit } from './plugins/suggestion-kit';
import { TableKit } from './plugins/table-kit';
Expand Down Expand Up @@ -67,6 +68,9 @@ export const EditorKit = [
...AlignKit,
...LineHeightKit,

// Layout
...PaginationKit,

// Collaboration
...DiscussionKit,
...CommentKit,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client';

import { PaginationPlugin } from '@platejs/pagination/react';

// Continuous-view page-break overlay. Enabled by default so demos show page
// markers immediately; the toolbar button toggles it at runtime.
export const PaginationKit = [PaginationPlugin];
1 change: 1 addition & 0 deletions apps/www/src/registry/registry-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const registryBlocks: Registry['items'] = [
'math-kit',
'media-kit',
'mention-kit',
'pagination-kit',
'slash-kit',
'suggestion-kit',
'table-kit',
Expand Down
12 changes: 12 additions & 0 deletions apps/www/src/registry/registry-kits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ export const registryKits: Registry['items'] = [
'math-kit',
'media-kit',
'mention-kit',
'pagination-kit',
'slash-kit',
'suggestion-kit',
'table-kit',
Expand All @@ -670,6 +671,17 @@ export const registryKits: Registry['items'] = [
],
type: 'registry:component',
},
{
dependencies: ['@platejs/pagination'],
files: [
{
path: 'components/editor/plugins/pagination-kit.tsx',
type: 'registry:component',
},
],
name: 'pagination-kit',
type: 'registry:component',
},
{
dependencies: ['@platejs/emoji', '@emoji-mart/data@1.2.1'],
files: [
Expand Down
10 changes: 10 additions & 0 deletions apps/www/src/registry/registry-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ export const uiComponents: Registry['items'] = [
'media-toolbar-button',
'mode-toolbar-button',
'more-toolbar-button',
'pagination-toolbar-button',
'table-toolbar-button',
'toggle-toolbar-button',
'turn-into-toolbar-button',
Expand Down Expand Up @@ -796,6 +797,15 @@ export const uiComponents: Registry['items'] = [
title: 'More Toolbar Button',
type: 'registry:ui',
},
{
dependencies: ['@platejs/pagination'],
description: 'A toolbar button for page break markers.',
files: [{ path: 'ui/pagination-toolbar-button.tsx', type: 'registry:ui' }],
name: 'pagination-toolbar-button',
registryDependencies: ['toolbar'],
title: 'Pagination Toolbar Button',
type: 'registry:ui',
},
{
dependencies: ['@platejs/resizable'],
description: 'A resizable wrapper with resize handles.',
Expand Down
2 changes: 2 additions & 0 deletions apps/www/src/registry/ui/fixed-toolbar-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { PaginationToolbarButton } from './pagination-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { ToggleToolbarButton } from './toggle-toolbar-button';
import { ToolbarGroup } from './toolbar';
Expand Down Expand Up @@ -125,6 +126,7 @@ export function FixedToolbarButtons() {
<BulletedListToolbarButton />
<TodoListToolbarButton />
<ToggleToolbarButton />
<PaginationToolbarButton />
</ToolbarGroup>

<ToolbarGroup>
Expand Down
26 changes: 26 additions & 0 deletions apps/www/src/registry/ui/pagination-toolbar-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';

import { PaginationPlugin } from '@platejs/pagination/react';
import { SeparatorHorizontalIcon } from 'lucide-react';
import { useEditorRef, usePluginOption } from 'platejs/react';
import type * as React from 'react';

import { ToolbarButton } from './toolbar';

export function PaginationToolbarButton(
props: React.ComponentProps<typeof ToolbarButton>
) {
const editor = useEditorRef();
const enabled = usePluginOption(PaginationPlugin, 'enabled');

return (
<ToolbarButton
{...props}
onClick={() => editor.setOption(PaginationPlugin, 'enabled', !enabled)}
pressed={enabled}
tooltip="Page breaks"
>
<SeparatorHorizontalIcon />
</ToolbarButton>
);
}
68 changes: 68 additions & 0 deletions docs/plans/2026-05-24-pagination-demo-print-regression.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Pagination Demo Print Regression

## Goal

Find why the demo printed a page successfully on branch
`codex/pagination-e2e-tests` but not on `codex/playground-pagination-toggle`,
then fix this branch.

## Constraints

- Pagination work must use pretext for faithful pagination.
- Do not edit `templates/**` manually.
- If code changes, verify in the same turn.
- If browser surface changes, verify with `dev-browser` before handoff.

## Plan

- [x] Load repo skills and rules.
- [x] Check existing pagination plans and documented learnings.
- [ ] Reproduce the current failure.
- [ ] Compare current branch with `codex/pagination-e2e-tests`.
- [ ] Identify root cause before patching.
- [ ] Implement focused fix.
- [ ] Run tests, coverage, lint/type checks, and browser proof as applicable.
- [ ] Evaluate whether to capture a `docs/solutions/` learning.

## Findings

- Current branch: `codex/playground-pagination-toggle`.
- Known-good comparison branch exists locally: `codex/pagination-e2e-tests`.
- `docs/solutions/patterns/critical-patterns.md` is referenced by the skill but
is absent in this repo.
- Existing pagination plans emphasize derived layout, pretext-based measurement,
and browser verification for render surfaces.
- The repo app route `/dev/pagination2` renders pagination labels on this branch:
`Page 1 of 4` through `Page 4 of 4`.
- The template `/editor` route renders zero page markers on load because
`PaginationKit` configures `PaginationPlugin` with `enabled: false`.
- Root cause: the comparison branch tests a direct demo plugin install; this
branch's playground integration starts the plugin disabled.
- Second root cause: full playground editors wrap each top-level Slate element
in block UI chrome (`div.relative.group`), while `topLevelBlockElements`
only measured direct `[data-slate-node="element"]` children. That made every
block fall back to one line, so the full editor composed as a single page.

## Progress

- 2026-05-24: Started branch comparison and regression investigation.
- 2026-05-24: Reproduced template `/editor` missing pagination markers; direct
`/dev/pagination2` demo remains green.
- 2026-05-24: Fixed measurement lookup for wrapped block DOM, enabled pagination
by default in the playground integration, added registry wiring and E2E tests.
- 2026-05-24: Verified template `/editor` renders `Page 1 of 2` / `Page 2 of 2`.

## Verification

- `pnpm install`
- `bun test --coverage packages/pagination/src` — 56 pass; package lines are
100% except `react/domMeasure.ts` at 81.48%; Bun did not emit branch coverage.
- `pnpm turbo build --filter=./packages/pagination`
- `pnpm turbo typecheck --filter=./packages/pagination --filter=./apps/www`
- `pnpm lint:fix`
- `bun typecheck` in `templates/plate-playground-template`
- `PLAYWRIGHT_BASE_URL=http://localhost:3002 pnpm exec playwright test tooling/e2e/pagination.spec.ts --browser=chromium --workers=1`
- Browser probe for `http://localhost:3001/editor` — 1 break line, 1 page marker,
labels `Page 1 of 2`, `Page 2 of 2`, no console errors.
- `dev-browser --connect http://127.0.0.1:9222 --help` blocked: CLI not installed
in this environment.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
module: Pagination
date: 2026-05-24
problem_type: ui_bug
component: tooling
symptoms:
- "The minimal pagination demo rendered page markers, but the full playground editor rendered none"
- "Template /editor showed zero pagination break lines and labels on load"
- "The full editor content was taller than one page, but pagination composed it as one page"
root_cause: logic_error
resolution_type: code_fix
severity: medium
tags:
- pagination
- pretext
- dom-measurement
- block-wrappers
- registry
---

# Pagination must measure wrapped block elements

## Problem

Pagination worked in the focused `/dev/pagination2` demo but did not render page markers in the full playground editor.

The two surfaces used the same pagination pipeline. The difference was the editor DOM shape.

## Root Cause

`topLevelBlockElements` queried only direct editable children:

```ts
editable.querySelectorAll(':scope > [data-slate-node="element"]');
```

That matched the focused demo, where Slate elements were direct children of the editable.

The full playground block UI wraps each top-level Slate element in an outer UI wrapper. In that DOM shape, the direct-child query returns no blocks. Pagination then falls back to synthetic one-line measurements and can compose the whole document as a single page.

There was also an integration issue: the playground template configured `PaginationPlugin` with `enabled: false`, so even a correct layout could stay invisible on first load.

## Fix

Resolve one top-level Slate element per editable child:

- use the child itself when it is a Slate element
- otherwise use the first nested Slate element inside that child
- preserve document order and avoid querying all descendants as independent blocks

Enable pagination in the editor kit integration by default and register the pagination toolbar control with the registry source.

## Why This Works

Pagination needs the same top-level block sequence that Slate renders, not a flat list of every descendant element.

Looking through each editable child preserves that top-level sequence while tolerating wrapper components added by block UI chrome.

## Verification

These checks passed:

```bash
bun test --coverage packages/pagination/src
pnpm turbo build --filter=./packages/pagination
pnpm turbo typecheck --filter=./packages/pagination
pnpm lint:fix
PLAYWRIGHT_BASE_URL=http://localhost:3002 pnpm exec playwright test tooling/e2e/pagination.spec.ts --browser=chromium --workers=1
```

Template `/editor` browser proof after refreshing the local pagination vendor package:

```json
{
"lineCount": 1,
"labels": ["Page 1 of 2", "Page 2 of 2"],
"errors": []
}
```

## Prevention

Do not use direct-child-only selectors for Plate editor block measurement when block UI wrappers can sit between the editable and Slate elements.

Pagination browser checks should cover both the minimal demo route and a full editor route with block wrappers.

When verifying a generated template against local package code, rebuild the package, refresh the template's vendored package copy, and reinstall the template dependency graph before testing the page.
12 changes: 7 additions & 5 deletions packages/pagination/src/react/PaginationPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,22 @@ export const PaginationPlugin = toPlatePlugin(BasePaginationPlugin, {
setOption('breaks', getContinuousBreaks(layout));
});

// A width change re-wraps text and changes pagination. Invalidate the layout
// and force a re-render so the dirty-recompute effect re-measures at the new
// width and the overlay re-anchors to the new block tops.
// A width change re-wraps text and changes pagination. Always invalidate the
// cached layout, but only re-render immediately while the overlay is visible.
useEffect(() => {
const editable = editor.api.toDOMNode(editor);
if (!editable || typeof ResizeObserver === 'undefined') return;

const observer = new ResizeObserver(() => {
invalidateLayoutRegistry(editor);
forceRecompute((n) => n + 1);

if (enabled) {
forceRecompute((n) => n + 1);
}
});
observer.observe(editable);

return () => observer.disconnect();
}, [editor]);
}, [editor, enabled]);
},
});
Loading