Skip to content

Fix table row splitting: click, cursor, selection, and rendering#145

Merged
hackerwins merged 25 commits into
mainfrom
fix/docs-arrow-navigation-crash
Apr 20, 2026
Merged

Fix table row splitting: click, cursor, selection, and rendering#145
hackerwins merged 25 commits into
mainfrom
fix/docs-arrow-navigation-crash

Conversation

@hackerwins

@hackerwins hackerwins commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Split tall table rows across page boundaries so content renders continuously
  • Fix click resolution, cursor placement, and cell selection for split rows with nested tables
  • Fix nested table background rendering order so selection highlights remain visible
  • Fix Cmd+A delete leaving empty table shells instead of empty paragraphs

Key changes

Row splitting (pagination.ts, table-layout.ts, table-renderer.ts, doc-canvas.ts):

  • Extend PageLine with rowSplitOffset/rowSplitHeight metadata
  • Detect rows exceeding page space and compute safe split heights
  • Render split fragments with clipping and re-drawn borders per page

Click resolution (text-editor.ts):

  • Use labeled break (break findInnerTableY) so nested table Y computation stops at the correct fragment
  • Account for split offset in nested table inner row resolution
  • Clamp target line to visible fragment bounds
  • Return correct block for deeper nesting instead of always blocks[0]
  • Guard against empty tables (rows=0) in click handling

Cursor rendering (peer-cursor.ts):

  • Extract findSplitFragment helper for fragment selection
  • Add straddling match for nested tables crossing split boundaries
  • Clamp cursor Y to fragment top when straddling produces negative offset

Selection highlighting (selection.ts, table-renderer.ts):

  • Recurse into nested tables during background pass so backgrounds don't cover selection highlights
  • Support split row fragments in cell-range rects for both top-level and nested tables

Other (text-editor.ts):

  • Cmd+A delete replaces everything with empty paragraph (not empty table shell)
  • Line-by-line arrow navigation in table cells

Test plan

  • Open a document with tall table rows that span page boundaries
  • Verify rows split across pages without blank space or clipping
  • Click on cells in split row fragments — cursor appears on correct page
  • Click on nested table cells within split rows — cursor appears at clicked position
  • Select cells in nested tables on both pages — selection highlight visible on both
  • Arrow Up/Down across split row boundaries moves cursor correctly
  • Cmd+A then Delete produces empty paragraph, not empty table

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Tables can now split across page boundaries, allowing tall rows to span multiple pages instead of forcing oversized pages.
    • Enhanced cursor navigation and text selection for split table rows across page fragments.
  • Tests

    • Added comprehensive test coverage for table row splitting during pagination.

hackerwins and others added 23 commits April 19, 2026 20:42
Cursor navigation in shared documents crashes with "Block not found"
when arrow keys reference blocks deleted by remote collaborators, or
when stale blockParentMap data points to removed table rows.

Three fixes:
- getBlockType() uses findBlock (non-throwing) with paragraph fallback
  to prevent toolbar render crashes
- handleKeyDown guards against stale cursor blockId, resetting to the
  first block when the referenced block no longer exists
- handleArrow wrapped in try-catch to recover from stale blockParentMap
  data by invalidating layout for the next keypress
- moveVertical cross-page jump now handles the case where the adjacent
  page only contains a tall table block, entering the table's last/first
  cell instead of returning the same position

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Describes the approach for splitting table rows at page boundaries
so that tall rows (with nested tables, images, long text) render
continuously instead of leaving blank space.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add cross-reference to the new row-splitting spec and summarize
the key changes to the pagination algorithm. Also register the
new design doc in the README index.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
9-task plan covering pagination, rendering, coordinate mapping,
navigation, nested table recursion, and rowSpan handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Extend the PageLine interface with optional metadata for table row
splitting across page boundaries. No behaviour change yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When a table row is split across pages, each fragment now gets its own
TableRenderRange entry instead of being deduped. The rendering pipeline
clips cell content to the fragment's visible area and re-draws borders
at fragment boundaries so each page shows a clean rectangular table
section.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The strict intersection approach failed for multi-column rows where
columns have different line heights (no common breakpoints exist).
The new findRowSplitHeight takes a target available height and returns
the maximum safe split point by clamping to each cell's largest
breakpoint within the available space, then taking the minimum.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The existing border rendering pass already draws cell borders within
the clip region. The extra fragment border code drew redundant vertical
lines that appeared as visual artifacts between columns.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The forward sweep in computeTableRangeForPageLine was including split
fragment rows in the non-split render pass, causing the same row to
render twice — once without split clipping and once with it. Stop the
sweep before split fragments so each gets its own isolated render pass.

Also remove debug logging from table-renderer.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When a cell's content fits entirely within the available height, it
imposes no constraint on the split point. Only cells with content
exceeding the available height limit the split height. This prevents
short header cells from forcing tiny 26px fragments.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When a cell contains a nested table, findRowSplitHeight now walks
the nested table's rows to find finer-grained split points instead
of treating the entire nested table as an atomic unit.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
collectTableCellImageRects was not accounting for rowSplitOffset,
causing image selection handles to appear offset from the actual
image in split row continuations. Subtract splitOffset from cellY
and skip images outside the visible fragment range.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
findRowSplitHeight was manually summing line heights starting from
DEFAULT_CELL_PADDING, which could diverge from the layout engine's
actual line.y values (which include block margins and spacing). Use
the pre-computed line.y + line.height from LayoutTableCell.lines for
accurate breakpoint positions.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
resolveOffsetInCellAtXY cached row→page info via Map.set, which
overwrote with the last fragment when a row was split across pages.
Clicks on the first fragment used the second fragment's page Y,
causing the resolved line to be off by one.

Now picks the fragment whose visible range contains the click Y
and subtracts the splitOffset from the absolute line Y calculation.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
resolveLineAbsoluteY returned the first PageLine match for a split
row, using the wrong page's Y offset. Now picks the fragment whose
visible range contains the line's row-relative Y position, and
subtracts splitOffset for correct absolute coordinates.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
resolvePositionPixel used the first PageLine match for a split row,
returning coordinates from the wrong page. Now picks the fragment
whose visible range contains the cursor line's Y position and
subtracts splitOffset. Applies to both top-level and nested table
cell paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Arrow Up/Down in table cells previously jumped between blocks,
skipping visual lines within wrapped text. Add moveCellLine() that
finds the current visual line in the cell's layout and moves to the
adjacent line. Falls back to block/cell navigation when at the
first/last line.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The up path used getParentTableBlock() to gate the layoutTable
lookup, which always returned truthy for cell-internal blocks,
causing tl to be undefined and skipping moveCellLine entirely.
Use the same direct layoutTable lookup as the down path.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
resolveTableCellClick used pl.line.height (full row height) for
hit testing, causing the first fragment to claim the entire row's
click area. Split fragments now use pl.rowSplitHeight for their
visible height so clicks land on the correct fragment.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Split table rows had multiple interrelated bugs that prevented
clicking, cursor placement, and cell selection from working
correctly when rows span page boundaries.

Click resolution:
- Fix labeled break in innerTableAbsY loop so the first matching
  fragment is used instead of being overwritten by later fragments
- Account for split offset in nested table inner row resolution
- Clamp target line to visible fragment when click falls in empty
  space below the last visible line
- Return the correct block for deeper-nesting fallthrough instead
  of always returning blocks[0]
- Guard against empty tables (rows=0) in resolveTableCellClick
- Use rowSplitHeight in paginatedPixelToPosition so small split
  fragments don't claim the space of subsequent lines

Cursor rendering (peer-cursor.ts):
- Extract findSplitFragment helper to deduplicate fragment
  selection logic between nested and non-nested paths
- Add straddling match for nested tables crossing split boundaries
- Clamp cursor Y to fragment top when straddling produces a
  negative offset

Selection highlighting:
- Render nested table backgrounds in the first pass
  (renderTableBackgrounds) so they don't cover selection highlights
- Support split row fragments in cell-range selection rects for
  both top-level and nested tables
- Use labeled break in baseY computation for nested tables

Other fixes:
- Replace all content with empty paragraph on Cmd+A delete instead
  of leaving an empty table shell

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@hackerwins has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 40 minutes and 4 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 40 minutes and 4 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2b7fe3a-71a0-440f-b037-5634eaeec2db

📥 Commits

Reviewing files that changed from the base of the PR and between c19022f and 98ceebb.

📒 Files selected for processing (6)
  • docs/design/docs/docs-pagination.md
  • packages/docs/src/view/doc-canvas.ts
  • packages/docs/src/view/pagination.ts
  • packages/docs/src/view/peer-cursor.ts
  • packages/docs/src/view/table-layout.ts
  • packages/docs/src/view/text-editor.ts
📝 Walkthrough

Walkthrough

This PR implements table row splitting across page boundaries. It extends the pagination data model with rowSplitOffset and rowSplitHeight fields to support fragmenting tall table rows, introduces a utility to compute safe split points aligned to cell content boundaries, and updates the rendering and interaction pipelines to clip canvas drawing per fragment and handle cursor/selection navigation across split fragments.

Changes

Cohort / File(s) Summary
Design Documentation
docs/design/README.md, docs/design/docs/docs-pagination.md, docs/design/docs/docs-table-row-splitting.md, docs/tasks/active/20260419-table-row-splitting-todo.md
Added comprehensive design documentation for table row splitting feature, including specification of pagination behavior, rendering changes, interaction updates, and implementation plan spanning data model, layout, pagination, rendering, and editor code.
Pagination Core
packages/docs/src/view/pagination.ts
Extended PageLine with optional rowSplitOffset and rowSplitHeight fields. Reworked table row pagination to emit multiple fragments per row when splitting, updated findPageForPosition to prefer last fragment for table blocks, and refined paginatedPixelToPosition hit-testing to use visible row height.
Layout Utilities
packages/docs/src/view/table-layout.ts
Added exported findRowSplitHeight utility to compute maximum safe pagination height for a table row by examining cell content boundaries and recursively evaluating nested table row boundaries.
Rendering Pipeline
packages/docs/src/view/doc-canvas.ts, packages/docs/src/view/table-renderer.ts
Extended table rendering functions to accept and apply rowSplitOffset and rowSplitHeight metadata for clipped canvas rendering. Updated deduplication logic in doc-canvas to process split fragments independently. Added split-aware clipping, border re-rendering per fragment, and nested-table recursion in table-renderer.
User Interaction
packages/docs/src/view/text-editor.ts, packages/docs/src/view/peer-cursor.ts, packages/docs/src/view/selection.ts
Updated text editor, cursor, and selection code to account for rowSplitOffset/rowSplitHeight in hit-testing, coordinate mapping, and navigation. Added split-fragment-aware cursor positioning, line-movement helpers within cells, and per-fragment selection rect emission for split rows.
Supporting Code
packages/docs/src/view/image-selection-overlay.ts
Extended image-rect collection to pass and apply split-fragment metadata when processing table cells, skipping image rects outside visible fragment bounds.
Test Coverage
packages/docs/test/view/table-row-split.test.ts
Added Vitest test suite exercising findRowSplitHeight control flow and pagination of tall table rows across pages with split-fragment validation.

Sequence Diagram(s)

sequenceDiagram
    participant Paginator as Paginator<br/>(pagination.ts)
    participant LayoutUtil as Layout Util<br/>(table-layout.ts)
    participant Renderer as Renderer<br/>(doc-canvas.ts)
    participant Canvas as Canvas<br/>(table-renderer.ts)
    participant Editor as Editor/Cursor<br/>(text-editor.ts)

    Paginator->>LayoutUtil: findRowSplitHeight(row, availableHeight)
    LayoutUtil-->>Paginator: safe split height or 0
    
    alt Row fits on page
        Paginator->>Paginator: emit single PageLine
    else Row spans pages
        Paginator->>Paginator: compute fragment 1 (rowSplitOffset=0)
        Paginator->>Paginator: emit PageLine with rowSplitHeight
        Paginator->>Paginator: compute fragment 2 (rowSplitOffset>0)
        Paginator->>Paginator: emit PageLine with rowSplitHeight
    end
    
    Renderer->>Canvas: renderTableBackgrounds(rowSplitOffset, rowSplitHeight)
    Canvas->>Canvas: ctx.clip() vertical slice
    Canvas-->>Renderer: fragment background drawn
    
    Renderer->>Canvas: renderTableContent(rowSplitOffset, rowSplitHeight)
    Canvas->>Canvas: ctx.save() + ctx.translate(-rowSplitOffset)
    Canvas->>Canvas: render content + borders
    Canvas->>Canvas: ctx.restore()
    Canvas-->>Renderer: fragment content drawn
    
    Editor->>Editor: click at position (x, y)
    Editor->>Editor: find PageLine fragment where<br/>y ∈ [splitOffset, splitOffset + splitHeight)
    Editor->>Editor: adjust coordinates by -splitOffset
    Editor-->>Editor: cell position resolved
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 When table rows grew far too tall,
A clever rabbit heard the call:
"Just split them up across the pages!"
And earned the dev team's hearty praises—
Now rows can dance from page to page! 📖✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Fix table row splitting: click, cursor, selection, and rendering' directly and specifically summarizes the main changes: implementing table row splitting and fixing related issues with click resolution, cursor placement, selection rendering, and table rendering.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/docs-arrow-navigation-crash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Resolve conflicts in peer-cursor.ts and selection.ts: keep
split-row-aware fragment selection (findSplitFragment helper)
over the non-split findPageLine approach from main.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@github-actions

github-actions Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 116.4s

Lane Status Duration
sheets:build ✅ pass 12.9s
docs:build ✅ pass 7.9s
verify:fast ✅ pass 57.9s
frontend:build ✅ pass 16.0s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.7s
cli:build ✅ pass 1.8s
verify:entropy ✅ pass 15.0s

Verification: verify:integration

Result: ✅ PASS

@codecov

codecov Bot commented Apr 20, 2026

Copy link
Copy Markdown

@hackerwins

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (5)
packages/docs/src/view/peer-cursor.ts (1)

161-165: ⚠️ Potential issue | 🟠 Major

Make nested cursor Y relative to the outer row before fragment lookup.

yOffsetInTable includes the top-level table’s rowYOffsets[rowIndex], but findSplitFragment() compares against row-local rowSplitOffset/rowSplitHeight. Nested cursors in any outer row after row 0 can resolve to the wrong fragment and render too low.

🐛 Proposed fix
         const outerRowIndex = nestingPath[0].rowIndex;
-        const cursorInRow = yOffsetInTable + targetLayout.runLineY;
+        const outerRowTop = lb.layoutTable.rowYOffsets[outerRowIndex] ?? 0;
+        const cursorInRow = yOffsetInTable + targetLayout.runLineY - outerRowTop;
               }
               if (outerCellData.blocks[bIdx]?.id === nextTableId) {
-                lineEndInRow = yOffsetInTable + outerCell.lines[li].height;
+                const outerPadding = outerCellData.style?.padding ?? 4;
+                const outerLineTop =
+                  (lb.layoutTable.rowYOffsets[outerSeg.rowIndex] ?? 0) +
+                  outerPadding +
+                  outerCell.lines[li].y;
+                lineEndInRow = outerLineTop + outerCell.lines[li].height - outerRowTop;
                 break;
               }

Also applies to: 270-306

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/peer-cursor.ts` around lines 161 - 165, yOffsetInTable
includes the outer table’s tl.rowYOffsets[rowIndex], causing nested cursors to
use absolute table Y when calling findSplitFragment(); compute a row-local Y
before fragment lookup by subtracting tl.rowYOffsets[rowIndex] (e.g. let
yOffsetInRow = yOffsetInTable - tl.rowYOffsets[rowIndex]) and pass that
row-local value to findSplitFragment()/any comparisons against
rowSplitOffset/rowSplitHeight; apply the same fix in the other nested-cursor
fragment-lookup block around the 270-306 region where the same pattern occurs.
packages/docs/src/view/image-selection-overlay.ts (1)

359-365: ⚠️ Potential issue | 🟠 Major

Filter nested-table image rects by the active split fragment.

Direct image runs are skipped when outside [rowSplitOffset, rowSplitOffset + rowSplitHeight), but nested tables are always traversed. In a split outer row, this emits every nested image for every fragment and can overwrite the image’s map entry with coordinates from the wrong page fragment.

Thread the visible fragment window into collectNestedTableImageRects() or skip recursion when the nested-table line is outside the fragment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/image-selection-overlay.ts` around lines 359 - 365,
The nested-table traversal ignores the active split fragment, causing nested
image rects from other fragments to be emitted; update the call site in the
block that checks line.nestedTable and innerBlock to pass the current visible
fragment window (rowSplitOffset and rowSplitHeight or equivalent bounds) into
collectNestedTableImageRects, and modify collectNestedTableImageRects to accept
these bounds and filter emitted rects (or skip recursion) when the nested table
line falls outside [rowSplitOffset, rowSplitOffset + rowSplitHeight). Use the
symbols collectNestedTableImageRects, line.nestedTable, innerBlock.tableData,
rowSplitOffset and rowSplitHeight to locate and update both the caller and the
callee so nested images are only collected for the active fragment.
packages/docs/src/view/text-editor.ts (1)

2661-2692: ⚠️ Potential issue | 🟠 Major

Handle full-document deletion before the same-block branch.

For a document containing only a table block, startBlockIdx === endBlockIdx, so Cmd+A Delete takes the same-block deleteText(..., 0) path and leaves the table shell. Move the full-selection replacement check above the same-block branch.

Suggested restructuring
     const { start, end } = normalized;
     const startBlockIdx = this.doc.getBlockIndex(start.blockId);
     const endBlockIdx = this.doc.getBlockIndex(end.blockId);
+    const blocks = this.doc.document.blocks;
+    const lastBlock = this.doc.getBlock(end.blockId);
+    const isFullSelection = startBlockIdx === 0
+      && endBlockIdx === blocks.length - 1
+      && start.offset === 0
+      && end.offset === getBlockTextLength(lastBlock);
+
+    if (isFullSelection) {
+      const emptyBlock = createBlock();
+      this.doc.insertBlockAt(0, emptyBlock);
+      const remaining = this.doc.document.blocks.length;
+      for (let i = remaining - 1; i > 0; i--) {
+        this.doc.deleteBlockByIndex(i);
+      }
+      this.cursor.moveTo({ blockId: emptyBlock.id, offset: 0 });
+      this.selection.setRange(null);
+      this.requestRender();
+      return true;
+    }

     if (startBlockIdx === endBlockIdx) {
       // Same block — mark dirty for incremental layout
       this.doc.deleteText(start, end.offset - start.offset);
       this.markDirty(start.blockId);
     } else {
       // Multi-block structural change — force full layout recompute
       this.invalidateLayout();
-
-      // Check if entire document is selected (all blocks from first to last,
-      // at offset 0 and end). Replace with a single empty paragraph.
-      const blocks = this.doc.document.blocks;
       const firstBlock = this.doc.getBlock(start.blockId);
-      const lastBlock = this.doc.getBlock(end.blockId);
-      const isFullSelection = startBlockIdx === 0
-        && endBlockIdx === blocks.length - 1
-        && start.offset === 0
-        && end.offset === getBlockTextLength(lastBlock);
-
-      if (isFullSelection) {
-        // Replace everything with a single empty paragraph
-        const emptyBlock = createBlock();
-        this.doc.insertBlockAt(0, emptyBlock);
-        // Remove all original blocks (now shifted to indices 1..length-1)
-        const remaining = this.doc.document.blocks.length;
-        for (let i = remaining - 1; i > 0; i--) {
-          this.doc.deleteBlockByIndex(i);
-        }
-        this.cursor.moveTo({ blockId: emptyBlock.id, offset: 0 });
-        this.selection.setRange(null);
-        this.requestRender();
-        return true;
-      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/text-editor.ts` around lines 2661 - 2692, The
full-document selection replacement must be detected and handled before the
same-block branch so a single-block document (e.g., a lone table) doesn't take
the deleteText path; move the isFullSelection check (using startBlockIdx,
endBlockIdx, start.offset === 0, end.offset === getBlockTextLength(lastBlock),
and blocks.length) to run before the if (startBlockIdx === endBlockIdx) branch,
and when true perform the same replacement steps (createBlock(),
this.doc.insertBlockAt(0, emptyBlock), remove remaining blocks via
this.doc.deleteBlockByIndex, move cursor via this.cursor.moveTo, clear selection
via this.selection.setRange(null), this.requestRender(), and return true) so
full-document deletion consistently replaces the whole document with a single
empty paragraph regardless of block count.
packages/docs/src/view/table-renderer.ts (1)

516-524: ⚠️ Potential issue | 🟠 Major

Draw borders at the fragment bounds when a row is split.

With split clipping, continuation fragments translate the row content upward, but the border code still draws at the original row top/bottom. Those lines are clipped out for middle/continuation fragments, so the page fragment may not get the promised top/bottom borders.

Suggested adjustment
-      const x = tableX + columnXOffsets[c];
-      const y = tableY + rowYOffsets[visibleStart];
+      const x = tableX + columnXOffsets[c];
+      const y = isSplit && r === pageStart
+        ? tableY + rowYOffsets[pageStart] + rowSplitOffset!
+        : tableY + rowYOffsets[visibleStart];
+      const borderHeight = isSplit && r === pageStart
+        ? rowSplitHeight!
+        : visibleHeight;

       // Use theme-aware default border color so borders adapt to dark mode
       const themeBorder: BorderStyle = { ...DEFAULT_BORDER_STYLE, color: Theme.defaultColor };
       drawBorder(ctx, cell.style?.borderTop ?? themeBorder, x, y, x + cellWidth, y);
-      drawBorder(ctx, cell.style?.borderBottom ?? themeBorder, x, y + visibleHeight, x + cellWidth, y + visibleHeight);
-      drawBorder(ctx, cell.style?.borderLeft ?? themeBorder, x, y, x, y + visibleHeight);
-      drawBorder(ctx, cell.style?.borderRight ?? themeBorder, x + cellWidth, y, x + cellWidth, y + visibleHeight);
+      drawBorder(ctx, cell.style?.borderBottom ?? themeBorder, x, y + borderHeight, x + cellWidth, y + borderHeight);
+      drawBorder(ctx, cell.style?.borderLeft ?? themeBorder, x, y, x, y + borderHeight);
+      drawBorder(ctx, cell.style?.borderRight ?? themeBorder, x + cellWidth, y, x + cellWidth, y + borderHeight);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/table-renderer.ts` around lines 516 - 524, Borders are
being drawn at the original row bounds so when rows are split across fragments
the continuation fragments miss the top/bottom borders; update the border draw
calls to use the fragment-adjusted vertical coordinates instead of the original
row top/bottom. Specifically, compute the fragment-local top (e.g., fragmentTop
= tableY + rowYOffsets[visibleStart] + fragmentTranslationY or use the already
computed y for the fragment) and call drawBorder with that fragmentTop and
fragmentTop + visibleHeight (using symbols tableX, columnXOffsets[c], cellWidth,
visibleStart, visibleHeight, and drawBorder) so borders render at the fragment
bounds for split rows.
packages/docs/src/view/selection.ts (1)

378-386: ⚠️ Potential issue | 🟠 Major

Make nested cell selection use the clicked split fragment too.

The isNested branch still uses findPageLine(...), which returns the first outer-row fragment. For a nested table inside a split outer row, selection rects for continuation-page content can be rendered on the first fragment instead of the fragment containing nestedYOff + runLineY.

Suggested direction
       if (isNested) {
         const outerRow = resolved.outerRowIndex;
-        const found = findPageLine(paginatedLayout, blockIndex, outerRow);
-        if (!found) return undefined;
-        return found.pageY + found.pageLine.y + nestedYOff
-          + tl.rowYOffsets[ownerRow]
-          + (runLineY - tl.rowYOffsets[ownerRow]);
+        const lineInOuterRow = nestedYOff + runLineY;
+        let bestResult: number | undefined;
+        for (const page of paginatedLayout.pages) {
+          const pageY = getPageYOffset(paginatedLayout, page.pageIndex);
+          for (const pl of page.lines) {
+            if (pl.blockIndex !== blockIndex || pl.lineIndex !== outerRow) continue;
+            const splitOffset = pl.rowSplitOffset ?? 0;
+            const visibleHeight = pl.rowSplitHeight ?? pl.line.height;
+            const absY = pageY + pl.y + lineInOuterRow - splitOffset;
+            if (
+              pl.rowSplitHeight === undefined ||
+              (lineInOuterRow >= splitOffset && lineInOuterRow < splitOffset + visibleHeight)
+            ) {
+              return absY;
+            }
+            bestResult = absY;
+          }
+        }
+        return bestResult;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/selection.ts` around lines 378 - 386, The isNested
branch in resolveLineAbsoluteY currently uses findPageLine(blockIndex, outerRow)
which returns the first outer-row fragment and causes continuation-page content
to be placed on the wrong fragment; change the logic in resolveLineAbsoluteY to
compute the target nested Y (const targetNestedY = nestedYOff + runLineY) and
search the paginatedLayout for the outer-row fragment whose pageLine vertical
span contains that target (i.e. fragment.pageLine.y <= targetNestedY <
fragment.pageLine.y + fragment.pageLine.height or equivalent), then use that
fragment's pageY and pageLine.y when computing the absolute Y instead of the
first found fragment; refer to symbols resolveLineAbsoluteY, isNested,
resolved.outerRowIndex, nestedYOff, runLineY, paginatedLayout, blockIndex and
replace the single findPageLine call with a search that selects the fragment
containing targetNestedY.
🧹 Nitpick comments (1)
packages/docs/test/view/table-row-split.test.ts (1)

40-51: Strengthen this test to catch unsafe multi-cell split heights.

toBeGreaterThanOrEqual(0) passes even when findRowSplitHeight() returns a height that cuts through one cell’s line. Add a mismatched-breakpoint case and assert the result is 0 or a boundary that is safe for every non-merged cell.

🧪 Example test direction
-  it('handles multi-column rows with different line heights', () => {
+  it('does not split multi-column rows at a mid-line height', () => {
     const block = createTableBlock(1, 2);
     const td = block.tableData!;
-    td.rows[0].cells[0].blocks[0].inlines = [{ text: 'A', style: {} }];
-    td.rows[0].cells[1].blocks[0].inlines = [{ text: 'B', style: {} }];
+    td.rows[0].cells[0].blocks = [
+      { id: 'a1', type: 'paragraph', inlines: [{ text: 'A1', style: { fontSize: 16 } }], style: {} },
+      { id: 'a2', type: 'paragraph', inlines: [{ text: 'A2', style: { fontSize: 16 } }], style: {} },
+    ];
+    td.rows[0].cells[1].blocks = [
+      { id: 'b1', type: 'paragraph', inlines: [{ text: 'B1', style: { fontSize: 24 } }], style: {} },
+      { id: 'b2', type: 'paragraph', inlines: [{ text: 'B2', style: { fontSize: 24 } }], style: {} },
+    ];
     const ctx = stubCtx();
     const layout = computeTableLayout(td, 'tbl', ctx, 200);
-    const splitH = findRowSplitHeight(layout, 0, 100);
+    const splitH = findRowSplitHeight(layout, 0, 60);
 
-    // Should return a safe height (min of per-cell breakpoints)
-    expect(splitH).toBeGreaterThanOrEqual(0);
+    expect(splitH).toBe(0);
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/test/view/table-row-split.test.ts` around lines 40 - 51, The
test currently only asserts splitH >= 0 which won't catch unsafe splits through
a cell's line; update the test that uses createTableBlock, computeTableLayout
and findRowSplitHeight to include a mismatched-breakpoint scenario where two
side-by-side cells have different line heights (e.g., one cell with single short
inline and the other with a taller inline) using stubCtx so that their per-cell
breakpoints differ, then call findRowSplitHeight for a split that would cut one
cell’s line and assert the returned split height is 0 (or the safe boundary that
avoids cutting any non-merged cell) to ensure the function rejects unsafe
multi-cell splits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/docs/docs-pagination.md`:
- Around line 423-424: Update the sentence about merged-cell support to reflect
the actual shipped behavior: clarify that full rowSpan/colSpan support is not
implemented yet (mark as "limited support" or "future work") because the
split-height helper does not account for row-spanning owners from earlier rows
and the task plan still treats rowSpan splitting as separate work; reference the
split-height helper and the task plan behavior so readers know the limitation
and that proper multi-row span handling is pending.

In `@docs/tasks/active/20260419-table-row-splitting-todo.md`:
- Around line 63-66: The fenced command blocks containing git commands (e.g.,
the snippet starting with "git add packages/docs/src/view/pagination.ts" and
"git commit -m ...", and the other similar git command blocks) are missing
language tags; update each triple-backtick fence that surrounds git command
snippets to use the shell language tag (```shell) so markdownlint recognizes
them—search for fences that contain "git add", "git commit", and other git
commands and replace the opening ``` with ```shell for each occurrence.
- Line 1: Create the paired lessons file named
docs/tasks/active/20260419-table-row-splitting-lessons.md next to the existing
todo file (docs/tasks/active/20260419-table-row-splitting-todo.md) and populate
it with a concise summary of what was implemented, the key design decisions
(including reasons and trade-offs), notable pitfalls and how they were resolved,
testing/validation steps taken, and any follow-up actions or open questions so
future readers can understand rationale and lessons learned from the
table-row-splitting implementation.

In `@packages/docs/src/view/doc-canvas.ts`:
- Around line 56-61: computeTableRangeForPageLine() currently extends
endRowIndex across later rows even when the starting page line (pl) is a split
fragment; modify the inner loop that scans following lines (where it checks
nextPl.blockIndex === pl.blockIndex) to also break if the current pl is a split
fragment (pl.rowSplitOffset !== undefined) but the candidate nextPl is not a
split fragment (nextPl.rowSplitOffset === undefined), preventing normal rows
from being swept into the split-fragment render pass; apply the same guard to
the identical loops referenced (the other occurrences around lines 100-110 and
477-480) so collectTableRenderRanges()/computeTableRangeForPageLine() stop
before non-split rows when starting from a split fragment.

In `@packages/docs/src/view/table-layout.ts`:
- Around line 629-682: The code currently uses a global DEFAULT_CELL_PADDING and
can produce split heights that slice mid-line; change it to compute each cell's
safe breakpoint using that cell's actual padding (e.g., const cellPadding =
cell.padding ?? DEFAULT_CELL_PADDING) and ensure per-cell bestBp is always
chosen at line or nested-row boundaries (only assign bestBp when a full line end
or a nested safe innerSplit boundary is found, never a mid-line value), then
treat cells whose content entirely fits as non-constraining and compute the
returned minSafe as the minimum across only those per-cell safe breakpoints;
update references in this block (layout.cells[rowIndex], cell.merged,
cell.lines, line.y, line.height, line.nestedTable, nt.rowHeights,
nt.rowYOffsets, findRowSplitHeight) accordingly.

In `@packages/docs/src/view/text-editor.ts`:
- Around line 2463-2475: The current loop determining currentLine ignores the
caret's lineAffinity and treats an offset equal to the end of a visual run as
belonging to the previous wrapped line; update the logic in this block (the loop
using remaining, pos.offset, currentLine and layoutCell.lines[li].runs) to
consider pos.lineAffinity (or accept a lineAffinity parameter) so that when
remaining === lineChars you choose the next visual line for Forward affinity and
the current/previous line for Backward affinity; also ensure the same
lineAffinity value is threaded into any higher-level selection/text-editor
helpers that call this routine so wrap-boundary offsets consistently resolve to
the intended visual line.

---

Outside diff comments:
In `@packages/docs/src/view/image-selection-overlay.ts`:
- Around line 359-365: The nested-table traversal ignores the active split
fragment, causing nested image rects from other fragments to be emitted; update
the call site in the block that checks line.nestedTable and innerBlock to pass
the current visible fragment window (rowSplitOffset and rowSplitHeight or
equivalent bounds) into collectNestedTableImageRects, and modify
collectNestedTableImageRects to accept these bounds and filter emitted rects (or
skip recursion) when the nested table line falls outside [rowSplitOffset,
rowSplitOffset + rowSplitHeight). Use the symbols collectNestedTableImageRects,
line.nestedTable, innerBlock.tableData, rowSplitOffset and rowSplitHeight to
locate and update both the caller and the callee so nested images are only
collected for the active fragment.

In `@packages/docs/src/view/peer-cursor.ts`:
- Around line 161-165: yOffsetInTable includes the outer table’s
tl.rowYOffsets[rowIndex], causing nested cursors to use absolute table Y when
calling findSplitFragment(); compute a row-local Y before fragment lookup by
subtracting tl.rowYOffsets[rowIndex] (e.g. let yOffsetInRow = yOffsetInTable -
tl.rowYOffsets[rowIndex]) and pass that row-local value to
findSplitFragment()/any comparisons against rowSplitOffset/rowSplitHeight; apply
the same fix in the other nested-cursor fragment-lookup block around the 270-306
region where the same pattern occurs.

In `@packages/docs/src/view/selection.ts`:
- Around line 378-386: The isNested branch in resolveLineAbsoluteY currently
uses findPageLine(blockIndex, outerRow) which returns the first outer-row
fragment and causes continuation-page content to be placed on the wrong
fragment; change the logic in resolveLineAbsoluteY to compute the target nested
Y (const targetNestedY = nestedYOff + runLineY) and search the paginatedLayout
for the outer-row fragment whose pageLine vertical span contains that target
(i.e. fragment.pageLine.y <= targetNestedY < fragment.pageLine.y +
fragment.pageLine.height or equivalent), then use that fragment's pageY and
pageLine.y when computing the absolute Y instead of the first found fragment;
refer to symbols resolveLineAbsoluteY, isNested, resolved.outerRowIndex,
nestedYOff, runLineY, paginatedLayout, blockIndex and replace the single
findPageLine call with a search that selects the fragment containing
targetNestedY.

In `@packages/docs/src/view/table-renderer.ts`:
- Around line 516-524: Borders are being drawn at the original row bounds so
when rows are split across fragments the continuation fragments miss the
top/bottom borders; update the border draw calls to use the fragment-adjusted
vertical coordinates instead of the original row top/bottom. Specifically,
compute the fragment-local top (e.g., fragmentTop = tableY +
rowYOffsets[visibleStart] + fragmentTranslationY or use the already computed y
for the fragment) and call drawBorder with that fragmentTop and fragmentTop +
visibleHeight (using symbols tableX, columnXOffsets[c], cellWidth, visibleStart,
visibleHeight, and drawBorder) so borders render at the fragment bounds for
split rows.

In `@packages/docs/src/view/text-editor.ts`:
- Around line 2661-2692: The full-document selection replacement must be
detected and handled before the same-block branch so a single-block document
(e.g., a lone table) doesn't take the deleteText path; move the isFullSelection
check (using startBlockIdx, endBlockIdx, start.offset === 0, end.offset ===
getBlockTextLength(lastBlock), and blocks.length) to run before the if
(startBlockIdx === endBlockIdx) branch, and when true perform the same
replacement steps (createBlock(), this.doc.insertBlockAt(0, emptyBlock), remove
remaining blocks via this.doc.deleteBlockByIndex, move cursor via
this.cursor.moveTo, clear selection via this.selection.setRange(null),
this.requestRender(), and return true) so full-document deletion consistently
replaces the whole document with a single empty paragraph regardless of block
count.

---

Nitpick comments:
In `@packages/docs/test/view/table-row-split.test.ts`:
- Around line 40-51: The test currently only asserts splitH >= 0 which won't
catch unsafe splits through a cell's line; update the test that uses
createTableBlock, computeTableLayout and findRowSplitHeight to include a
mismatched-breakpoint scenario where two side-by-side cells have different line
heights (e.g., one cell with single short inline and the other with a taller
inline) using stubCtx so that their per-cell breakpoints differ, then call
findRowSplitHeight for a split that would cut one cell’s line and assert the
returned split height is 0 (or the safe boundary that avoids cutting any
non-merged cell) to ensure the function rejects unsafe multi-cell splits.
🪄 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: 9e2bb14b-fa88-47f2-b0b8-de99ffc5b8df

📥 Commits

Reviewing files that changed from the base of the PR and between 073d6c4 and c19022f.

📒 Files selected for processing (13)
  • docs/design/README.md
  • docs/design/docs/docs-pagination.md
  • docs/design/docs/docs-table-row-splitting.md
  • docs/tasks/active/20260419-table-row-splitting-todo.md
  • packages/docs/src/view/doc-canvas.ts
  • packages/docs/src/view/image-selection-overlay.ts
  • packages/docs/src/view/pagination.ts
  • packages/docs/src/view/peer-cursor.ts
  • packages/docs/src/view/selection.ts
  • packages/docs/src/view/table-layout.ts
  • packages/docs/src/view/table-renderer.ts
  • packages/docs/src/view/text-editor.ts
  • packages/docs/test/view/table-row-split.test.ts

Comment thread docs/design/docs/docs-pagination.md Outdated
@@ -0,0 +1,940 @@
# Table Row Splitting Implementation Plan

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that both active task files exist.
fd -a '20260419-table-row-splitting-(todo|lessons)\.md$' docs/tasks/active

Repository: wafflebase/wafflebase

Length of output: 138


Create the paired 20260419-table-row-splitting-lessons.md file.

This task requires a paired lessons file per the coding guidelines for non-trivial tasks. Currently only the todo file exists; add docs/tasks/active/20260419-table-row-splitting-lessons.md to document the key learnings and decisions from this implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tasks/active/20260419-table-row-splitting-todo.md` at line 1, Create the
paired lessons file named
docs/tasks/active/20260419-table-row-splitting-lessons.md next to the existing
todo file (docs/tasks/active/20260419-table-row-splitting-todo.md) and populate
it with a concise summary of what was implemented, the key design decisions
(including reasons and trade-offs), notable pitfalls and how they were resolved,
testing/validation steps taken, and any follow-up actions or open questions so
future readers can understand rationale and lessons learned from the
table-row-splitting implementation.

Comment on lines +63 to +66
```
git add packages/docs/src/view/pagination.ts
git commit -m "Add rowSplitOffset/rowSplitHeight fields to PageLine"
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add shell language tags to command fences.

markdownlint flags these fenced command blocks as missing a language. Use ```shell for the git command snippets.

Also applies to: 185-188, 387-390, 548-550, 616-618, 658-660, 810-812, 902-904, 937-939

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

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

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tasks/active/20260419-table-row-splitting-todo.md` around lines 63 - 66,
The fenced command blocks containing git commands (e.g., the snippet starting
with "git add packages/docs/src/view/pagination.ts" and "git commit -m ...", and
the other similar git command blocks) are missing language tags; update each
triple-backtick fence that surrounds git command snippets to use the shell
language tag (```shell) so markdownlint recognizes them—search for fences that
contain "git add", "git commit", and other git commands and replace the opening
``` with ```shell for each occurrence.

Comment thread packages/docs/src/view/doc-canvas.ts Outdated
Comment thread packages/docs/src/view/table-layout.ts Outdated
Comment on lines +629 to +682
const padding = DEFAULT_CELL_PADDING;
const cells = layout.cells[rowIndex];
if (!cells || cells.length === 0) return 0;

let minSafe = availableHeight;
let hasCells = false;

for (let c = 0; c < cells.length; c++) {
const cell = cells[c];
if (cell.merged) continue;
hasCells = true;

// Use the layout engine's actual line.y values (which include any
// block margins and spacing) instead of re-summing heights manually.
// Breakpoints are at padding + line.y + line.height for each line.
let bestBp = 0;
let allFit = true;
for (const line of cell.lines) {
const lineEnd = padding + line.y + line.height;
if (line.nestedTable) {
// Recurse into nested table: each row boundary is a breakpoint
const nt = line.nestedTable;
const ntBase = padding + line.y; // Y of nested table top within row
for (let nr = 0; nr < nt.rowHeights.length; nr++) {
const rowEnd = ntBase + nt.rowYOffsets[nr] + nt.rowHeights[nr];
if (rowEnd <= availableHeight) {
bestBp = rowEnd;
} else {
const nestedAvail = availableHeight - ntBase - nt.rowYOffsets[nr];
if (nestedAvail > 0) {
const innerSplit = findRowSplitHeight(nt, nr, nestedAvail);
if (innerSplit > 0) {
bestBp = ntBase + nt.rowYOffsets[nr] + innerSplit;
}
}
allFit = false;
break;
}
}
} else {
if (lineEnd <= availableHeight) {
bestBp = lineEnd;
} else {
allFit = false;
}
}
if (!allFit) break;
}
if (!allFit) {
minSafe = Math.min(minSafe, bestBp);
}
// When allFit, this cell's content ends before availableHeight,
// so splitting at any height >= bestBp is safe — no constraint.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Return only split heights that are safe for every cell.

Math.min() across per-cell breakpoints can return a height that is mid-line for another cell. This also hardcodes DEFAULT_CELL_PADDING, so custom cell padding shifts the real safe boundaries. Both cases can split through visible content.

🐛 Directional fix
 export interface LayoutTableCell {
   lines: LayoutLine[];
   blockBoundaries: number[];
   width: number;
   height: number;
+  padding: number;
   merged: boolean;
 }
-      cellRow.push({ lines, blockBoundaries, width: cellWidth, height: cellHeight, merged: false });
+      cellRow.push({ lines, blockBoundaries, width: cellWidth, height: cellHeight, padding, merged: false });
-  const padding = DEFAULT_CELL_PADDING;
   const cells = layout.cells[rowIndex];
   if (!cells || cells.length === 0) return 0;
 
-  let minSafe = availableHeight;
+  const candidates = new Set<number>();
+  const perCellBreaks: number[][] = [];
   let hasCells = false;
 
   for (let c = 0; c < cells.length; c++) {
     const cell = cells[c];
     if (cell.merged) continue;
     hasCells = true;
+    const padding = cell.padding;
+    const breaks: number[] = [];
 
-    let bestBp = 0;
-    let allFit = true;
     for (const line of cell.lines) {
       const lineEnd = padding + line.y + line.height;
+      if (lineEnd <= availableHeight) candidates.add(lineEnd);
+      breaks.push(lineEnd);
+      // Preserve the nested-table breakpoint handling here, pushing each
+      // nested safe boundary into both `breaks` and `candidates`.
+    }
+    perCellBreaks.push(breaks);
+  }
+
+  if (!hasCells) return 0;
+
+  const safe = [...candidates]
+    .filter((h) => h <= availableHeight)
+    .filter((h) => perCellBreaks.every((breaks) => breaks.includes(h) || h >= (breaks.at(-1) ?? 0)))
+    .sort((a, b) => a - b);
+
+  return safe.at(-1) ?? 0;
-      ...
-    }
-  }
-
-  return hasCells ? minSafe : 0;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/table-layout.ts` around lines 629 - 682, The code
currently uses a global DEFAULT_CELL_PADDING and can produce split heights that
slice mid-line; change it to compute each cell's safe breakpoint using that
cell's actual padding (e.g., const cellPadding = cell.padding ??
DEFAULT_CELL_PADDING) and ensure per-cell bestBp is always chosen at line or
nested-row boundaries (only assign bestBp when a full line end or a nested safe
innerSplit boundary is found, never a mid-line value), then treat cells whose
content entirely fits as non-constraining and compute the returned minSafe as
the minimum across only those per-cell safe breakpoints; update references in
this block (layout.cells[rowIndex], cell.merged, cell.lines, line.y,
line.height, line.nestedTable, nt.rowHeights, nt.rowYOffsets,
findRowSplitHeight) accordingly.

Comment thread packages/docs/src/view/text-editor.ts Outdated
- Respect lineAffinity in moveCellLine so ArrowUp/Down at wrap
  boundaries picks the correct visual line
- Subtract outerRowTop from cursorInRow in nested table cursor
  rendering so rows after row 0 resolve to the correct fragment
- Prevent split-fragment render pass from sweeping subsequent
  non-split rows into its clipped draw
- Use per-cell padding in findRowSplitHeight instead of hardcoded
  DEFAULT_CELL_PADDING
- Update docs-pagination.md to reflect that merged-cell splitting
  is not yet implemented

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@hackerwins
hackerwins merged commit d91c60b into main Apr 20, 2026
4 checks passed
@hackerwins
hackerwins deleted the fix/docs-arrow-navigation-crash branch April 20, 2026 23:37
hackerwins added a commit that referenced this pull request Apr 22, 2026
chart-pivot-range-shift (#147), table-row-splitting (#145),
and table-copy-paste (#140) are all merged.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@coderabbitai coderabbitai Bot mentioned this pull request May 1, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant