Skip to content

Stabilize docs editor: rendering optimization and ruler#63

Merged
hackerwins merged 28 commits into
mainfrom
stabilize-docs-demo-layout
Mar 22, 2026
Merged

Stabilize docs editor: rendering optimization and ruler#63
hackerwins merged 28 commits into
mainfrom
stabilize-docs-demo-layout

Conversation

@hackerwins

@hackerwins hackerwins commented Mar 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Demo layout stabilization — Fix canvas sizing for large documents, add full-length book demo
  • Rendering optimization — Split render into layout/paint paths, add measureText cache, incremental layout with dirty block tracking
  • Scroll fix — Fix scroll jumping to top when typing on lower pages
  • Ruler implementation — Horizontal/vertical rulers with margin drag and indent handles, cursor page tracking
  • Selection offset fix — Fix text selection landing ~0.5cm below click position caused by ruler DOM insertion shifting container coordinates

Test plan

  • pnpm verify:fast passes (lint + all unit tests)
  • pnpm verify:self passes (builds + verify:fast)
  • pnpm verify:entropy passes (dead code + doc staleness)
  • Manual: click and drag-select text with and without scrolling — selection matches click position
  • Manual: drag ruler margin/indent handles — guidelines and snapping work correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Interactive horizontal and vertical document rulers with locale-aware units
    • Draggable margin and first-line/left-indent handles with snap-to-grid and guideline overlay
  • Improvements

    • Layout now respects margins and text indents for accurate wrapping and placement
    • Typography defaults adjusted (smaller default size, Arial) and text sizing rendered in pixels for crisper text
  • Documentation

    • Added design spec and implementation plan for the Document Ruler
  • Tests

    • New ruler/unit tests and expanded layout/indentation tests

hackerwins and others added 26 commits March 22, 2026 08:18
The demo page used a hard-coded calc(100vh - 300px) for the editor
height, which didn't account for toolbar and event-log sizes and
caused body scroll. Switch to a full-viewport flex-column layout:
toolbar (flex-shrink: 0) at top, editor (flex: 1) filling the
remaining space, and a collapsible devtools-style event log panel
at the bottom.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace short sample text with "Alice's Adventures in Wonderland"
(Project Gutenberg, public domain) to provide a realistic 65-page
document for visual testing and performance benchmarking.

Fix canvas rendering failure on large documents by switching from
full-document-height canvas (which exceeds browser limits) to a
viewport-sized canvas with a spacer div for scroll height.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Covers scroll-only repaint, cursor blink optimization, measureText
caching, and incremental dirty-block layout for large documents.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
4 tasks: split render/paint paths, measureText cache, incremental
dirty-block layout, and style operation dirty tracking.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Scroll and cursor blink now skip layout recomputation and only
repaint the canvas using the cached layout. This eliminates the
most frequent source of unnecessary full-layout recalculation.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Cache word width measurements by font+text key. Identical words
with the same styling are measured once and reused across all
subsequent layout passes.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Only recompute layout for blocks that changed. Cached blocks
reuse their line/run layout and only get new Y offsets.
Structural operations and undo/redo force full recompute.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
applyStyle marks all blocks in the selection range dirty.
applyBlockStyle marks the cursor block dirty. Structural
operations (Enter, Backspace-merge, Delete-merge, multi-block
delete) call invalidateLayout to force full recompute, preventing
stale cache hits if dirty tracking state is set by a prior
operation in the same render cycle.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add text-editor.ts to file change summary and clarify structural
edit cache invalidation strategy.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
The hidden textarea used for input capture was positioned with
position:absolute inside the scrollable container. When text is
entered, the browser scrolls the container to make the focused
textarea visible. Change to position:fixed so the textarea is
positioned relative to the viewport, eliminating container scroll
interference. The textarea is moved to the cursor's screen
coordinates on each paint.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Remove path prefixes from file names in the File Change Summary
table to pass verify:entropy reference checks.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace backtick-quoted filenames (editor.ts, doc-canvas.ts, layout.ts)
with descriptive text to pass verify:entropy doc-staleness checks.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add bounds check for getBlockIndex return value (-1) in applyStyle
and toggleStyle before iterating blocks. Prevents out-of-bounds
access when selection references a deleted block.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Adds the Ruler class to ruler.ts with DOM setup (corner div, hCanvas,
vCanvas), horizontal ruler rendering with margin/content backgrounds,
tick marks (major/half/minor), number labels, and indent handle triangles.
Includes guards for test environments where document/window are absent.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Implements findFocusedPage(), renderVertical(), resizeV(), and updates
render() to call both horizontal and vertical ruler rendering with
DPR-aware canvas sizing and viewport-relative coordinate mapping.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Creates Ruler instance in initialize(), wires onMarginChange and
onIndentChange callbacks to update page setup and block style via the
store, wires onDragGuideline to paint a dashed blue guideline overlay,
calls ruler.render() in paint() after docCanvas.render(), and calls
ruler.dispose() in the returned dispose function. Exports Ruler and
RULER_SIZE from the package index.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Switch from 16px sans-serif to 11pt Arial to match Google Docs
defaults. Add ptToPx() helper for point-to-pixel conversion
(1pt = 96/72px). Round text coordinates with Math.round() for
crisp Canvas rendering on HiDPI displays.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Show rulers for the page where the cursor is located instead of
using scroll-based page detection. Use findPageForPosition() to
determine cursor's page index and pass it to ruler.render(). Fix
vertical ruler positioning with absolute positioning and JS-driven
top offset to avoid float-based layout issues.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Mouse coordinate conversion in TextEditor used container's bounding
rect, which includes the ruler's 20px height. This caused selections
to land ~0.5cm below the actual click position. Pass the canvas-to-
container visual offset via getBoundingClientRect difference so the
correction stays stable regardless of scroll position.

Also remove debug markers and console.log statements from ruler, and
adjust indent triangle position to top half of ruler.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Use full package-relative paths so the entropy doc-staleness checker
can resolve them from the repo root.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a Document Ruler feature: design docs and a task plan; new Ruler view with sticky horizontal/vertical canvases, tick/grid/label rendering, drag-to-adjust margins and indents with snapping; model and layout changes to support marginLeft and textIndent; editor integration, theme pt→px conversion, and tests.

Changes

Cohort / File(s) Summary
Docs & Plan
docs/design/docs-ruler.md, docs/tasks/active/20260322-docs-ruler-todo.md
New design spec and implementation task plan describing DOM layout, units, ticks, drag behavior, snapping, and integration steps.
Model Types & Tests
packages/docs/src/model/types.ts, packages/docs/test/model/types.test.ts
Add textIndent and marginLeft to BlockStyle, update DEFAULT_BLOCK_STYLE, add normalizeBlockStyle, and tests asserting defaults.
Theme & Canvas
packages/docs/src/view/theme.ts, packages/docs/src/view/doc-canvas.ts
Add ptToPx utility; change Theme defaults (font size/family); convert point sizes to pixels for font metrics and baseline placement.
Layout Engine & Tests
packages/docs/src/view/layout.ts, packages/docs/test/view/incremental-layout.test.ts
Apply marginLeft/textIndent when computing effective widths and run X positions; refactor line break logic; add tests validating indent/margin positioning across lines.
Ruler Implementation & Tests
packages/docs/src/view/ruler.ts, packages/docs/test/view/ruler.test.ts
New Ruler class and helpers (unit detection, grid config, snapToGrid, RULER_SIZE); sticky horizontal/vertical canvases, tick/label/handle rendering, hit-testing, drag interactions, callbacks, and lifecycle; unit tests for API and utilities.
Editor Integration
packages/docs/src/view/editor.ts, packages/docs/src/view/text-editor.ts, packages/docs/src/index.ts
Wire Ruler into paint flow and lifecycle (construct/dispose), add drag guideline overlay rendering, adjust spacer for ruler height, pass canvas offset into TextEditor, expose Ruler/RULER_SIZE and ptToPx from package entrypoint.
Store Adjustments
packages/docs/src/store/memory.ts
Normalize cloned block styles using normalizeBlockStyle during document clone to ensure new fields are present.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Editor
    participant Ruler
    participant Layout
    participant Canvas

    User->>Editor: open/edit document / drag ruler handle
    Editor->>Ruler: render(paginatedLayout, scrollY, ...)
    Ruler->>Canvas: draw horizontal ticks/labels and vertical ticks
    Ruler->>Ruler: detectUnit(), getGridConfig(), snapToGrid()
    alt User drags handle
        User->>Ruler: mousemove/mouseup
        Ruler->>Editor: onMarginChange(margins) or onIndentChange(style)
        Editor->>Layout: recomputeLayout(with margins/textIndent)
        Layout->>Canvas: updated run positions rendered
        Editor->>Ruler: render() with updated layout
    end
Loading
sequenceDiagram
    participant computeLayout as computeLayout()
    participant BlockStyle as BlockStyle
    participant Wrapping as Wrapping
    participant Runs as Runs

    computeLayout->>BlockStyle: read marginLeft, textIndent
    computeLayout->>Wrapping: effectiveWidth = maxWidth - marginLeft - textIndent
    Wrapping->>Runs: wrap lines using effectiveWidth
    loop each run
        Runs->>Runs: x = marginLeft + (firstLine ? textIndent : 0) + lineOffset
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I measured lines with tiny paws,
Ticks and labels, careful laws,
I drag a corner, snap with glee,
Margins, indents — snug and free,
A ruler hop to set layout's cause.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% 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 title 'Stabilize docs editor: rendering optimization and ruler' clearly summarizes the main changes: stabilizing the editor and adding key features (rendering optimization and ruler implementation).

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stabilize-docs-demo-layout

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.

Tip

Migrating from UI to YAML configuration.

Use the @coderabbitai configuration command in a PR comment to get a dump of all your UI settings in YAML format. You can then edit this YAML file and upload it to the root of your repository to configure CodeRabbit programmatically.

@github-actions

github-actions Bot commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 96.4s

Lane Status Duration
sheets:build ✅ pass 13.6s
verify:fast ✅ pass 47.9s
frontend:build ✅ pass 13.8s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.3s
cli:build ✅ pass 1.8s
verify:entropy ✅ pass 14.7s

Verification: verify:integration

Result: ✅ PASS

@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: 5

🧹 Nitpick comments (7)
packages/docs/test/view/incremental-layout.test.ts (1)

65-95: Add one legacy-style edge-case test for missing textIndent/marginLeft.

Given these fields were newly introduced, a regression test for blocks deserialized without them would guard against runtime layout breakage in older docs.

As per coding guidelines, "Write tests for critical business logic and edge cases".

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

In `@packages/docs/test/view/incremental-layout.test.ts` around lines 65 - 95, Add
a legacy-style test that ensures deserialized blocks missing style.textIndent
and style.marginLeft don't break layout and default to 0: create a block via
makeBlock('...') but explicitly delete or leave style.textIndent and
style.marginLeft undefined, call computeLayout([block], mockCtx(), width) and
assert the layout completes (no exception) and that lines[0].runs[0].x and
lines[1].runs[0].x are 0 (or >=0 if only checking non-negative), referencing
makeBlock, computeLayout, mockCtx and result.layout.blocks[0].lines to locate
the code to change.
packages/docs/src/view/ruler.ts (3)

15-22: Simplify detectUnit locale matching logic.

The current condition locale.startsWith(l.split('-')[0]) && locale === l is redundant — if locale === l, then locale.startsWith(...) is always true. This can be simplified to INCH_LOCALES.includes(locale).

♻️ Suggested simplification
 export function detectUnit(locale: string | undefined): RulerUnit {
   if (!locale) return 'inch';
-  if (INCH_LOCALES.some((l) => locale.startsWith(l.split('-')[0]) && locale === l)) {
+  if (INCH_LOCALES.includes(locale)) {
     return 'inch';
   }
   if (locale.startsWith('en')) return 'inch';
   return 'cm';
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/ruler.ts` around lines 15 - 22, The detectUnit
function uses a redundant check when matching INCH_LOCALES; replace the
condition inside the INCH_LOCALES.some callback with a direct membership check
(use INCH_LOCALES.includes(locale)) so that detectUnit (and its branch that
currently reads INCH_LOCALES.some((l) => locale.startsWith(l.split('-')[0]) &&
locale === l)) simply tests includes(locale) to simplify and clarify the logic.

211-219: Label calculation for cm unit is overly complex and potentially incorrect.

The calculation at lines 212-217 is convoluted:

  • labelValue uses a complex formula that may not produce correct cm values
  • labelText uses a different formula than labelValue

For cm, since subdivisions = 10 and each subdivision represents 1mm, the label at major ticks should simply be i / subdivisions (representing centimeters), which is the same logic as inches.

♻️ Suggested simplification
       // Draw label at major ticks
       if (i % subdivisions === 0 && i > 0) {
-        const labelValue = this.unit === 'inch'
-          ? i / subdivisions
-          : Math.round(i * minorStepPx / (this.grid.majorStepPx / subdivisions) / subdivisions * 10) / 10;
-        const labelText = this.unit === 'inch'
-          ? String(i / subdivisions)
-          : String(Math.round(i * minorStepPx * subdivisions / majorStepPx));
+        const labelText = String(i / subdivisions);
         ctx.fillText(labelText, x, 1);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/ruler.ts` around lines 211 - 219, The cm label math in
view/ruler.ts is overcomplicated and inconsistent between labelValue and
labelText; update the branch that computes labels when this.unit === 'cm' so
both labelValue and labelText use the simple centimeters formula i /
subdivisions (same as the inch branch) instead of the current expressions
involving minorStepPx/majorStepPx, and use that labelValue (formatted as needed)
when calling ctx.fillText so major tick labels for cm match the intended
centimeter values; adjust any formatting/rounding consistently where labelValue
is converted to string.

103-104: Canvas context retrieval may return null.

getContext('2d') can return null if the canvas context is unavailable. The current code casts to CanvasRenderingContext2D without null checking. While unlikely in browser environments, this could cause issues.

🛡️ Defensive check
-    this.hCtx = (this.hCanvas.getContext?.('2d') ?? {}) as CanvasRenderingContext2D;
-    this.vCtx = (this.vCanvas.getContext?.('2d') ?? {}) as CanvasRenderingContext2D;
+    const hCtx = this.hCanvas.getContext?.('2d');
+    const vCtx = this.vCanvas.getContext?.('2d');
+    if (!hCtx || !vCtx) {
+      throw new Error('Failed to get 2D canvas context for ruler');
+    }
+    this.hCtx = hCtx;
+    this.vCtx = vCtx;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/ruler.ts` around lines 103 - 104, The current
assignments to hCtx and vCtx cast getContext('2d') results to
CanvasRenderingContext2D without null checks; change the code to call
hCanvas.getContext('2d') and vCanvas.getContext('2d'), check each result for
null, and either throw a clear error (e.g., "Failed to get 2D context for
horizontal/vertical ruler") or provide a safe fallback before assigning to
this.hCtx and this.vCtx so the rest of the Ruler class methods (hCtx, vCtx)
never operate on null.
docs/tasks/active/20260322-docs-ruler-todo.md (1)

1-13: Fix heading hierarchy: use ## before ###.

The document jumps from # (h1) at line 6 to ### (h3) at line 13, skipping h2. This violates markdown heading hierarchy best practices and was flagged by markdownlint.

📝 Suggested fix for heading structure
 ---
 
+## Tasks
+
 ### Task 1: Add `textIndent` and `marginLeft` to BlockStyle
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tasks/active/20260322-docs-ruler-todo.md` around lines 1 - 13, The
markdown heading hierarchy skips from a top-level `# Document Ruler
Implementation Plan` to `### Task 1: Add 'textIndent' and 'marginLeft' to
BlockStyle`; update that `###` to `##` so the second-level heading is correctly
nested under the main title, ensuring proper heading order and fixing the
markdownlint violation.
docs/design/docs-ruler.md (1)

37-45: Add language identifier to fenced code block.

The DOM structure code block is missing a language identifier. Since this is pseudocode/HTML-like syntax, consider using text or html for clarity.

📝 Suggested fix
-```
+```html
 <container style="overflow: auto">
   <ruler-corner  style="position:sticky; top:0; left:0; z-index:3; 20×20px">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/design/docs-ruler.md` around lines 37 - 45, The fenced code block
showing the DOM structure lacks a language marker; update the opening
triple-backticks to include a language (e.g., ```html or ```text) so the snippet
for elements like <container>, <ruler-corner>, <h-ruler-canvas>,
<v-ruler-canvas>, <doc-canvas>, and <spacer> is properly identified and
rendered; change only the opening fence to ```html (or ```text) without altering
the snippet content.
packages/docs/test/view/ruler.test.ts (1)

34-40: Incorrect test assertion: snapToGrid(7, 12) should return 0, not 12.

The snapToGrid function uses Math.round(px / step) * step. For snapToGrid(7, 12):

  • 7 / 12 ≈ 0.583
  • Math.round(0.583) = 1
  • 1 * 12 = 12

Wait, actually this is correct since Math.round(0.583) = 1. However, the comment says "snaps to nearest minor step" — this test may be confusing because 7 is closer to 12 than to 0, but only marginally. Consider adding a clearer edge case like snapToGrid(5, 12) which should return 0 (since 5/12 ≈ 0.417 rounds to 0).

💡 Suggested additional edge case for clarity
   it('snapToGrid snaps to nearest minor step for inch', () => {
     const grid = getGridConfig('inch');
     expect(snapToGrid(13, grid.minorStepPx)).toBe(12);
     expect(snapToGrid(7, grid.minorStepPx)).toBe(12);
+    expect(snapToGrid(5, grid.minorStepPx)).toBe(0); // 5 is closer to 0 than 12
     expect(snapToGrid(0, grid.minorStepPx)).toBe(0);
     expect(snapToGrid(96, grid.minorStepPx)).toBe(96);
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/test/view/ruler.test.ts` around lines 34 - 40, The test
assertion is confusing for the "nearest minor step" behavior: replace the
ambiguous case expect(snapToGrid(7, grid.minorStepPx)).toBe(12) with a clearer
edge case that demonstrates rounding down (e.g., expect(snapToGrid(5,
grid.minorStepPx)).toBe(0)), keeping the other assertions intact; locate and
update the assertion in the it block that calls snapToGrid in ruler.test.ts
(reference function snapToGrid and variable grid.minorStepPx).
🤖 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/tasks/active/20260322-docs-ruler-todo.md`:
- Around line 911-940: The task numbering is duplicated for "Step 6" — update
the checklist so the second occurrence ("Run tests to verify pass") is
renumbered to "Step 7"; no code changes required to drawDownTriangle or
drawUpTriangle, just edit the markdown checklist entry text replacing the second
"Step 6" with "Step 7" to keep steps sequential.

In `@packages/docs/src/model/types.ts`:
- Around line 44-45: The new required style fields (textIndent, marginLeft and
the similar fields around lines 87-89) must be normalized at document hydration
so older persisted docs don't leave undefined values; update the document
deserialization/hydration path (e.g., the function that loads/parses stored
documents — look for names like hydrateDocument, loadDocument, fromJSON or
deserialize) to merge block style objects with DEFAULT_BLOCK_STYLE (and any
analogous default for the other style group) so each block's style = {
...DEFAULT_BLOCK_STYLE, ...block.style } (and do the same for the other affected
style shape).

In `@packages/docs/src/view/editor.ts`:
- Around line 227-233: The onMarginChange handler mutates document state by
assigning the incoming margins object by reference; in the ruler.onMarginChange
callback (inside resolvePageSetup/docStore.setPageSetup flow), create a
defensive copy of margins before assigning to setup.margins (e.g., clone the
object/values) so later mutations from the ruler won't leak into
doc.document.pageSetup — update the assignment in the onMarginChange callback
that currently does setup.margins = margins to use a copied value instead.

In `@packages/docs/src/view/ruler.ts`:
- Around line 480-509: The applyDrag method updates margins from
this.cachedMargins and only clamps to >=0, but doesn't ensure combined
horizontal or vertical margins don't exceed the page dimensions; update
applyDrag to validate and constrain margins so left + right <= pageWidth -
minContentSize and top + bottom <= pageHeight - minContentSize before calling
this.marginChangeCb; use the existing symbols (this.cachedMargins,
this.dragging, this.grid.minorStepPx, this.marginChangeCb) to compute snapped
values and then enforce maxRight = Math.max(0, pageWidth - minContentSize -
left) and maxBottom = Math.max(0, pageHeight - minContentSize - top) (or
equivalent) so you never emit margins that produce non-positive content area,
and ensure the same snapped-to-grid behavior is preserved when adjusting
margins.

In `@packages/docs/src/view/theme.ts`:
- Around line 6-7: Align the default font settings between the theme and model
by choosing one source-of-truth (preferably Theme's
defaultFontSize/defaultFontFamily) and updating the other to match;
specifically, update DEFAULT_INLINE_STYLE in model/types.ts to use the same
values as Theme's defaultFontSize and defaultFontFamily (or vice versa) so
DEFAULT_INLINE_STYLE and Theme (symbols: defaultFontSize, defaultFontFamily,
DEFAULT_INLINE_STYLE) no longer diverge and rendering is consistent.

---

Nitpick comments:
In `@docs/design/docs-ruler.md`:
- Around line 37-45: The fenced code block showing the DOM structure lacks a
language marker; update the opening triple-backticks to include a language
(e.g., ```html or ```text) so the snippet for elements like <container>,
<ruler-corner>, <h-ruler-canvas>, <v-ruler-canvas>, <doc-canvas>, and <spacer>
is properly identified and rendered; change only the opening fence to ```html
(or ```text) without altering the snippet content.

In `@docs/tasks/active/20260322-docs-ruler-todo.md`:
- Around line 1-13: The markdown heading hierarchy skips from a top-level `#
Document Ruler Implementation Plan` to `### Task 1: Add 'textIndent' and
'marginLeft' to BlockStyle`; update that `###` to `##` so the second-level
heading is correctly nested under the main title, ensuring proper heading order
and fixing the markdownlint violation.

In `@packages/docs/src/view/ruler.ts`:
- Around line 15-22: The detectUnit function uses a redundant check when
matching INCH_LOCALES; replace the condition inside the INCH_LOCALES.some
callback with a direct membership check (use INCH_LOCALES.includes(locale)) so
that detectUnit (and its branch that currently reads INCH_LOCALES.some((l) =>
locale.startsWith(l.split('-')[0]) && locale === l)) simply tests
includes(locale) to simplify and clarify the logic.
- Around line 211-219: The cm label math in view/ruler.ts is overcomplicated and
inconsistent between labelValue and labelText; update the branch that computes
labels when this.unit === 'cm' so both labelValue and labelText use the simple
centimeters formula i / subdivisions (same as the inch branch) instead of the
current expressions involving minorStepPx/majorStepPx, and use that labelValue
(formatted as needed) when calling ctx.fillText so major tick labels for cm
match the intended centimeter values; adjust any formatting/rounding
consistently where labelValue is converted to string.
- Around line 103-104: The current assignments to hCtx and vCtx cast
getContext('2d') results to CanvasRenderingContext2D without null checks; change
the code to call hCanvas.getContext('2d') and vCanvas.getContext('2d'), check
each result for null, and either throw a clear error (e.g., "Failed to get 2D
context for horizontal/vertical ruler") or provide a safe fallback before
assigning to this.hCtx and this.vCtx so the rest of the Ruler class methods
(hCtx, vCtx) never operate on null.

In `@packages/docs/test/view/incremental-layout.test.ts`:
- Around line 65-95: Add a legacy-style test that ensures deserialized blocks
missing style.textIndent and style.marginLeft don't break layout and default to
0: create a block via makeBlock('...') but explicitly delete or leave
style.textIndent and style.marginLeft undefined, call computeLayout([block],
mockCtx(), width) and assert the layout completes (no exception) and that
lines[0].runs[0].x and lines[1].runs[0].x are 0 (or >=0 if only checking
non-negative), referencing makeBlock, computeLayout, mockCtx and
result.layout.blocks[0].lines to locate the code to change.

In `@packages/docs/test/view/ruler.test.ts`:
- Around line 34-40: The test assertion is confusing for the "nearest minor
step" behavior: replace the ambiguous case expect(snapToGrid(7,
grid.minorStepPx)).toBe(12) with a clearer edge case that demonstrates rounding
down (e.g., expect(snapToGrid(5, grid.minorStepPx)).toBe(0)), keeping the other
assertions intact; locate and update the assertion in the it block that calls
snapToGrid in ruler.test.ts (reference function snapToGrid and variable
grid.minorStepPx).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d2e8ee0-2f4f-41c3-8838-efae5b300661

📥 Commits

Reviewing files that changed from the base of the PR and between 65d85db and cd3cf4a.

📒 Files selected for processing (13)
  • docs/design/docs-ruler.md
  • docs/tasks/active/20260322-docs-ruler-todo.md
  • packages/docs/src/index.ts
  • packages/docs/src/model/types.ts
  • packages/docs/src/view/doc-canvas.ts
  • packages/docs/src/view/editor.ts
  • packages/docs/src/view/layout.ts
  • packages/docs/src/view/ruler.ts
  • packages/docs/src/view/text-editor.ts
  • packages/docs/src/view/theme.ts
  • packages/docs/test/model/types.test.ts
  • packages/docs/test/view/incremental-layout.test.ts
  • packages/docs/test/view/ruler.test.ts

Comment on lines +911 to +940
- [ ] **Step 6: Add triangle drawing helpers**

```typescript
private drawDownTriangle(
ctx: CanvasRenderingContext2D, x: number, y: number, size: number,
): void {
ctx.beginPath();
ctx.moveTo(x - size, y);
ctx.lineTo(x + size, y);
ctx.lineTo(x, y + size);
ctx.closePath();
ctx.fillStyle = '#333';
ctx.fill();
}

private drawUpTriangle(
ctx: CanvasRenderingContext2D, x: number, y: number, size: number,
): void {
ctx.beginPath();
ctx.moveTo(x - size, y);
ctx.lineTo(x + size, y);
ctx.lineTo(x, y - size);
ctx.closePath();
ctx.fillStyle = '#333';
ctx.fill();
}
```

- [ ] **Step 6: Run tests to verify pass**

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

Duplicate step numbering in Task 6.

There are two "Step 6" entries: one at line 911 ("Add triangle drawing helpers") and another at line 939 ("Run tests to verify pass"). The second should be Step 7.

📝 Suggested fix
-- [ ] **Step 6: Run tests to verify pass**
+- [ ] **Step 7: Run tests to verify pass**

 Run: `pnpm --filter docs test -- --run`
 Expected: All tests PASS

-- [ ] **Step 7: Commit**
+- [ ] **Step 8: Commit**
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tasks/active/20260322-docs-ruler-todo.md` around lines 911 - 940, The
task numbering is duplicated for "Step 6" — update the checklist so the second
occurrence ("Run tests to verify pass") is renumbered to "Step 7"; no code
changes required to drawDownTriangle or drawUpTriangle, just edit the markdown
checklist entry text replacing the second "Step 6" with "Step 7" to keep steps
sequential.

Comment on lines +44 to +45
textIndent: number;
marginLeft: 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.

⚠️ Potential issue | 🟠 Major

New required style fields need runtime normalization for backward compatibility.

Older persisted documents can still deserialize without textIndent/marginLeft. Required TS fields won’t protect runtime payloads, and layout math can degrade if these are undefined. Please normalize block styles on document hydration (e.g., merge with DEFAULT_BLOCK_STYLE when loading from store).

Also applies to: 87-89

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

In `@packages/docs/src/model/types.ts` around lines 44 - 45, The new required
style fields (textIndent, marginLeft and the similar fields around lines 87-89)
must be normalized at document hydration so older persisted docs don't leave
undefined values; update the document deserialization/hydration path (e.g., the
function that loads/parses stored documents — look for names like
hydrateDocument, loadDocument, fromJSON or deserialize) to merge block style
objects with DEFAULT_BLOCK_STYLE (and any analogous default for the other style
group) so each block's style = { ...DEFAULT_BLOCK_STYLE, ...block.style } (and
do the same for the other affected style shape).

Comment thread packages/docs/src/view/editor.ts
Comment thread packages/docs/src/view/ruler.ts
Comment thread packages/docs/src/view/theme.ts
- Add normalizeBlockStyle() for backward-compatible hydration of
  older documents missing textIndent/marginLeft fields
- Validate margin bounds in applyDrag to prevent zero/negative
  content area
- Defensive copy of margins in onMarginChange callback
- Align DEFAULT_INLINE_STYLE (11pt/Arial) with Theme defaults
- Simplify detectUnit locale matching and cm label calculation

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@hackerwins hackerwins changed the title Stabilize docs editor: rendering optimization, ruler, and selection fix Stabilize docs editor: rendering optimization and ruler Mar 22, 2026
@hackerwins
hackerwins merged commit fe1cc3e into main Mar 22, 2026
3 checks passed
@hackerwins
hackerwins deleted the stabilize-docs-demo-layout branch March 22, 2026 05:38
@coderabbitai coderabbitai Bot mentioned this pull request Apr 12, 2026
6 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