Skip to content

Add block type extensions for docs word processor (Phase 1)#83

Merged
hackerwins merged 26 commits into
mainfrom
feat/docs-block-types
Mar 25, 2026
Merged

Add block type extensions for docs word processor (Phase 1)#83
hackerwins merged 26 commits into
mainfrom
feat/docs-block-types

Conversation

@hackerwins

@hackerwins hackerwins commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Block types: Add heading (H1-H6), list-item (ordered/unordered), horizontal rule, title, and subtitle block types to the docs data model
  • Rich rendering: Canvas rendering for headings with size defaults, list markers (bullets ●○■, numbered 1./a./i.), horizontal rules, title/subtitle styles, and text highlight (background color)
  • Toolbar UX: Google Docs-style heading dropdown, text/highlight color pickers, indent/outdent buttons, list toggle buttons, grouped layout (Undo/Redo | Styles | Font Styles | Block Styles)
  • Alignment: Left/center/right/justify with keyboard shortcuts and dropdown hints
  • Keyboard shortcuts: Full Google Docs-compatible shortcuts — Cmd+Alt+0-6 (headings), Cmd+Shift+7/8 (lists), Cmd+Shift+L/E/R/J (alignment), Cmd+]/[ (indent/outdent)
  • Input handling: Markdown auto-conversion (#/##/- /1. /---), Tab/Shift+Tab for list indent, Enter to continue/exit lists
  • Collaboration: Yorkie Tree serialization for all new block attributes, cursor positioning fix for empty list items
  • Safety: HR merge guards, inline normalization on block type change, list level cap at 8

Test plan

  • Unit tests for block type model (types.test.ts)
  • Unit tests for document operations incl. HR edge cases (document.test.ts)
  • Unit tests for layout engine list counters (layout.test.ts)
  • pnpm verify:fast passes (lint + all unit tests)
  • Manual: verify heading/list/HR rendering in browser
  • Manual: verify highlight color applies behind text
  • Manual: verify indent/outdent moves bullet markers for list items
  • Manual: verify justify alignment distributes words evenly

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for multiple block types: headings (H1–H6), titles, subtitles, and horizontal rules.
    • Introduced ordered and unordered lists with adjustable indentation levels.
    • Added text highlight (background color) support.
    • Enabled text justify alignment option.
    • Expanded toolbar with style selection, highlight controls, and list indentation buttons.
    • Added keyboard shortcuts for block formatting and Markdown-style auto-conversion.
  • Documentation

    • Updated product roadmap with Phase 1 completion status and detailed feature specifications.

hackerwins and others added 21 commits March 26, 2026 00:02
When createBlock('heading') is called without opts, headingLevel now
defaults to 1 instead of being left undefined. Added tests for
list-item and heading defaults without opts, plus parameterized
coverage for all six heading levels via getHeadingDefaults.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Heading Enter creates a paragraph. List Enter inherits list type.
Empty list Enter exits the list. HR Enter creates a paragraph after.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Heading blocks now render with their default font size and weight.
The resolveBlockInlines() helper merges heading defaults as a base
layer before measurement, so explicit inline styles still override.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Apply list-level-based left margin indent in computeLayout() for
list-item blocks. Render bullet/number markers in DocCanvas on the
first line of each list-item block, using UNORDERED_MARKERS for
bullets and placeholder numbering for ordered lists.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
HR blocks get a fixed 20px height with no text runs in layout, and
render as a thin horizontal line spanning the content area.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Enter on heading creates paragraph. Tab/Shift+Tab changes list level.
Enter on empty list exits list. HR blocks prevent text input.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add three new methods to the EditorAPI interface and implementation
so the toolbar and external code can query and mutate block types.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add block-type controls to the docs formatting toolbar:
- Heading dropdown with Normal text and Heading 1-6 options
- Bulleted list toggle button
- Numbered list toggle button

Co-Authored-By: Claude Opus 4.6 <[email protected]>
All five Phase 1 items done: heading (H1-H6), lists (ordered/unordered),
horizontal rule, layout engine branching, and Yorkie serialization.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
When a list-item block has no text, resolvePositionPixel() fell back to
pageX + margins.left, ignoring the block's marginLeft (which includes
the list indent). The cursor appeared before the bullet marker instead
of after it. Add blockMarginLeft to the empty-line fallback.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
When a list-item block has no text, resolvePositionPixel() fell back to
pageX + margins.left, ignoring the list indent. The LayoutBlock stores
the original block (not the effectiveBlock with added marginLeft), so
we must compute the list indent directly using LIST_INDENT_PX.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add 'title' and 'subtitle' to BlockType union
- Title defaults: 26pt, no bold. Subtitle defaults: 15pt, gray (#666666)
- resolveBlockInlines() applies title/subtitle defaults like headings
- Toolbar dropdown redesigned: shows current style name, styled previews
  for Normal text, Title, Subtitle, Heading 1-3

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Call editor.focus() after style dropdown, list toggle, alignment,
and text color changes so the user can keep typing without clicking
back into the canvas.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add indent() and outdent() to EditorAPI (36px step, matching list indent)
- Add decrease/increase indent buttons to toolbar with focus return

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Toolbar now groups as: Undo/Redo | Styles | Font Styles | Block Styles,
matching Google Docs toolbar organization for better discoverability.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add backgroundColor to InlineStyle for text highlighting
- Render highlight as filled rect behind text in doc-canvas
- Replace strikethrough toolbar button with highlight color picker
- Fix indent/outdent for list items to change listLevel instead of
  marginLeft, so the bullet marker moves with the text

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

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9517c91d-071e-4c89-9036-72e6a5389b03

📥 Commits

Reviewing files that changed from the base of the PR and between e9ec76a and 6dd45e2.

📒 Files selected for processing (10)
  • docs/design/docs-wordprocessor-roadmap.md
  • docs/tasks/active/20260325-docs-wordprocessor-todo.md
  • packages/docs/src/model/document.ts
  • packages/docs/src/model/types.ts
  • packages/docs/src/view/editor.ts
  • packages/docs/src/view/layout.ts
  • packages/docs/src/view/text-editor.ts
  • packages/docs/test/model/document.test.ts
  • packages/frontend/src/app/docs/docs-formatting-toolbar.tsx
  • packages/frontend/src/app/docs/yorkie-doc-store.ts

📝 Walkthrough

Walkthrough

Adds multi-block types (title, subtitle, heading, list-item, horizontal-rule), inline highlight/backgroundColor, list indentation and counters, type-aware split/merge and setBlockType APIs, layout/rendering updates (justify, HR, list markers), editor keyboard/toolbar controls, Yorkie serialization for new fields, and matching tests and docs updates.

Changes

Cohort / File(s) Summary
Task Tracking
docs/tasks/active/20260325-docs-wordprocessor-todo.md
Phase 1 checklist items updated to mark headings, lists, HR, title/subtitle, toolbar grouping, highlight color, shortcuts, and hints as completed (docs-only checklist changes).
Types & Exports
packages/docs/src/model/types.ts, packages/docs/src/index.ts
Introduce BlockType/HeadingLevel; extend Block with headingLevel/listKind/listLevel; add backgroundColor to InlineStyle; export createBlock, heading defaults, list constants, and re-export computeListCounters.
Document Model
packages/docs/src/model/document.ts, packages/docs/test/model/document.test.ts
Add Doc.setBlockType(...); make splitBlock/deleteBackward type-aware (empty list → paragraph, HR split inserts paragraph, HR backspace deletes HR); clear stale type-specific fields; tests added/updated for these behaviors.
Layout & Measurement
packages/docs/src/view/layout.ts, packages/docs/test/view/layout.test.ts
Add resolveBlockInlines, support heading/title/subtitle default inlines, list indentation via LIST_INDENT_PX, HR placeholder lines (HR_HEIGHT), justify alignment, and computeListCounters with ordered-format logic; tests added.
Rendering / Canvas
packages/docs/src/view/doc-canvas.ts
DocCanvas.render(...) accepts optional layout; render HR as horizontal stroke; draw list markers (bullets/numbers) using list counters and listLevel; support run backgroundColor rendering; new helper for list markers.
Editor & Text Input
packages/docs/src/view/editor.ts, packages/docs/src/view/text-editor.ts
Extend EditorAPI with getBlockType, setBlockType, toggleList, indent, outdent; add keyboard handlers (Tab, Mod+Alt/Shift combos, alignment including justify, heading shortcuts, indent/outdent), Markdown auto-conversion (e.g., --- → HR), block-aware enter/tab behavior; editor now passes computed layout into render.
Cursor & Positioning
packages/docs/src/view/peer-cursor.ts
resolvePositionPixel accounts for list indentation when resolving X for empty lines (adds effective marginLeft + LIST_INDENT_PX * (listLevel+1)).
Persistence (Yorkie)
packages/frontend/src/app/docs/yorkie-doc-store.ts
Serialize/parse backgroundColor; emit optional block attrs (headingLevel, listKind, listLevel) only when present; preserve type and handle HR in tree-to-Block conversion.
Frontend Toolbar UI
packages/frontend/src/app/docs/docs-formatting-toolbar.tsx
Add Styles dropdown (paragraph/title/subtitle/headings), highlight color swatches/reset, list toggle and indent controls; remove strikethrough button; ensure editor focus after style actions.
Tests & Coverage
packages/docs/test/*
Add/extend tests: createBlock, getHeadingDefaults, layout tests (HR, list indentation, counters), and model tests for setBlockType and type-aware splitBlock.

Sequence Diagram

sequenceDiagram
    participant User
    participant TextEditor
    participant EditorAPI
    participant Doc
    participant YorkieStore
    participant Layout
    participant DocCanvas

    User->>TextEditor: keyboard / toolbar input
    TextEditor->>EditorAPI: request (setBlockType / toggleList / indent / outdent / getBlockType)
    EditorAPI->>Doc: apply mutation (setBlockType / split / indent logic)
    Doc->>YorkieStore: snapshot & persist changes
    YorkieStore-->>Doc: persist confirmed
    EditorAPI->>Layout: computeLayout(blocks)
    Layout->>Layout: computeListCounters(...)
    EditorAPI->>DocCanvas: render(pagination, layout)
    DocCanvas->>DocCanvas: resolveBlockInlines, render runs, markers, HRs
    DocCanvas->>User: updated canvas
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through types and lines of text,
I nested lists and numbered next,
Rules sweep wide and headings rise,
Highlights gleam and bullets prize,
A joyful nibble — small commit, big flex! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
Title check ✅ Passed The PR title directly reflects the main objective: introducing new block type extensions (heading, list-item, horizontal-rule, title, subtitle) for the docs word processor as Phase 1 work.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 feat/docs-block-types

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.

@github-actions

github-actions Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 105.0s

Lane Status Duration
sheets:build ✅ pass 13.0s
docs:build ✅ pass 5.7s
verify:fast ✅ pass 49.3s
frontend:build ✅ pass 15.3s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.4s
cli:build ✅ pass 1.8s
verify:entropy ✅ pass 15.0s

Verification: verify:integration

Result: ✅ PASS

- Cmd+Shift+7: toggle numbered list
- Cmd+Shift+8: toggle bulleted list
- Cmd+]: increase indent
- Cmd+[: decrease indent
- Show shortcut hints in toolbar tooltips for all new buttons

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

@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/frontend/src/app/docs/docs-formatting-toolbar.tsx (1)

165-170: Avoid calling getBlockType() twice.

editor.getBlockType() is invoked twice in adjacent lines. Store the result in a variable to avoid redundant computation.

♻️ Proposed fix
                 <span className="truncate">
-                  {editor ? getBlockLabel(
-                    editor.getBlockType().type,
-                    editor.getBlockType().headingLevel,
-                  ) : "Normal text"}
+                  {editor ? (() => {
+                    const blockType = editor.getBlockType();
+                    return getBlockLabel(blockType.type, blockType.headingLevel);
+                  })() : "Normal text"}
                 </span>

Alternatively, compute this outside the JSX for better readability:

const currentBlockType = editor?.getBlockType();
const blockLabel = currentBlockType 
  ? getBlockLabel(currentBlockType.type, currentBlockType.headingLevel)
  : "Normal text";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/frontend/src/app/docs/docs-formatting-toolbar.tsx` around lines 165
- 170, The JSX calls editor.getBlockType() twice; cache its result to avoid
redundant calls by creating a local variable (e.g., currentBlockType) before the
return and use that when computing the label via
getBlockLabel(currentBlockType.type, currentBlockType.headingLevel) or falling
back to "Normal text"; update the span to use the cached
blockLabel/currentBlockType instead of calling editor.getBlockType() again so
only one getBlockType() invocation occurs.
packages/docs/src/view/text-editor.ts (1)

724-739: Extract max list level to a named constant.

The maximum list level of 8 appears as a magic number. Consider defining it as a constant (possibly in model/types.ts alongside LIST_INDENT_PX) for consistency with editor.ts which also needs this bound.

♻️ Proposed improvement
+const MAX_LIST_LEVEL = 8;
+
 private handleTab(shift: boolean): void {
   const block = this.doc.getBlock(this.cursor.position.blockId);
   if (block.type !== 'list-item') return;

   this.saveSnapshot();
   const currentLevel = block.listLevel ?? 0;
-  const newLevel = shift ? Math.max(0, currentLevel - 1) : Math.min(8, currentLevel + 1);
+  const newLevel = shift ? Math.max(0, currentLevel - 1) : Math.min(MAX_LIST_LEVEL, currentLevel + 1);
   if (newLevel === currentLevel) return;
🤖 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 724 - 739, Replace the
magic number 8 in handleTab with a named constant: define e.g. LIST_MAX_LEVEL
(or MAX_LIST_LEVEL) next to LIST_INDENT_PX in model/types.ts, export it, and
import it into packages/docs/src/view/text-editor.ts; then use LIST_MAX_LEVEL in
the Math.min call when computing newLevel and update any other places (like
editor.ts) that currently use 8 to reference the same exported constant so the
bound is consistent across the codebase.
packages/docs/src/view/doc-canvas.ts (3)

272-286: Remove unused lineX parameter.

The lineX parameter is passed to renderListMarker but never used in the function body. Either remove it or document why it's reserved for future use.

♻️ Proposed fix
   private renderListMarker(
     block: Block,
-    lineX: number,
     lineY: number,
     lineHeight: number,
     markerX: number,
     markerText: string,
   ): void {

And update the call site at line 164:

-            this.renderListMarker(block, pageX + pl.x, pageY + pl.y, pl.line.height, markerX, marker);
+            this.renderListMarker(block, pageY + pl.y, pl.line.height, markerX, marker);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/doc-canvas.ts` around lines 272 - 286, The
renderListMarker function signature includes an unused parameter lineX; remove
lineX from the parameter list in renderListMarker and update every call site
that passes lineX to instead pass only the remaining parameters (ensure calls to
renderListMarker now match the new signature: (block, lineY, lineHeight,
markerX, markerText)); after changes, run the TypeScript build to catch any
missed call sites and adjust any affected variable names or argument ordering
accordingly.

160-160: Document the magic number in marker positioning.

The -4 offset in the marker X calculation lacks explanation. Consider extracting it to a named constant or adding a comment explaining its purpose (e.g., visual centering adjustment).

📝 Suggested improvement
+            const MARKER_OFFSET_ADJUSTMENT = 4; // Fine-tune horizontal centering of marker glyph
-            const markerX = pageX + margins.left + LIST_INDENT_PX * level + LIST_INDENT_PX / 2 - 4;
+            const markerX = pageX + margins.left + LIST_INDENT_PX * level + LIST_INDENT_PX / 2 - MARKER_OFFSET_ADJUSTMENT;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/doc-canvas.ts` at line 160, The marker X position uses
a unexplained magic offset (-4) in the calculation for markerX; replace this
literal with a clearly named constant (e.g., MARKER_CENTER_OFFSET_PX or
LIST_MARKER_VISUAL_ADJUST_PX) or add an inline comment explaining it is a visual
centering adjustment, and update the expression where markerX is computed so the
constant name (or comment) documents purpose; ensure the new constant is
colocated near related layout constants (like LIST_INDENT_PX) for
discoverability.

68-68: Consider caching list counters during layout computation.

computeListCounters is called on every render frame. For documents with many list items, this may introduce unnecessary overhead. Since list counters only change when document structure changes, consider computing them once during computeLayout and including them in DocumentLayout.

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

In `@packages/docs/src/view/doc-canvas.ts` at line 68, computeListCounters is
being recomputed on every render (call at doc-canvas: listCounters =
computeListCounters(layout.blocks.map(b => b.block))), causing unnecessary work;
move its computation into computeLayout and store the result on the returned
DocumentLayout so renders reuse it. Update computeLayout (and the DocumentLayout
type) to produce a listCounters Map<string,string> alongside existing layout
data, remove the computeListCounters call from render (doc-canvas) and read
layout.listCounters instead, and ensure any code paths that mutate document
structure trigger a recompute via computeLayout so the cached counters stay
correct.
packages/docs/src/view/layout.ts (1)

161-163: Consider extracting HR_HEIGHT as a module-level constant.

The horizontal rule height is defined inline. If this value is needed elsewhere (e.g., for cursor positioning or rendering), extracting it would improve maintainability.

♻️ Proposed improvement
+/** Fixed height for horizontal-rule blocks in pixels. */
+const HR_HEIGHT = 20;
+
 // In computeLayout:
     if (block.type === 'horizontal-rule') {
-      const HR_HEIGHT = 20;
       lines = [{ runs: [], y: 0, height: HR_HEIGHT, width: availableWidth }];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/layout.ts` around lines 161 - 163, Extract the inline
HR_HEIGHT constant used in the horizontal-rule branch into a module-level (and
exported if used elsewhere) constant like HORIZONTAL_RULE_HEIGHT so other code
(cursor positioning or render logic) can reuse it; update the branch where
block.type === 'horizontal-rule' to reference the new module-level constant
(instead of the local HR_HEIGHT) and adjust any other places that need the same
value to import/use that constant to keep a single source of truth.
packages/docs/src/view/editor.ts (1)

576-580: Consider sharing INDENT_STEP constant with LIST_INDENT_PX.

INDENT_STEP = 36 is defined locally and matches LIST_INDENT_PX from model/types.ts. Using the shared constant would ensure they stay synchronized.

♻️ Proposed fix

Add to imports:

-import type { InlineStyle, BlockStyle, BlockType, HeadingLevel } from '../model/types.js';
+import type { InlineStyle, BlockStyle, BlockType, HeadingLevel } from '../model/types.js';
+import { LIST_INDENT_PX } from '../model/types.js';

Then use it:

       } else {
-        const INDENT_STEP = 36;
         doc.applyBlockStyle(block.id, {
-          marginLeft: (block.style.marginLeft ?? 0) + INDENT_STEP,
+          marginLeft: (block.style.marginLeft ?? 0) + LIST_INDENT_PX,
         });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs/src/view/editor.ts` around lines 576 - 580, Replace the local
INDENT_STEP = 36 with the shared constant LIST_INDENT_PX from the model types so
the indent step stays synchronized; import LIST_INDENT_PX and use it in the
doc.applyBlockStyle call (referencing block.id and block.style.marginLeft)
instead of defining INDENT_STEP locally.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/docs/src/model/document.ts`:
- Around line 158-169: The Backspace merge path allows merging a text paragraph
into a non-text block (e.g., 'horizontal-rule'), causing the paragraph text to
be lost; modify deleteBackward() and/or mergeBlocks() to guard against merging
into non-text blocks by checking the target block's type (e.g., block.type ===
'horizontal-rule') and either (a) refuse the merge and return early, or (b)
convert the non-text block into a text-capable block (create a paragraph block
with inlines) before merging; update the logic in mergeBlocks() and the caller
deleteBackward() to detect non-text block types and handle them explicitly so
text never gets merged into a non-text block.
- Around line 272-296: When changing a block's type in setBlockType, also
normalize block.inlines to maintain the invariant: if the new type is
'horizontal-rule' set block.inlines = [] (dropping any hidden text), and if
switching from 'horizontal-rule' to an editable type ensure block.inlines =
[{type: 'text', text: ''}] (a single empty inline) so methods like insertText()
and resolveOffset() can operate safely; update block.inlines before calling
this.store.updateBlock(blockId, block) and this.refresh().

In `@packages/docs/src/view/editor.ts`:
- Around line 567-584: The indent() method can increase list nesting without an
upper bound; update indent() to cap the computed newLevel to the same maximum
used by handleTab() (e.g., 8) before calling doc.setBlockType for list-item
blocks; locate the indent() function and the doc.setBlockType call and clamp
newLevel = Math.min(newLevel, MAX_LIST_LEVEL) (or equivalent) so toolbar
indentation and text-editor.handleTab() use the same limit; no changes needed
for INDENT_STEP branch.

In `@packages/docs/test/model/document.test.ts`:
- Around line 282-328: Add two regression tests inside the existing "splitBlock
— type-aware" describe: one that creates a horizontal-rule then a paragraph (use
Doc.create(), setBlockType(...,'horizontal-rule'), splitBlock(...,0) or insert a
following paragraph) and then simulates backspace at the start of that paragraph
(call the model's deleteBackward / backspace API on the new paragraph at offset
0) asserting the rule is preserved/merged correctly and no invalid state occurs;
and a second test that exercises paragraph ↔ horizontal-rule transitions
(setBlockType to 'horizontal-rule' then convert back to 'paragraph' and
vice‑versa, using splitBlock, setBlockType and insertText) and asserts there is
no leftover hidden text or an empty-inline invalid state (inspect
block.inlineChildren or block.content and block.type invariants). Reference
Doc.create, setBlockType, splitBlock, insertText and the model's
deleteBackward/backspace method to locate where to add the checks.

In `@packages/frontend/src/app/docs/yorkie-doc-store.ts`:
- Around line 149-164: The deserializer always synthesizes a single empty inline
into block.inlines, which turns non-text blocks like
createBlock('horizontal-rule') (which should have inlines: []) back into
text-bearing blocks; update the block construction so the inlines fallback is
only applied for text-bearing block types (e.g.,
paragraph/heading/code/blockquote or whatever set your app treats as text
blocks). Concretely, change the inlines initialization in the block object (the
local variable block in yorkie-doc-store.ts) to test attrs.type (or the computed
block.type) and only set inlines: [{ text: '', style: {} }] when the type is a
text block; leave inlines as [] for non-text types (so
createBlock('horizontal-rule') remains line-less). Ensure subsequent
headingLevel/listKind/listLevel handling is unchanged.

---

Nitpick comments:
In `@packages/docs/src/view/doc-canvas.ts`:
- Around line 272-286: The renderListMarker function signature includes an
unused parameter lineX; remove lineX from the parameter list in renderListMarker
and update every call site that passes lineX to instead pass only the remaining
parameters (ensure calls to renderListMarker now match the new signature:
(block, lineY, lineHeight, markerX, markerText)); after changes, run the
TypeScript build to catch any missed call sites and adjust any affected variable
names or argument ordering accordingly.
- Line 160: The marker X position uses a unexplained magic offset (-4) in the
calculation for markerX; replace this literal with a clearly named constant
(e.g., MARKER_CENTER_OFFSET_PX or LIST_MARKER_VISUAL_ADJUST_PX) or add an inline
comment explaining it is a visual centering adjustment, and update the
expression where markerX is computed so the constant name (or comment) documents
purpose; ensure the new constant is colocated near related layout constants
(like LIST_INDENT_PX) for discoverability.
- Line 68: computeListCounters is being recomputed on every render (call at
doc-canvas: listCounters = computeListCounters(layout.blocks.map(b =>
b.block))), causing unnecessary work; move its computation into computeLayout
and store the result on the returned DocumentLayout so renders reuse it. Update
computeLayout (and the DocumentLayout type) to produce a listCounters
Map<string,string> alongside existing layout data, remove the
computeListCounters call from render (doc-canvas) and read layout.listCounters
instead, and ensure any code paths that mutate document structure trigger a
recompute via computeLayout so the cached counters stay correct.

In `@packages/docs/src/view/editor.ts`:
- Around line 576-580: Replace the local INDENT_STEP = 36 with the shared
constant LIST_INDENT_PX from the model types so the indent step stays
synchronized; import LIST_INDENT_PX and use it in the doc.applyBlockStyle call
(referencing block.id and block.style.marginLeft) instead of defining
INDENT_STEP locally.

In `@packages/docs/src/view/layout.ts`:
- Around line 161-163: Extract the inline HR_HEIGHT constant used in the
horizontal-rule branch into a module-level (and exported if used elsewhere)
constant like HORIZONTAL_RULE_HEIGHT so other code (cursor positioning or render
logic) can reuse it; update the branch where block.type === 'horizontal-rule' to
reference the new module-level constant (instead of the local HR_HEIGHT) and
adjust any other places that need the same value to import/use that constant to
keep a single source of truth.

In `@packages/docs/src/view/text-editor.ts`:
- Around line 724-739: Replace the magic number 8 in handleTab with a named
constant: define e.g. LIST_MAX_LEVEL (or MAX_LIST_LEVEL) next to LIST_INDENT_PX
in model/types.ts, export it, and import it into
packages/docs/src/view/text-editor.ts; then use LIST_MAX_LEVEL in the Math.min
call when computing newLevel and update any other places (like editor.ts) that
currently use 8 to reference the same exported constant so the bound is
consistent across the codebase.

In `@packages/frontend/src/app/docs/docs-formatting-toolbar.tsx`:
- Around line 165-170: The JSX calls editor.getBlockType() twice; cache its
result to avoid redundant calls by creating a local variable (e.g.,
currentBlockType) before the return and use that when computing the label via
getBlockLabel(currentBlockType.type, currentBlockType.headingLevel) or falling
back to "Normal text"; update the span to use the cached
blockLabel/currentBlockType instead of calling editor.getBlockType() again so
only one getBlockType() invocation occurs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e091d336-faa1-45c4-b6de-3ad344212e22

📥 Commits

Reviewing files that changed from the base of the PR and between ed79a4d and 126b283.

📒 Files selected for processing (14)
  • docs/tasks/active/20260325-docs-wordprocessor-todo.md
  • packages/docs/src/index.ts
  • packages/docs/src/model/document.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/peer-cursor.ts
  • packages/docs/src/view/text-editor.ts
  • packages/docs/test/model/document.test.ts
  • packages/docs/test/model/types.test.ts
  • packages/docs/test/view/layout.test.ts
  • packages/frontend/src/app/docs/docs-formatting-toolbar.tsx
  • packages/frontend/src/app/docs/yorkie-doc-store.ts

Comment thread packages/docs/src/model/document.ts
Comment thread packages/docs/src/model/document.ts
Comment thread packages/docs/src/view/editor.ts
Comment thread packages/docs/test/model/document.test.ts
Comment thread packages/frontend/src/app/docs/yorkie-doc-store.ts
- Cmd+Shift+L/E/R/J: left/center/right/justify alignment
- Add justify alignment to BlockStyle and layout engine
- Show shortcut hints in style and alignment dropdown items
- Cmd+Shift+7/8: numbered/bulleted list (from prior commit)

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

@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.

🧹 Nitpick comments (2)
packages/frontend/src/app/docs/docs-formatting-toolbar.tsx (1)

165-170: Minor optimization: avoid duplicate getBlockType() call.

editor.getBlockType() is called twice. Consider destructuring once for slightly cleaner code:

♻️ Proposed refactor
-                  {editor ? getBlockLabel(
-                    editor.getBlockType().type,
-                    editor.getBlockType().headingLevel,
-                  ) : "Normal text"}
+                  {editor
+                    ? (() => {
+                        const { type, headingLevel } = editor.getBlockType();
+                        return getBlockLabel(type, headingLevel);
+                      })()
+                    : "Normal text"}

Or extract to a local variable before the JSX.

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

In `@packages/frontend/src/app/docs/docs-formatting-toolbar.tsx` around lines 165
- 170, The JSX calls editor.getBlockType() twice; to avoid the duplicate call,
read and destructure the block type once into a local variable (e.g., const
blockType = editor.getBlockType()) before the return/JSX and then pass
blockType.type and blockType.headingLevel into getBlockLabel; update the span to
use that local variable and keep the fallback "Normal text" when editor is
falsy.
packages/docs/src/view/text-editor.ts (1)

780-796: Inconsistent max indent level between Tab and Cmd+].

handleTab caps the list level at 8 (line 754: Math.min(8, currentLevel + 1)), but handleIndent has no upper bound (line 787: (block.listLevel ?? 0) + 1). This allows unbounded nesting via Cmd+] while Tab is constrained.

Consider applying the same cap for consistency:

♻️ Proposed fix to add consistent max level
   private handleIndent(): void {
     const INDENT_STEP = 36;
+    const MAX_LIST_LEVEL = 8;
     const block = this.doc.getBlock(this.cursor.position.blockId);
     this.saveSnapshot();
     if (block.type === 'list-item') {
+      const newLevel = Math.min(MAX_LIST_LEVEL, (block.listLevel ?? 0) + 1);
       this.doc.setBlockType(block.id, 'list-item', {
         listKind: block.listKind,
-        listLevel: (block.listLevel ?? 0) + 1,
+        listLevel: newLevel,
       });
     } else {
🤖 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 780 - 796, handleIndent
currently increases list nesting without an upper bound causing inconsistency
with handleTab; update handleIndent to cap the list level the same way (use
Math.min(8, (block.listLevel ?? 0) + 1)) when calling this.doc.setBlockType for
list-item blocks, and ensure non-list indentation remains unchanged (INDENT_STEP
logic for applyBlockStyle stays as-is); locate handleIndent, INDENT_STEP,
getBlock, saveSnapshot, setBlockType, and applyBlockStyle to make the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/docs/src/view/text-editor.ts`:
- Around line 780-796: handleIndent currently increases list nesting without an
upper bound causing inconsistency with handleTab; update handleIndent to cap the
list level the same way (use Math.min(8, (block.listLevel ?? 0) + 1)) when
calling this.doc.setBlockType for list-item blocks, and ensure non-list
indentation remains unchanged (INDENT_STEP logic for applyBlockStyle stays
as-is); locate handleIndent, INDENT_STEP, getBlock, saveSnapshot, setBlockType,
and applyBlockStyle to make the change.

In `@packages/frontend/src/app/docs/docs-formatting-toolbar.tsx`:
- Around line 165-170: The JSX calls editor.getBlockType() twice; to avoid the
duplicate call, read and destructure the block type once into a local variable
(e.g., const blockType = editor.getBlockType()) before the return/JSX and then
pass blockType.type and blockType.headingLevel into getBlockLabel; update the
span to use that local variable and keep the fallback "Normal text" when editor
is falsy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3126b73-6beb-4df6-ab57-60730e242110

📥 Commits

Reviewing files that changed from the base of the PR and between 126b283 and e9ec76a.

📒 Files selected for processing (2)
  • packages/docs/src/view/text-editor.ts
  • packages/frontend/src/app/docs/docs-formatting-toolbar.tsx

hackerwins and others added 3 commits March 26, 2026 05:14
Phase 1 fully complete with additional items: Title/Subtitle,
justify alignment, highlight color, keyboard shortcuts with
toolbar hints. Mark Phase 2.2 (highlight) as done early.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Address CodeRabbit review feedback:
- Guard deleteBackward from merging text into horizontal-rule blocks
- Normalize inlines in setBlockType (HR gets [], text types get [empty])
- Cap list indent level at 8 in both editor.ts and text-editor.ts
- Fix Yorkie deserialization to keep HR blocks inline-free
- Add regression tests for HR edge cases

Co-Authored-By: Claude Opus 4.6 <[email protected]>
toggleStyle was always applying { bold: true } etc. instead of
checking the current style and flipping it. Now reads the inline
style at cursor position and inverts boolean properties.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@hackerwins
hackerwins merged commit 53816af into main Mar 25, 2026
1 check passed
@hackerwins
hackerwins deleted the feat/docs-block-types branch March 25, 2026 20:24
hackerwins added a commit that referenced this pull request Mar 27, 2026
Remove unused lineX parameter from renderListMarker, remove unused
imports, and add missing textIndent/marginLeft fields to BlockStyle
test fixtures.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
hackerwins pushed a commit that referenced this pull request Mar 27, 2026
Replace pattern-tiling with Google Sheets-style OLS (Ordinary Least
Squares) regression for numeric cell autofill. When 2+ numeric cells
exist along the fill axis, compute y = mx + b and extrapolate the
trend. Mixed content (formulas, text, empty) falls back to existing
tiling behavior.

Use single-fraction computation with toPrecision(15) to minimize
IEEE 754 floating-point drift, matching Excel/Google Sheets precision.

Also fix docs package build and frontend test failures introduced by
PR #83/#84: refactor TypeScript parameter properties and const enum
unsupported by Node's --experimental-strip-types, remove unused
parameters/imports, and add missing BlockStyle fields in test fixtures.

Co-authored-by: Claude Opus 4.6 <[email protected]>
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