Skip to content

Add slides ruler + presentation-wide draggable guides#285

Merged
hackerwins merged 16 commits into
mainfrom
slides-ruler-extract-core
May 24, 2026
Merged

Add slides ruler + presentation-wide draggable guides#285
hackerwins merged 16 commits into
mainfrom
slides-ruler-extract-core

Conversation

@hackerwins

@hackerwins hackerwins commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add horizontal + vertical rulers to the slides editor, plus presentation-wide alignment guides that can be drag-created from the rulers and snapped to during element drags.
  • Tone-down the chrome: slim 14-px transparent ruler pinned to the canvas-area frame (not the slide), slide vertically + horizontally centered, speaker-notes panel borderless with a Google-Slides-style top-drag resizer.
  • Reuse the docs ruler's tick + unit primitives via shared exports on @wafflebase/docs (slides extracts tickHeights / labelFontSize / labelInset params; docs behaviour preserved bit-for-bit).

Design: docs/design/slides/slides-ruler.md · Plan: docs/tasks/active/20260523-slides-ruler-todo.md

What landed (13 commits across 6 phases + UX polish)

  • P1 — Extract docs ruler tick + unit helpers into a shared module (no docs behaviour change).
  • P2SlidesRuler controller, H/V canvases, 144 dpi, zoom-aware density.
  • P3Guide type + Yorkie schema + Mem/Yorkie store APIs + passive overlay paint.
  • P4 — Ruler drag-out → addGuide; existing guide drag → moveGuide / drag-onto-ruler → removeGuide; hover cursor; right-click menu.
  • P5 — Guides participate in the snap engine with priority slide-center > guide > edge; snapped guide is thickened + deepened.
  • P6 — Read-only mount tests; design doc reconciled with shipped behavior.
  • Polish — Slim transparent ruler; ruler pinned to frame; slide vertically centered; notes borderless + draggable top divider with localStorage persistence.

Test plan

  • Open a slides document — ruler appears at the top + left edges of the canvas area (stays put as the slide drifts inside the frame).
  • Slide is vertically AND horizontally centered in the canvas area on tall viewports.
  • Drag from either ruler onto the canvas → magenta preview line follows cursor → mouseup commits a new guide.
  • Drag an existing guide (hover within 4 px → cursor swaps to col-resize / row-resize) → repositions on mouseup.
  • Drag a guide back onto a ruler → guide is deleted.
  • Right-click on a guide → context menu shows Delete guide / Delete all on this axis / Delete all guides.
  • Drag an element near a guide → element snaps; the guide thickens to 2 px in deeper magenta.
  • slide-center snap still wins over a guide within the same threshold; guide still wins over a closer element edge.
  • Drag the divider above the speaker notes upward → notes panel grows, canvas shrinks (clamped to ≥ 40 % of column height).
  • Reload — speaker notes height persists (wfb-slides-notes-height); left thumbnail width persists (wfb-slides-left-width).
  • Open with a viewer-role share link → ruler + guides visible, but drag-out / guide-drag / right-click menu inert.
  • Concurrent users see committed guides appear (in-flight preview is local-only in v1; presence field reserved).
  • Presentation mode + per-slide thumbnails do not paint guides.
  • pnpm verify:fast passes (1273 slides tests + 792 docs tests + backend / frontend gates).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Horizontal and vertical rulers in the slides editor with locale-aware units.
    • Draggable presentation-wide alignment guides: create from rulers, move, delete; persist with the presentation.
    • Guides integrated into snapping with clarified priority so center snaps and guides behave predictably.
    • Speaker notes panel now resizable with persistent height.
  • Documentation

    • Added design spec and phased implementation plan for ruler/guide features.

Review Change Stack

hackerwins and others added 13 commits May 23, 2026 15:15
Captures the v1 design for slides rulers (H/V, corner origin, inch/cm)
and presentation-wide draggable guides, along with a six-phase
implementation plan covering the docs ruler refactor, the slides
ruler controller, the guide data model + Yorkie schema, interactions,
snap integration, and read-only handling.

The design intentionally defers text-box-local ruler mode and the
View > Show ruler / Show guides toggles to a later release.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Split the monolithic Ruler class so its tick rendering and locale-
aware unit math can be reused by the upcoming slides ruler without
dragging docs-specific concepts (pages, margins, indent handles)
along.

`getGridConfig` now takes an optional `pxPerInch` so slides can pass
its 144 dpi physical scale (1920 logical px / 13.333") instead of the
hardcoded 96 dpi docs uses. `drawTicks` is a pure renderer that fills
ticks + labels within a measured region, with an optional density
override so the slides ruler can thin labels out when zoomed far
below 1×.

The docs Ruler class is otherwise unchanged: it still owns its
margin-drag and indent-handle interactions and produces the same
output as on main. All 792 unit tests in pnpm verify:fast pass.

Prep for the slides ruler — see docs/design/slides/slides-ruler.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Add a SlidesRuler controller that paints horizontal + vertical ruler
canvases on the slide stage. Reuses the docs tick / unit primitives
through @wafflebase/docs with a slides-specific 144 dpi physical
scale (1920 logical px / 13.333" PDF page). Tick density adapts to
the editor zoom so the ruler stays legible from 0.25× to 3×.

The editor takes optional hRulerCanvas / vRulerCanvas / rulerCorner
refs; when supplied it instantiates the ruler and repaints it on
every render() / setHostSize() call. SlidesView wires the new DOM
elements at the corner + top + left edges of the slide stage and
offsets the slide canvas by RULER_SIZE to make room.

Display-only — drag-out guides and snap integration land in
follow-up phases. See docs/design/slides/slides-ruler.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Introduce a Guide type and `SlidesDocument.guides: Guide[]`, store API
(addGuide / moveGuide / removeGuide), Mem and Yorkie implementations,
and a passive overlay render so existing guides paint as 1-px magenta
lines spanning the slide canvas. Guides live at the presentation
level (not per-slide), matching the Google Slides / PowerPoint
convention.

The Yorkie adapter lazy-inits `root.guides` on attach so pre-v0.4.2
documents load without a migration script. migrateDocument and the
backend readSlidesRoot also default the field to `[]` so the read
path stays shape-stable across pre- / post-ruler decks.

User interactions (drag-out from rulers, guide drag, deletion) and
snap integration land in subsequent phases. See
docs/design/slides/slides-ruler.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Add ruler / guide pointer interactions:

- Mousedown on either ruler canvas seeds a pending guide that follows
  the cursor; mouseup inside the slide commits via store.addGuide,
  mouseup outside cancels.
- Mousedown within 4 px of an existing guide starts a move drag;
  mouseup over the slide commits store.moveGuide (no-op when the
  position is unchanged), mouseup over either ruler removes the guide.
- Hover within 4 px of a guide swaps the canvas cursor to col-resize /
  row-resize so the affordance is discoverable without instructions.
- Right-click on a guide opens a 3-item context menu (delete, delete
  all on this axis, delete all guides).

All in-flight drags paint a half-opacity preview through the overlay
layer so the user can see the gesture before commit; the committed
guide is suppressed during its own move so the user does not see a
double line. Drag math clamps to the slide extent and the
draggingGuide presence field is reserved for the eventual peer
preview wire-up.

See docs/design/slides/slides-ruler.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Extend SnapGuide with a third 'guide' kind and feed each presentation-
wide Guide into snapDelta as three candidate offsets per axis (left /
right / centre for vertical guides; top / bottom / centre for
horizontal). The winner-selection now honours a fixed priority —
slide-center > guide > edge — so an explicit guide beats a nearby
element edge inside the same 8-px threshold.

When a drag lands on a permanent guide the overlay thickens that guide
to 2 px in a deeper magenta instead of overlaying a separate dashed
snap-indicator on top, keeping the visual uncluttered. Snap entries
of kind 'guide' suppress the regular makeGuide overlay for the same
reason. Keyboard nudge keeps its existing snap-less path; the new
parameter on snapDelta is optional so callers that don't pass guides
get the prior behaviour byte-for-byte.

See docs/design/slides/slides-ruler.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Verify the v1 ruler honours the read-only mount contract:

- Ruler-canvas pointerdown is bound inside `attachInteractions()`
  (gated by `options.readOnly`), so a viewer-role mount never starts
  a drag-out gesture.
- Guide hit-test on the slide canvas also lives inside
  `attachInteractions()`'s pointerdown router, so an existing guide
  cannot be moved or deleted from a viewer mount.

Add two regression tests that seed a guide, mount the editor with
`readOnly: true`, dispatch pointerdown / pointerup, and assert the
`guides[]` array is unchanged.

Presenter and thumbnail paths already paint through `SlideRenderer`
without involving `renderOverlay`, so guides are automatically
absent from presentation mode and from per-slide thumbnails — no
new code needed there. Slides PDF export is not yet implemented;
when it lands it follows the same canvas-only path.

Reconcile the design doc with what shipped: the ruler markers and
per-drag position labels are moved to v1.1, snap differentiation is
"thicken + deepen" rather than a separate dashed indicator, and the
in-flight preview is local-only in v1 (the `draggingGuide` presence
slot is reserved for the v1.1 broadcast wiring).

Closes the work tracked in
docs/tasks/active/20260523-slides-ruler-todo.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
A 20-px gray ruler bar around the slide treats the canvas like a
measurable document. Slides is closer to a stage, so the chrome
should defer to the slide elevation rather than frame it.

Tone-down the slides ruler:

- RULER_SIZE drops 20 → 14, reclaiming ~30 % of the canvas gutter on
  the top + left edges.
- Background fill is removed (clearRect instead) so the ruler inherits
  the column color. The corner is transparent too — no more gray
  block at the slide's top-left.
- Tick color shifts from #999 to #b0b0b0; labels shrink from 9 px to
  8 px Arial and sit at y = 0 (was y = 1) so they don't crowd the
  major tick.

drawTicks gains optional `tickHeights`, `labelInset`, and
`verticalLabelInset` params (defaults preserve docs behaviour bit-for-
bit). Slides passes a 6 / 4 / 2 tick height and inset 0 / 4 so the
slim ruler renders cleanly without label / tick overlap.

The 20 → 14 cascade propagates through `SLIDES_RULER_SIZE` to
slides-view's canvas offset and resize observer math without any
further edits.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Two layout fixes for the slides editor shell:

- The slide used to hug the top edge of the canvas column (`alignSelf:
  flex-start`), leaving an awkward gap below on tall viewports. Wrap
  the canvas in a new `canvasArea` flex child that fills the
  remaining vertical space and centers the canvas both horizontally
  and vertically — matches the Google Slides feel.

- Add a horizontal resizer between the canvas area and the speaker-
  notes panel. Drag up to expand notes, drag down to shrink, mirrored
  off the existing thumbnail-panel divider's visual + interaction
  pattern (1-px hairline that thickens on hover, mousedown latches
  the gesture against document-level mousemove/mouseup).

The notes height is a controlled state with a 60-px floor, a 60 %
column-height ceiling, and `localStorage` persistence under
`wfb-slides-notes-height` — the same shape the leftWidth handle
uses. Pulling the ResizeObserver math into a `refitCanvas` function
lets the notes drag rerun the fit without faking a ResizeObserver
tick (the column's outer size hasn't changed during a notes drag,
so the observer would never fire).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The notes textarea was rendered as a bordered box with its own
intrinsic minHeight and a native resize handle — a holdover from
before the slides editor shell had its own drag affordance for the
notes height.

Strip the box: no border, no border-radius, transparent background,
disable native resize. The textarea now stretches to `height: 100%`
so the user's drag of the notes resizer above directly grows the
editable surface. Padding + foreground color stay so the typing area
still has breathing room and follows the active theme.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Move the H / V ruler canvases out of canvasWrap (which holds the
slide + overlay) and into canvasArea (the centred flex container).
The ruler now hugs the canvas-area's top + left edges with
`right: 0` / `bottom: 0`, so it stays put as the slide drifts inside
the frame — either because the column resizes or because the user
drags the speaker-notes panel taller. Mirrors how Photoshop / Figma
anchor rulers to the workspace edge rather than to the artwork.

The slide centring is unchanged; the canvas area picks up
`padding-top: RULER_SIZE; padding-left: RULER_SIZE` so the slide
centres inside the *non-ruler* portion of the frame.

`SlidesRuler` reads each ruler canvas's own `clientWidth` /
`clientHeight` to size its backing store, and projects the slide's
tick "zero" via `(frameWidth - hostWidth) / 2` (the same maths the
flex centring applies). CSS dimensions on the canvases are now the
host's responsibility — the absolute `right: 0` / `bottom: 0` pins
would otherwise be overwritten on every render.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
A bare `position: absolute; left: 14px; right: 0;` doesn't grow a
canvas element to fill the gap — canvases fall back to their bitmap
intrinsic dimensions, so the ruler collapsed into a tiny sliver at
the top-left of canvasArea.

Set the CSS `width` (h-ruler) and `height` (v-ruler) explicitly:

- Seed an initial value at mount so the first paint sees a sensible
  size before the ResizeObserver fires.
- Recompute on every `refitCanvas` tick from
  `canvasArea.getBoundingClientRect()` so a notes drag (which changes
  canvasArea height but not the right column's overall size) still
  resizes the vertical ruler.

The `right: 0` / `bottom: 0` declarations are now redundant and
removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The previous commit referenced hostW / hostH while seeding the ruler
canvas dimensions, but those bindings were declared lower down in the
same effect — TDZ blew up the React mount with "Cannot access
'hostW' before initialization".

Hoist the `let hostW = initial.width; let hostH = initial.height;`
to before the ruler DOM construction so the seed reads valid
numbers. canvasWrap dimensions move in the same block; refit then
overwrites them on the first ResizeObserver tick as before.

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

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds display rulers and presentation-wide alignment guides with shared tick/unit renderer, persistent Yorkie-backed storage, editor wiring for drag/create/move/delete, snap engine integration, overlay rendering, and tests/docs/tasks.

Changes

Slides Ruler Feature with Guides and Shared Rendering

Layer / File(s) Summary
Design and tasks
docs/design/README.md, docs/design/slides/slides-ruler.md, docs/tasks/README.md, docs/tasks/active/20260523-slides-ruler-todo.md
Design doc and active task entry describing rulers/guides, phased implementation plan, verification gates, and non-goals.
Docs ruler refactor & shared primitives
packages/docs/src/view/ruler/unit.ts, packages/docs/src/view/ruler/tick-renderer.ts, packages/docs/src/view/ruler/index.ts, packages/docs/src/index.ts, packages/docs/src/view/editor.ts, packages/docs/test/view/ruler.test.ts
Extracts locale-aware unit/grid helpers and a canvas tick renderer (drawTicks); updates docs Ruler to call shared renderer; updates barrel exports and tests to use the new index entry.
Guide model, migration, and exports
packages/slides/src/model/presentation.ts, packages/slides/src/model/migrate.ts, packages/slides/src/index.ts, packages/slides/src/node.ts
Adds Guide/GuideAxis types and SlidesDocument.guides; migrates/normalizes incoming guide data and re-exports types.
Backend Yorkie root wiring
packages/backend/src/yorkie/slides-tree.ts, packages/backend/src/yorkie/slides-tree.spec.ts
Yorkie root accepts optional guides; reads/writes backfill default []; test fixtures updated.
Frontend types & presence
packages/frontend/src/types/slides-document.ts, packages/frontend/src/types/users.ts
Adds YorkieGuide/guides to Yorkie root type and draggingGuide to SlidesPresence for live preview.
Store interface & in-memory implementation
packages/slides/src/store/store.ts, packages/slides/src/store/memory.ts
Adds addGuide/moveGuide/removeGuide to SlidesStore; implements them in MemSlidesStore with batching and position validation; emptyDocument() initializes guides.
Yorkie-backed Slides store
packages/frontend/src/app/slides/yorkie-slides-store.ts
Backfills root.guides on attach, includes guides in reads/migrations, exposes guide mutation methods, and mirrors guides into undo/redo root replacements.
Slides view DOM and sizing
packages/frontend/src/app/slides/slides-view.tsx
Restructures canvas area to reserve ruler gutter, pins corner/horizontal/vertical ruler elements, adds refitCanvas sizing accounting for notes panel and frame gap, and adds persisted notes resizer UI.
SlidesRuler component
packages/slides/src/view/editor/ruler/ruler.ts, packages/slides/src/view/editor/ruler/index.ts
New SlidesRuler class: DPR-aware canvases, unit detection/override, zoom-adaptive tick density, uses shared drawTicks, exposes render/dispose and constants/types.
Ruler interaction primitives
packages/slides/src/view/editor/ruler/interactions.ts
Stateless helpers: GUIDE_HIT_PX, GuideDragHost, hitTestGuide, startRulerDragOut, startGuideMove, and clamping logic for guide create/move/delete flows.
Editor integration: pointer/context handlers
packages/slides/src/view/editor/editor.ts
Editor accepts ruler hosts, instantiates SlidesRuler, tracks pendingGuide, wires ruler pointer handlers to interaction primitives, supports guide hit-testing, context-menu actions (delete single/axis/all), hover cursors, and includes guides in snap computations.
Snap engine: guide support & priority
packages/slides/src/view/editor/snap.ts
Adds 'guide' SnapGuide kind and guideId, accepts guides param to generate guide candidates, and selects winners by priority (slide-center > guides > edges) before distance.
Overlay rendering & notes styling
packages/slides/src/view/editor/overlay.ts, packages/slides/src/view/editor/notes-panel.ts
Renders permanent guides and pending preview, emphasizes snapped guide, skips duplicate guide-draw in snap loop, and updates notes textarea styles and focus ring helper.
Tests & fixtures
packages/slides/test/store/memory.test.ts, packages/slides/test/view/editor/ruler/ruler.test.ts, packages/slides/test/view/editor/ruler/interactions.test.ts, packages/slides/test/view/editor/snap.test.ts, packages/slides/test/view/editor/editor.test.ts, packages/slides/test/view/canvas/*.test.ts, packages/backend/src/yorkie/slides-tree.spec.ts
Adds guide CRUD tests with batching/undo, SlidesRuler unit tests, interaction tests for drag/create/move/delete and clamping, snap tests for guide priority and thresholds, editor read-only suppression tests, and updated fixtures including guides: [].

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐇 I hopped to the ruler, drew lines neat and straight,

Guides now stand proud, helping layouts align their fate,
Drag, snap, and persist across the deck's song—
Tiny tick marks hum as elements belong.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title "Add slides ruler + presentation-wide draggable guides" accurately summarizes the main feature addition: introducing rulers and draggable alignment guides to the slides editor.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 slides-ruler-extract-core

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 May 23, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 207.7s

Lane Status Duration
sheets:build ✅ pass 13.3s
docs:build ✅ pass 12.1s
slides:build ✅ pass 13.5s
verify:fast ✅ pass 123.8s
frontend:build ✅ pass 17.1s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.6s
cli:build ✅ pass 2.0s
verify:entropy ✅ pass 20.8s

Verification: verify:integration

Result: ✅ PASS

@codecov

codecov Bot commented May 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 25.00000% with 108 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/docs/src/view/ruler/tick-renderer.ts 6.38% 88 Missing ⚠️
packages/docs/src/view/ruler/index.ts 18.18% 18 Missing ⚠️
packages/docs/src/view/editor.ts 0.00% 1 Missing ⚠️
packages/docs/src/view/ruler/unit.ts 96.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":401,"request":{"method":"PATCH","url":"https://api.github.com/repos/wafflebase/wafflebase/issues/comments/4524388993","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nThis PR implements the Slides Ruler feature with persistent presentation-wide alignment guides. It extracts shared tick/unit rendering logic from the docs ruler, introduces a guides data model persisted in Yorkie, implements store layer mutations, renders an interactive ruler component in the slides editor, and integrates guides into the snap engine with priority-based conflict resolution.\n\n## Changes\n\n**Slides Ruler Implementation**\n\n|Layer / File(s)|Summary|\n|---|---|\n|**Design and Implementation Planning** <br> `docs/design/README.md`, `docs/design/slides/slides-ruler.md`, `docs/tasks/README.md`, `docs/tasks/active/20260523-slides-ruler-todo.md`|Design document specifies v1 features: permanent rulers, locale-based units, zoom-adaptive tick density, guide creation via drag-out, snap integration, and read-only suppression. Six-phase implementation plan with verification gates and risk mitigation table.|\n|**Shared Ruler Infrastructure Extraction** <br> `packages/docs/src/view/ruler/tick-renderer.ts`, `packages/docs/src/view/ruler/unit.ts`, `packages/docs/src/view/ruler/index.ts`, `packages/docs/src/view/editor.ts`, `packages/docs/src/index.ts`, `packages/docs/test/view/ruler.test.ts`|Extracts tick rendering (major/half/minor ticks, labels) and unit/grid helpers (locale detection, grid config, snap-to-grid) from docs ruler into reusable modules. Docs ruler refactored to call `drawTicks` helper. New exports expose `RulerUnit`, `GridConfig`, `TickDensity`, `DrawTicksOpts` types.|\n|**Guides Data Model and Schema** <br> `packages/slides/src/model/presentation.ts`, `packages/slides/src/model/migrate.ts`, `packages/backend/src/yorkie/slides-tree.ts`, `packages/frontend/src/types/slides-document.ts`, `packages/frontend/src/types/users.ts`, `packages/backend/src/yorkie/slides-tree.spec.ts`|Introduces `GuideAxis` (`'x' \\| `'y'`) and `Guide` (`id`, `axis`, `position`) types. Extends `SlidesDocument` with `guides: Guide[]`. Adds `SlidesYorkieRoot.guides` optional field with lazy initialization. Extends `YorkieGuide` and `SlidesPresence.draggingGuide` for presence-driven previews. Migration normalizes legacy documents to empty guides array.|\n|**Store Layer: In-Memory and Yorkie** <br> `packages/slides/src/store/store.ts`, `packages/slides/src/store/memory.ts`, `packages/frontend/src/app/slides/yorkie-slides-store.ts`, `packages/slides/src/index.ts`, `packages/slides/src/node.ts`, `packages/slides/src/import/pptx/index.ts`, `packages/slides/src/view/canvas/layout-preview.ts`|`SlidesStore` interface gains `addGuide(axis, position)`, `moveGuide(id, position)`, `removeGuide(id)` methods. `MemSlidesStore` mutates `this.doc.guides` with batching enforcement and id validation. `YorkieSlidesStore` mutates Yorkie root guides with lazy init on add, including document read/write round-trip serialization. All document initializers include `guides: []`.|\n|**Slides Ruler Display Component** <br> `packages/slides/src/view/editor/ruler/ruler.ts`, `packages/slides/src/view/editor/ruler/index.ts`|`SlidesRuler` renders zoom-scaled tick marks for horizontal/vertical rulers using shared `drawTicks` from docs. Detects locale-based units (inch/cm), computes grid from `SLIDES_PX_PER_INCH` (144), selects `TickDensity` based on zoom, manages DPR-aware canvas backing-store, offsets slide center within frame. Exposes `densityFor` test seam and `dispose` lifecycle.|\n|**Ruler Guide Interactions** <br> `packages/slides/src/view/editor/ruler/interactions.ts`|Centralizes guide drag interactions: `GUIDE_HIT_PX` (4px tolerance), `hitTestGuide` selection, `startRulerDragOut` (create gesture), `startGuideMove` (move/delete gesture). Uses `GuideDragHost` interface for editor callbacks (pending preview updates, commit mutations, coordinate conversion, cursor management, region checks).|\n|**Editor DOM Layout and Ruler Wiring** <br> `packages/frontend/src/app/slides/slides-view.tsx`|Adds ruler DOM (corner square + horizontal/vertical canvases) pinned to canvas-area edges. Introduces `refitCanvas` helper for computing slide area dimensions accounting for notes height and ruler gutters. Persists left-panel width and notes-panel height in localStorage. Wires ruler elements to `initializeEditor` for editor-rendered rulers.|\n|**Editor Pointer and Context Menu Handlers** <br> `packages/slides/src/view/editor/editor.ts`|Editor instantiates `SlidesRuler` when hosts provided; maintains `pendingGuide` state for drag preview. `onPointerDown` hit-tests guides before elements; `onContextMenu` shows guide-specific actions (delete single, delete axis, delete all). Hover cursor changes to `col-resize`/`row-resize` over guides. Initial `markDirty()` on mount forces overlay paint. Snap delta receives `guides` parameter.|\n|**Snap Engine Guide Support** <br> `packages/slides/src/view/editor/snap.ts`|`snapDelta` accepts optional `guides` parameter; generates edge and center-based snap candidates per guide. Introduces `SnapGuideKind` ('slide-center' \\| 'guide' \\| 'edge') and `PRIORITY` map. `bestSnapAdjust` applies priority-based selection: higher priority overrides distance unless equal; otherwise smallest distance wins. `SnapGuide` emits optional `guideId` for guide-snapped targets.|\n|**Overlay Guide Rendering** <br> `packages/slides/src/view/editor/overlay.ts`|`OverlayOptions` gains `permanentGuides` (committed guides) and `pendingGuide` (in-flight preview). `renderOverlay` paints committed guides first (1px magenta lines), suppresses guide copy during drag, visually emphasizes snapped guides, renders semi-transparent `pendingGuide`. New `makePermanentGuide` helper renders full-slide magenta lines with `data-guide` id attribute.|\n|**UI Polish and Notes Panel** <br> `packages/slides/src/view/editor/notes-panel.ts`|Notes textarea styled with `height: 100%` to fill drag-resized host, `border: none`, `outline: none`, `resize: none`, `background: transparent` for seamless editor integration.|\n|**Comprehensive Test Coverage** <br> `packages/slides/test/store/memory.test.ts`, `packages/slides/test/view/editor/ruler/ruler.test.ts`, `packages/slides/test/view/editor/ruler/interactions.test.ts`, `packages/slides/test/view/editor/snap.test.ts`, `packages/slides/test/view/editor/editor.test.ts`, `packages/slides/test/view/canvas/*.test.ts`|Adds `guides: []` to all `SlidesDocument` test fixtures. New `MemSlidesStore` suite: add/move/remove guides, batching, undo. `SlidesRuler` tests: constants, render safety, corner bg, density transitions, unit switching, dispose, backing-store DPR. `interactions` tests: `hitTestGuide`, `startRulerDragOut` (commit/cancel), `startGuideMove` (move/delete/clamp). Snap tests: guide snapping, priority over edges, outside threshold. Editor read-only tests: no ruler drag-out, no guide move creation.|\n\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~60 minutes\n\n## Possibly related PRs\n\n- [wafflebase/wafflebase#209](https://github.com/wafflebase/wafflebase/pull/209): Both PRs modify the Slides editor snap/overlay pipeline—`snapDelta` now emits guide metadata and the editor/overlay renders those snap guides during drag, so the ruler-guides work builds directly on the prior live snap guides plumbing.\n- [wafflebase/wafflebase#63](https://github.com/wafflebase/wafflebase/pull/63): The main PR's Slides Ruler work reuses and refactors the existing Docs Editor ruler's unit/grid/tick logic, which is introduced in PR `#63` as the initial Docs ruler implementation.\n- [wafflebase/wafflebase#282](https://github.com/wafflebase/wafflebase/pull/282): The main PR's read-only viewer test cases in the Slides editor build directly on the `readOnly` behavior and interaction-gating introduced in the retrieved PR.\n\n## Poem\n\n> 🎨 The rabbit traces guides with precision,\n> Rulers march left and top with vision—\n> Snaplines converge with priority's grace,\n> Zoom-smart ticks mark every space.\n> Shared tick math flows from docs to slides,\n> As presentation alignment guides arrive! 🎯\n\n</details>\n\n<!-- walkthrough_end -->\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary>\n\n### ❌ Failed checks (1 warning)\n\n|     Check name     | Status     | Explanation                                                                           | Resolution                                                                         |\n| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |\n| Docstring Coverage | ⚠️ Warning | Docstring coverage is 35.14% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |\n\n<details>\n<summary>✅ Passed checks (4 passed)</summary>\n\n|         Check name         | Status   | Explanation                                                                                                                 |\n| :------------------------: | :------- | :-------------------------------------------------------------------------------------------------------------------------- |\n|      Description Check     | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                 |\n|         Title check        | ✅ Passed | The title accurately and concisely summarizes the primary changes: adding rulers and draggable guides to the slides editor. |\n|     Linked Issues check    | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                    |\n| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                    |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>📝 Generate docstrings</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> Create stacked PR\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> Commit on current branch\n\n</details>\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `slides-ruler-extract-core`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->\n\n> [!WARNING]\n> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.\n> \n> <details>\n> <summary>🔧 ESLint</summary>\n> \n> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.\n> \n> ESLint skipped: no ESLint configuration detected in root package.json. To enable, add `eslint` to devDependencies.\n> \n> \n> \n> </details>\n\n<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=wafflebase/wafflebase&utm_content=285)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":0,"signal":{}}},"response":{"url":"https://api.github.com/repos/wafflebase/wafflebase/issues/comments/4524388993","status":401,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","connection":"close","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Sat, 23 May 2026 06:30:41 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-media-type":"github.v3; format=json","x-github-request-id":"8008:2BAFBF:86EEAB3:206DE6EC:6A114991","x-xss-protection":"0"},"data":{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}}}

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

Caution

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

⚠️ Outside diff range comments (2)
packages/slides/src/view/editor/editor.ts (2)

2383-2389: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep permanent guides visible in every live overlay repaint.

Both of these preview paths rebuild overlay.innerHTML without passing permanentGuides / pendingGuide, so presentation guides disappear while connector-endpoint drags or adjustment drags are in flight.

Proposed fix
       renderOverlay(this.options.overlay, selected, {
         scale: this.scale(),
         slideWidth: SLIDE_WIDTH,
         slideHeight: SLIDE_HEIGHT,
         allElements: startSlide.elements,
         connectorAffordance: this.connectorAffordance(),
+        permanentGuides: this.options.store.read().guides,
+        pendingGuide: this.pendingGuide,
       });
     renderOverlay(this.options.overlay, selected, {
       scale: this.scale(),
       slideWidth: SLIDE_WIDTH,
       slideHeight: SLIDE_HEIGHT,
       allElements: synthetic.elements,
+      permanentGuides: this.options.store.read().guides,
+      pendingGuide: this.pendingGuide,
     });

Also applies to: 2539-2544

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/view/editor/editor.ts` around lines 2383 - 2389, The live
overlay repaint paths call renderOverlay(this.options.overlay, selected, {...})
but omit passing the current permanentGuides and pendingGuide, causing
presentation guides to vanish during connector/adjustment drags; update those
renderOverlay invocations (the ones using this.options.overlay and selected with
scale/slideWidth/slideHeight/allElements/connectorAffordance) to include the
active guide state by passing permanentGuides: this.permanentGuides and
pendingGuide: this.pendingGuide (or the equivalent properties on the component)
into the options object so permanent guides remain visible during live preview
repaints.

1089-1119: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Cancel in-flight guide drags when the editor detaches.

These guide drags are started fire-and-forget, and the surrounding comments say the helpers install their own document listeners. detach() only unregisters handlers tracked via this.on(...), so navigating away mid-drag can leave guide listeners running against a disposed editor/store until pointerup happens.

Please verify whether the ruler interaction helpers expose teardown or accept an AbortSignal, then thread that cancellation into detach().

#!/bin/bash
set -euo pipefail

editor_file="packages/slides/src/view/editor/editor.ts"
interactions_file="packages/slides/src/view/editor/ruler/interactions.ts"

echo "== editor attach/detach =="
sed -n '1089,1258p' "$editor_file"

echo
echo "== guide interaction helpers =="
sed -n '1,260p' "$interactions_file"

echo
echo "== listener lifecycle markers =="
rg -n -C2 'startGuideMove|startRulerDragOut|AbortController|AbortSignal|addEventListener|removeEventListener|pointermove|pointerup' \
  "$editor_file" "$interactions_file"

Expected result: if the helper functions add document listeners and neither return a disposer nor consume an abort signal, the teardown hole is confirmed.

Also applies to: 1177-1193

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/view/editor/editor.ts` around lines 1089 - 1119, The
detach() path currently leaves in-flight guide/ruler drags running because
helpers like startGuideMove and startRulerDragOut install their own document
listeners but aren't cancelled; update the ruler interaction lifecycle so
detach() cancels them: inspect startGuideMove/startRulerDragOut in
ruler/interactions.ts and either (A) make them accept an AbortSignal parameter
or (B) make them return a disposer function/object, then store that
AbortController or disposer on the editor instance (e.g. this.activeGuideAbort
or this.activeGuideDisposers) when starting a drag and call abort() or invoke
the disposer inside detach() (alongside this.ruler?.dispose()) to remove any
document listeners and prevent callbacks after teardown. Ensure the symbol names
startGuideMove, startRulerDragOut, detach, and this.ruler?.dispose are used to
locate code to change.
🧹 Nitpick comments (3)
packages/slides/test/view/editor/snap.test.ts (1)

89-94: ⚡ Quick win

Consider asserting array length before accessing element.

For clearer test failures, assert that result.guides has the expected length before accessing result.guides[0]. If the array is unexpectedly empty, the current pattern will produce a less clear error message.

🧪 Proposed pattern for clearer test assertions

For example, at lines 89-94:

     expect(result.dx).toBe(40);
+    expect(result.guides).toHaveLength(1);
     expect(result.guides[0]).toMatchObject({
       axis: 'x',
       position: 900,
       kind: 'guide',
       guideId: 'g1',
     });

Apply the same pattern at lines 109, 124-127, and 136-141.

Also applies to: 109-109, 124-127, 136-141

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/test/view/editor/snap.test.ts` around lines 89 - 94, Add
explicit length assertions before indexing into result.guides to make failures
clearer: for each assertion that accesses result.guides[0] (the block that
currently checks axis/position/kind/guideId) and the similar assertions later in
the test suite, insert expect(result.guides).toHaveLength(<expectedCount>)
immediately prior to accessing element 0 (and use toHaveLength for other indexed
assertions as appropriate); update the test cases that reference result.guides
(the first expect block checking axis/position/kind/guideId and the subsequent
blocks at the other guide assertions) so each indexes only after the length
assertion.
packages/backend/src/yorkie/slides-tree.spec.ts (1)

76-76: ⚡ Quick win

Add an empty-root default assertion for guides.

Since guides is now part of the canonical slides shape, please also assert expect(result.guides).toEqual([]) in the empty-root test to cover the backward-compatibility path.

As per coding guidelines: "**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/src/yorkie/slides-tree.spec.ts` at line 76, Update the
"empty-root" test in slides-tree.spec.ts to assert the new canonical field by
adding expect(result.guides).toEqual([]) alongside the existing assertions;
locate the test that builds/returns result (look for the test name or variable
result in the empty-root test) and add the guides assertion to cover the
backward-compatibility path so the spec verifies that guides defaults to an
empty array.
packages/slides/src/view/editor/ruler/interactions.ts (1)

101-128: ⚡ Quick win

Tear down drag state fully on cancel/dispose.

Current disposers only remove listeners. If a drag is interrupted before pointerup (e.g., unmount/cancel), pending guide state and body cursor can remain stale. Add unified cleanup and pointercancel handling in both drag flows.

💡 Proposed direction
   const onMove = (e: PointerEvent) => {
@@
   };
+  const cleanup = () => {
+    document.removeEventListener('pointermove', onMove);
+    document.removeEventListener('pointerup', onUp);
+    document.removeEventListener('pointercancel', onCancel);
+    host.setBodyCursor(null);
+    host.setPendingGuide(null);
+  };
+  const onCancel = () => cleanup();
   const onUp = (e: PointerEvent) => {
-    document.removeEventListener('pointermove', onMove);
-    document.removeEventListener('pointerup', onUp);
-    host.setBodyCursor(null);
     const { x, y } = host.clientToLogical(e.clientX, e.clientY);
     if (host.isInsideSlide(x, y)) {
       const position = clamp(axis === 'x' ? x : y, axis);
       host.commitAddGuide(axis, position);
     }
-    host.setPendingGuide(null);
+    cleanup();
   };
   document.addEventListener('pointermove', onMove);
   document.addEventListener('pointerup', onUp);
+  document.addEventListener('pointercancel', onCancel);
@@
-  return () => {
-    document.removeEventListener('pointermove', onMove);
-    document.removeEventListener('pointerup', onUp);
-  };
+  return cleanup;

Apply the same cleanup pattern to startGuideMove.

Also applies to: 144-179

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/view/editor/ruler/interactions.ts` around lines 101 -
128, The drag disposers currently only remove pointer listeners which leaves
stale state (pending guide and body cursor) if a drag is cancelled/unmounted;
update the cleanup to be a unified function that removes 'pointermove',
'pointerup', and 'pointercancel' listeners and also calls
host.setPendingGuide(null) and host.setBodyCursor(null); use that cleanup in the
returned disposer and in the onUp/onCancel handlers (replace the inline
document.removeEventListener calls in onUp with a call to the shared cleanup),
and apply the same pattern inside startGuideMove so both guide creation and
guide moving consistently clear state and listeners.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/slides/slides-ruler.md`:
- Around line 70-80: The markdown fenced code blocks in the slides (the package
tree block showing "packages/docs/src/view/ruler/ ..." and the editor-shell
snippet showing "<slide-stage> ..." ) are missing language identifiers and
trigger MD040; update both backtick fences to include a language tag (e.g.,
```text) so markdownlint passes. Locate the two fenced blocks that contain the
package tree and the editor-shell/ruler canvas snippet (the blocks around the
Ruler file list and the <slide-stage> layout) and add the same explicit language
identifier to each opening fence.

In `@packages/frontend/src/app/slides/slides-view.tsx`:
- Around line 480-487: refitCanvas() must clamp the restored notesHeight against
MAX_NOTES_H_RATIO so a value loaded from localStorage can't exceed the current
column cap; before computing reservedBelow (and before using notesHeight to
compute availH), compute a clampedNotesHeight = Math.min(notesHeight,
rightRect.height * MAX_NOTES_H_RATIO) (or similar) and use clampedNotesHeight
for reservedBelow and any layout math; update any logic that reads notesHeight
from storage (and the refitCanvas body) to apply this clamp so the cap is
enforced on resize/reload without requiring a drag.

In `@packages/frontend/src/app/slides/yorkie-slides-store.ts`:
- Around line 1476-1505: The addGuide, moveGuide, and removeGuide mutations
modify this.doc but the undo/redo restore path (replaceRoot / snapshot
restoration) doesn’t include snapshot.guides, so guide changes aren’t reverted;
update the restore logic used by replaceRoot (or the snapshot builder used for
undo/redo) to include snapshot.guides (and preserve id, axis, position) so that
undo/redo fully restores guide arrays, and ensure the guide shape/types match
what addGuide/moveGuide/removeGuide write (referencing functions addGuide,
moveGuide, removeGuide and the snapshot/replaceRoot restore path) so guide state
is captured and restored during history operations.

In `@packages/slides/src/model/migrate.ts`:
- Around line 32-37: migrateGuide currently returns loose values (any) allowing
id to be undefined and position to be NaN/Infinity; update migrateGuide to
return a properly typed Guide: ensure id is a non-empty string (if g?.id is not
a string or empty, synthesize a stable fallback id e.g. via crypto.randomUUID()
or project createId helper), normalize axis to 'x'|'y' as you already do, and
coerce/validate position with Number.isFinite(g?.position) ? Number(g.position)
: 0; change the function signature to return Guide (not any) so callers like
store.moveGuide/removeGuide, overlay, and snap consume safe values and avoid
throwing or propagating NaN.

In `@packages/slides/src/store/memory.ts`:
- Around line 878-891: Reject non-finite positions in addGuide and moveGuide by
validating the position with Number.isFinite(position) before mutating state; in
addGuide (where Guide is created) and in moveGuide (before assigning
guide.position) call requireBatch(), then if Number.isFinite(position) is false
throw an Error like `Invalid guide position: ${position}`, otherwise proceed
(use existing symbols addGuide, moveGuide, generateId, this.doc.guides,
GuideAxis, guide.position).

In `@packages/slides/src/view/editor/notes-panel.ts`:
- Around line 55-56: The code removes the textarea outline (ta.style.outline =
'none'), which breaks keyboard focus visibility; instead, keep a focus indicator
by replacing the blanket outline removal with a focus-visible style: add a CSS
class (eg. notes-textarea) or inject a stylesheet for the textarea that sets
outline: none only for :focus (not :focus-visible) and defines a subtle
accessible focus-visible rule (e.g., thin high-contrast ring or box-shadow) that
preserves the borderless look when unfocused but shows a clear indicator when
the element has keyboard focus; update the code that sets ta.style.outline to
apply the class or ensure the injected focus-visible CSS is present so ta gains
the new focus-visible styling.

In `@packages/slides/src/view/editor/overlay.ts`:
- Around line 118-124: The highlighted snapped guide is widened to 2px but not
repositioned, shifting the visible line by 0.5px; update the overlay logic that
handles snappedGuideIds (where g.axis === 'x' sets el.style.width and else sets
el.style.height) to also offset the element’s position so the 2px stroke is
centered on the snapped coordinate: when g.axis === 'x' set el.style.left to the
snapped coordinate minus 1px (using g.coord or the existing coordinate source),
and when axis !== 'x' set el.style.top to the snapped coordinate minus 1px,
keeping the rest of the visual styling intact.

In `@packages/slides/src/view/editor/ruler/interactions.ts`:
- Around line 69-80: hitTestGuide currently returns the first guide within
GUIDE_HIT_PX instead of the closest; modify hitTestGuide to scan all guides and
track the minimum distance and corresponding guide (using g.axis and g.position
with point.x/point.y) and after the loop return the guide with the smallest
distance if that distance <= GUIDE_HIT_PX, otherwise return null. Ensure types
remain Guide | null and behavior unchanged when no guide is within threshold.

---

Outside diff comments:
In `@packages/slides/src/view/editor/editor.ts`:
- Around line 2383-2389: The live overlay repaint paths call
renderOverlay(this.options.overlay, selected, {...}) but omit passing the
current permanentGuides and pendingGuide, causing presentation guides to vanish
during connector/adjustment drags; update those renderOverlay invocations (the
ones using this.options.overlay and selected with
scale/slideWidth/slideHeight/allElements/connectorAffordance) to include the
active guide state by passing permanentGuides: this.permanentGuides and
pendingGuide: this.pendingGuide (or the equivalent properties on the component)
into the options object so permanent guides remain visible during live preview
repaints.
- Around line 1089-1119: The detach() path currently leaves in-flight
guide/ruler drags running because helpers like startGuideMove and
startRulerDragOut install their own document listeners but aren't cancelled;
update the ruler interaction lifecycle so detach() cancels them: inspect
startGuideMove/startRulerDragOut in ruler/interactions.ts and either (A) make
them accept an AbortSignal parameter or (B) make them return a disposer
function/object, then store that AbortController or disposer on the editor
instance (e.g. this.activeGuideAbort or this.activeGuideDisposers) when starting
a drag and call abort() or invoke the disposer inside detach() (alongside
this.ruler?.dispose()) to remove any document listeners and prevent callbacks
after teardown. Ensure the symbol names startGuideMove, startRulerDragOut,
detach, and this.ruler?.dispose are used to locate code to change.

---

Nitpick comments:
In `@packages/backend/src/yorkie/slides-tree.spec.ts`:
- Line 76: Update the "empty-root" test in slides-tree.spec.ts to assert the new
canonical field by adding expect(result.guides).toEqual([]) alongside the
existing assertions; locate the test that builds/returns result (look for the
test name or variable result in the empty-root test) and add the guides
assertion to cover the backward-compatibility path so the spec verifies that
guides defaults to an empty array.

In `@packages/slides/src/view/editor/ruler/interactions.ts`:
- Around line 101-128: The drag disposers currently only remove pointer
listeners which leaves stale state (pending guide and body cursor) if a drag is
cancelled/unmounted; update the cleanup to be a unified function that removes
'pointermove', 'pointerup', and 'pointercancel' listeners and also calls
host.setPendingGuide(null) and host.setBodyCursor(null); use that cleanup in the
returned disposer and in the onUp/onCancel handlers (replace the inline
document.removeEventListener calls in onUp with a call to the shared cleanup),
and apply the same pattern inside startGuideMove so both guide creation and
guide moving consistently clear state and listeners.

In `@packages/slides/test/view/editor/snap.test.ts`:
- Around line 89-94: Add explicit length assertions before indexing into
result.guides to make failures clearer: for each assertion that accesses
result.guides[0] (the block that currently checks axis/position/kind/guideId)
and the similar assertions later in the test suite, insert
expect(result.guides).toHaveLength(<expectedCount>) immediately prior to
accessing element 0 (and use toHaveLength for other indexed assertions as
appropriate); update the test cases that reference result.guides (the first
expect block checking axis/position/kind/guideId and the subsequent blocks at
the other guide assertions) so each indexes only after the length assertion.
🪄 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: 403c281b-9f4c-463b-b59e-a4fe5c80053b

📥 Commits

Reviewing files that changed from the base of the PR and between 95e5686 and 9b6b112.

📒 Files selected for processing (40)
  • docs/design/README.md
  • docs/design/slides/slides-ruler.md
  • docs/tasks/README.md
  • docs/tasks/active/20260523-slides-ruler-todo.md
  • packages/backend/src/yorkie/slides-tree.spec.ts
  • packages/backend/src/yorkie/slides-tree.ts
  • packages/docs/src/index.ts
  • packages/docs/src/view/editor.ts
  • packages/docs/src/view/ruler/index.ts
  • packages/docs/src/view/ruler/tick-renderer.ts
  • packages/docs/src/view/ruler/unit.ts
  • packages/docs/test/view/ruler.test.ts
  • packages/frontend/src/app/slides/slides-view.tsx
  • packages/frontend/src/app/slides/yorkie-slides-store.ts
  • packages/frontend/src/types/slides-document.ts
  • packages/frontend/src/types/users.ts
  • packages/slides/src/import/pptx/index.ts
  • packages/slides/src/index.ts
  • packages/slides/src/model/migrate.ts
  • packages/slides/src/model/presentation.ts
  • packages/slides/src/node.ts
  • packages/slides/src/store/memory.ts
  • packages/slides/src/store/store.ts
  • packages/slides/src/view/canvas/layout-preview.ts
  • packages/slides/src/view/editor/editor.ts
  • packages/slides/src/view/editor/notes-panel.ts
  • packages/slides/src/view/editor/overlay.ts
  • packages/slides/src/view/editor/ruler/index.ts
  • packages/slides/src/view/editor/ruler/interactions.ts
  • packages/slides/src/view/editor/ruler/ruler.ts
  • packages/slides/src/view/editor/snap.ts
  • packages/slides/test/store/memory.test.ts
  • packages/slides/test/view/canvas/element-renderer.test.ts
  • packages/slides/test/view/canvas/group-render.test.ts
  • packages/slides/test/view/canvas/slide-renderer.test.ts
  • packages/slides/test/view/canvas/thumbnail.test.ts
  • packages/slides/test/view/editor/editor.test.ts
  • packages/slides/test/view/editor/ruler/interactions.test.ts
  • packages/slides/test/view/editor/ruler/ruler.test.ts
  • packages/slides/test/view/editor/snap.test.ts

Comment thread docs/design/slides/slides-ruler.md Outdated
Comment thread packages/frontend/src/app/slides/slides-view.tsx
Comment thread packages/frontend/src/app/slides/yorkie-slides-store.ts
Comment thread packages/slides/src/model/migrate.ts Outdated
Comment thread packages/slides/src/store/memory.ts
Comment thread packages/slides/src/view/editor/notes-panel.ts
Comment thread packages/slides/src/view/editor/overlay.ts Outdated
Comment thread packages/slides/src/view/editor/ruler/interactions.ts Outdated
At zoom-to-fit the slide canvas was butting right up against the H/V
rulers — the 12-px drop shadow blurred into the tick marks and the
1-px slide outline overlapped the ruler edge, making the chrome
look smudgy.

Subtract a 12-px gap from `slideAvailW` / `slideAvailH` inside
`refitCanvas` so the slide is sized to leave a constant margin from
every ruler / canvas-area edge. The flex centring on `canvasArea`
distributes the freed space evenly, and the existing `(frame -
host) / 2` slide-offset math inside `SlidesRuler` keeps tick "0"
aligned with the slide's new left / top edge — no ruler-side
changes needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <[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: 3

♻️ Duplicate comments (2)
packages/frontend/src/app/slides/yorkie-slides-store.ts (1)

1476-1505: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guide mutations are not fully undo/redo-safe yet.

These new guide writes are batched/snapshotted, but replaceRoot (Line 472+) still doesn’t restore snapshot.guides, so guide add/move/remove won’t reliably revert.

💡 Proposed fix
  private replaceRoot(snapshot: SlidesDocument): void {
    this.doc.update((r) => {
      r.meta = clone(snapshot.meta);
-     const rootAny = r as { themes?: Theme[]; masters?: Master[] };
+     const rootAny = r as {
+       themes?: Theme[];
+       masters?: Master[];
+       guides?: Array<{ id: string; axis: 'x' | 'y'; position: number }>;
+     };
      rootAny.themes = clone(snapshot.themes);
      rootAny.masters = clone(snapshot.masters);
+     rootAny.guides = clone(snapshot.guides);
      const nextSlides: YorkieSlide[] = snapshot.slides.map((s) => ({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/frontend/src/app/slides/yorkie-slides-store.ts` around lines 1476 -
1505, The guide mutations (addGuide, moveGuide, removeGuide) are batched but
undo/redo fails because replaceRoot doesn't restore snapshot.guides; update the
logic in replaceRoot to include snapshot.guides in the restored state (ensure
you copy/assign snapshot.guides onto the new root object rather than ignoring
it), preserve types for guides (Array<{id: string; axis: 'x'|'y'; position:
number}>) and use a defensive clone when applying snapshot.guides so subsequent
mutations don't share references with the snapshot.
packages/slides/src/store/memory.ts (1)

878-891: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard guide positions against non-finite numbers.

addGuide and moveGuide accept NaN/Infinity (Line 878 and Line 886), which can poison downstream snap/overlay geometry.

💡 Proposed fix
  addGuide(axis: GuideAxis, position: number): string {
    this.requireBatch();
+   if (!Number.isFinite(position)) {
+     throw new Error(`Invalid guide position: ${position}`);
+   }
    const id = generateId();
    const guide: Guide = { id, axis, position };
    this.doc.guides.push(guide);
    return id;
  }

  moveGuide(id: string, position: number): void {
    this.requireBatch();
+   if (!Number.isFinite(position)) {
+     throw new Error(`Invalid guide position: ${position}`);
+   }
    const guide = this.doc.guides.find((g) => g.id === id);
    if (!guide) throw new Error(`Guide not found: ${id}`);
    guide.position = position;
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/store/memory.ts` around lines 878 - 891, Validate that
the position is a finite number in both addGuide and moveGuide: before
creating/adding the Guide in addGuide (function addGuide) and before assigning
guide.position in moveGuide (function moveGuide), check
Number.isFinite(position) and throw a clear error (e.g., "Invalid guide
position") if it isn't; keep the existing requireBatch() calls and error
behavior for missing guide in moveGuide.
🧹 Nitpick comments (2)
packages/backend/src/yorkie/slides-tree.spec.ts (1)

76-76: ⚡ Quick win

Add an explicit default assertion for guides in the empty-root read test.

Now that guides is part of the canonical top-level shape, add expect(result.guides).toEqual([]) in the returns sensible defaults when reading an empty root case to lock migration/default behavior.

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

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/src/yorkie/slides-tree.spec.ts` at line 76, In the "returns
sensible defaults when reading an empty root" test in slides-tree.spec.ts, add
an explicit assertion that result.guides equals an empty array to lock in the
new canonical top-level shape; locate the test case (the one that currently
checks defaults for an empty root) and add expect(result.guides).toEqual([])
alongside the existing default assertions so the test fails if guides is ever
omitted or given a different default.
packages/slides/test/store/memory.test.ts (1)

778-789: ⚡ Quick win

The “one undo step” assertion is currently non-discriminating.

Because state is [] both before and after undo(), this test can pass even if no undo entry was created. Assert stack behavior too (e.g., canUndo() true after batch, then false after one undo()).

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

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/test/store/memory.test.ts` around lines 778 - 789, Test
currently only checks final state array and can hide missing undo stack
behavior; update the spec to assert undo stack behavior: after creating const
store = new MemSlidesStore() and wrapping operations in store.batch(() => {
const id = store.addGuide('x', 100); store.moveGuide(id, 200);
store.removeGuide(id); }), assert store.canUndo() is true before calling
store.undo(), then call store.undo() and assert store.canUndo() is false (and
still assert read().guides remains []); use the MemSlidesStore, batch, addGuide,
moveGuide, removeGuide, read, undo, and canUndo symbols to locate the test and
add these extra expectations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/slides/slides-ruler.md`:
- Around line 103-106: Update the design spec constants for the ruler thickness
from 20 px to 14 px: change the <ruler-corner> size to 14×14, set
<h-ruler-canvas> top/left offsets from top:0; left:20px; height:20px to top:0;
left:14px; height:14px, set <v-ruler-canvas> offsets from top:20px; left:0;
bottom:0; width:20px to top:14px; left:0; bottom:0; width:14px, and set
<canvas-pane> top/left offsets from top:20px; left:20px to top:14px; left:14px;
also apply the same 20→14px replacements for the other occurrences of these
elements mentioned (the later spec blocks for the same components).

In `@packages/frontend/src/app/slides/yorkie-slides-store.ts`:
- Around line 1476-1494: addGuide and moveGuide currently accept non-finite
positions (NaN/Infinity); add validation at the start of both functions to
reject non-finite values by using Number.isFinite(position) and throw a clear
Error when invalid. Update addGuide (before generateId/this.doc.update) and
moveGuide (before this.doc.update) to perform this check so invalid guide
geometry never gets written to the CRDT; keep existing calls to requireBatch(),
generateId(), and doc.update unchanged aside from the added guard.

In `@packages/slides/test/view/editor/snap.test.ts`:
- Around line 112-123: The test's intent is to verify that a user guide outranks
a closer element edge, but the fixture makes the guide closer (position 505
gives a 2px gap vs the edge's 3px). Update the guides fixture so the guide is
slightly farther than the edge but still within threshold (e.g., change guides =
[{ id: 'g1', axis: 'x', position: 507 }]) so the edge is the closer target (3px)
while the guide (4px) still qualifies, then run snapDelta(...) as before to
assert the guide wins; adjust the inline comments to reflect the new gaps if
present and keep references to snapDelta, guides, and others intact.

---

Duplicate comments:
In `@packages/frontend/src/app/slides/yorkie-slides-store.ts`:
- Around line 1476-1505: The guide mutations (addGuide, moveGuide, removeGuide)
are batched but undo/redo fails because replaceRoot doesn't restore
snapshot.guides; update the logic in replaceRoot to include snapshot.guides in
the restored state (ensure you copy/assign snapshot.guides onto the new root
object rather than ignoring it), preserve types for guides (Array<{id: string;
axis: 'x'|'y'; position: number}>) and use a defensive clone when applying
snapshot.guides so subsequent mutations don't share references with the
snapshot.

In `@packages/slides/src/store/memory.ts`:
- Around line 878-891: Validate that the position is a finite number in both
addGuide and moveGuide: before creating/adding the Guide in addGuide (function
addGuide) and before assigning guide.position in moveGuide (function moveGuide),
check Number.isFinite(position) and throw a clear error (e.g., "Invalid guide
position") if it isn't; keep the existing requireBatch() calls and error
behavior for missing guide in moveGuide.

---

Nitpick comments:
In `@packages/backend/src/yorkie/slides-tree.spec.ts`:
- Line 76: In the "returns sensible defaults when reading an empty root" test in
slides-tree.spec.ts, add an explicit assertion that result.guides equals an
empty array to lock in the new canonical top-level shape; locate the test case
(the one that currently checks defaults for an empty root) and add
expect(result.guides).toEqual([]) alongside the existing default assertions so
the test fails if guides is ever omitted or given a different default.

In `@packages/slides/test/store/memory.test.ts`:
- Around line 778-789: Test currently only checks final state array and can hide
missing undo stack behavior; update the spec to assert undo stack behavior:
after creating const store = new MemSlidesStore() and wrapping operations in
store.batch(() => { const id = store.addGuide('x', 100); store.moveGuide(id,
200); store.removeGuide(id); }), assert store.canUndo() is true before calling
store.undo(), then call store.undo() and assert store.canUndo() is false (and
still assert read().guides remains []); use the MemSlidesStore, batch, addGuide,
moveGuide, removeGuide, read, undo, and canUndo symbols to locate the test and
add these extra expectations.
🪄 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: c967d170-a7a6-45bc-9c8d-b78d073f8573

📥 Commits

Reviewing files that changed from the base of the PR and between 95e5686 and 14817f4.

📒 Files selected for processing (40)
  • docs/design/README.md
  • docs/design/slides/slides-ruler.md
  • docs/tasks/README.md
  • docs/tasks/active/20260523-slides-ruler-todo.md
  • packages/backend/src/yorkie/slides-tree.spec.ts
  • packages/backend/src/yorkie/slides-tree.ts
  • packages/docs/src/index.ts
  • packages/docs/src/view/editor.ts
  • packages/docs/src/view/ruler/index.ts
  • packages/docs/src/view/ruler/tick-renderer.ts
  • packages/docs/src/view/ruler/unit.ts
  • packages/docs/test/view/ruler.test.ts
  • packages/frontend/src/app/slides/slides-view.tsx
  • packages/frontend/src/app/slides/yorkie-slides-store.ts
  • packages/frontend/src/types/slides-document.ts
  • packages/frontend/src/types/users.ts
  • packages/slides/src/import/pptx/index.ts
  • packages/slides/src/index.ts
  • packages/slides/src/model/migrate.ts
  • packages/slides/src/model/presentation.ts
  • packages/slides/src/node.ts
  • packages/slides/src/store/memory.ts
  • packages/slides/src/store/store.ts
  • packages/slides/src/view/canvas/layout-preview.ts
  • packages/slides/src/view/editor/editor.ts
  • packages/slides/src/view/editor/notes-panel.ts
  • packages/slides/src/view/editor/overlay.ts
  • packages/slides/src/view/editor/ruler/index.ts
  • packages/slides/src/view/editor/ruler/interactions.ts
  • packages/slides/src/view/editor/ruler/ruler.ts
  • packages/slides/src/view/editor/snap.ts
  • packages/slides/test/store/memory.test.ts
  • packages/slides/test/view/canvas/element-renderer.test.ts
  • packages/slides/test/view/canvas/group-render.test.ts
  • packages/slides/test/view/canvas/slide-renderer.test.ts
  • packages/slides/test/view/canvas/thumbnail.test.ts
  • packages/slides/test/view/editor/editor.test.ts
  • packages/slides/test/view/editor/ruler/interactions.test.ts
  • packages/slides/test/view/editor/ruler/ruler.test.ts
  • packages/slides/test/view/editor/snap.test.ts

Comment thread docs/design/slides/slides-ruler.md Outdated
Comment thread packages/frontend/src/app/slides/yorkie-slides-store.ts
Comment thread packages/slides/test/view/editor/snap.test.ts Outdated
hackerwins and others added 2 commits May 24, 2026 11:15
CodeRabbit review fixes (8):

- migrate.ts: typed migrateGuide; synthesize id when missing; reject
  NaN/Infinity position (Critical — undo + render math break otherwise)
- yorkie-slides-store: replaceRoot now restores snapshot.guides
  (Major — undo/redo of guide ops was silently dropping them)
- memory.ts + yorkie store: assertFinitePosition on addGuide /
  moveGuide so non-finite values fail loudly instead of poisoning
  downstream snap / overlay math
- notes-panel.ts: add a `:focus-visible` outline rule (WCAG 2.4.7 —
  the inline `outline: none` stripped the default ring; keyboard
  users now get a subtle 2-px inset ring)
- overlay.ts: shift left/top by -0.5 px when thickening a snapped
  guide to 2 px so the line stays centred on the snap coordinate
- ruler/interactions.ts: hitTestGuide now scans the full guide list
  and returns the closest within threshold (matches its docstring;
  prior implementation returned the first qualifying guide)
- slides-view.tsx refitCanvas: re-clamp notesHeight against the
  live MAX_NOTES_H_RATIO cap so a localStorage value (or window
  resize) can't leave the panel exceeding the cap until next drag
- slides-ruler.md: add `text` language tag to two fenced code blocks
  (markdownlint MD040)

Feature — extend permanent guides into the ruler area:

Permanent guide lines now extend past the slide bounds by
GUIDE_EXTEND_PX (10 000) on each side so they visually connect into
the H/V rulers, matching the Figma / Sketch convention. canvasArea
gains `overflow: hidden` to clip the excess at the frame edge so
lines don't leak into the speaker-notes panel. canvasWrap moves to
`z-index: 2` and the ruler canvases drop to `z-index: 1` so the
guide divs paint above the ruler ticks at their crossing points
(corner stays at z-index 3; no guide can cross it because guides
always sit at slide-x ≥ 0, which maps well to the right of the
corner's footprint after frame padding + centring).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- design doc: sync the 20-px ruler references to the shipped 14-px
  slim ruler (DOM tree snippet at lines 100-110 + nearby prose +
  risks table). Spec drift would otherwise mislead anyone reading
  slides-ruler.md after the slim-ruler commit.
- snap.test priority case: the fixture had the guide closer (2 px)
  than the edge (3 px), so the test trivially passed even without
  the priority rule. Move the guide to position 507 so the edge is
  the closer candidate (3 px) and the guide (4 px) only wins via
  priority — the rule the test actually exists to pin.

Skipped: the duplicate "reject non-finite guide positions in Yorkie
mutators" comment — `assertFiniteGuidePosition` already guards both
`addGuide` and `moveGuide` on the YorkieSlidesStore (added in the
previous review commit at line 1512); CodeRabbit appears to have
re-flagged it against a pre-fix snapshot.

Co-Authored-By: Claude Opus 4.7 (1M context) <[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.

Caution

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

⚠️ Outside diff range comments (1)
packages/slides/src/view/editor/notes-panel.ts (1)

61-61: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Inline outline: none suppresses the injected :focus-visible ring

ta.style.outline = 'none' sets the outline property inline, which outranks the stylesheet rule injected by ensureNotesPanelStyles(); the .wfb-slides-notes-ta:focus-visible outline won’t reliably show for keyboard users.

♿ Minimal fix
-  ta.style.outline = 'none';
+  // Outline behavior is controlled in ensureNotesPanelStyles().
   style.textContent =
+    '.wfb-slides-notes-ta:focus { outline: none; }' +
     '.wfb-slides-notes-ta:focus-visible {' +
     '  outline: 2px solid var(--ring, `#3a7`);' +
     '  outline-offset: -2px;' +
     '  border-radius: 2px;' +
     '}';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/view/editor/notes-panel.ts` at line 61, The inline
assignment ta.style.outline = 'none' overrides the stylesheet's
.wfb-slides-notes-ta:focus-visible ring, so remove that inline outline setting
(delete the ta.style.outline = 'none' line) and rely on ensureNotesPanelStyles()
/ the .wfb-slides-notes-ta class to provide the focus-visible outline for
keyboard users; if you must reset outline programmatically, remove the inline
property with ta.style.removeProperty('outline') instead of setting it to
'none'.
🧹 Nitpick comments (1)
packages/slides/src/model/migrate.ts (1)

47-53: ⚡ Quick win

Prefer unknown + narrowing instead of any in migrateGuide

migrateGuide is the migration boundary for arbitrary document data; using unknown preserves the existing runtime behavior while keeping the input type-safe.

♻️ Suggested refactor
-function migrateGuide(g: any): Guide {
-  const id = typeof g?.id === 'string' && g.id.length > 0 ? g.id : generateId();
-  const axis: GuideAxis = g?.axis === 'y' ? 'y' : 'x';
-  const rawPos = g?.position;
+function migrateGuide(g: unknown): Guide {
+  const raw = (g ?? {}) as {
+    id?: unknown;
+    axis?: unknown;
+    position?: unknown;
+  };
+  const id = typeof raw.id === 'string' && raw.id.length > 0 ? raw.id : generateId();
+  const axis: GuideAxis = raw.axis === 'y' ? 'y' : 'x';
+  const rawPos = raw.position;
   const position =
     typeof rawPos === 'number' && Number.isFinite(rawPos) ? rawPos : 0;
   return { id, axis, position };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/model/migrate.ts` around lines 47 - 53, Change the
parameter type of migrateGuide from any to unknown and keep the existing runtime
narrowing checks (typeof g?.id === 'string', g?.axis === 'y', typeof rawPos ===
'number' && Number.isFinite(rawPos)) so the function remains a safe migration
boundary; update the signature migrateGuide(g: unknown): Guide and refer to g
only after those guards (no unchecked property access or type assertions) so the
current logic that computes id, axis, and position is preserved while using
unknown for type safety.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/slides/src/view/editor/notes-panel.ts`:
- Line 61: The inline assignment ta.style.outline = 'none' overrides the
stylesheet's .wfb-slides-notes-ta:focus-visible ring, so remove that inline
outline setting (delete the ta.style.outline = 'none' line) and rely on
ensureNotesPanelStyles() / the .wfb-slides-notes-ta class to provide the
focus-visible outline for keyboard users; if you must reset outline
programmatically, remove the inline property with
ta.style.removeProperty('outline') instead of setting it to 'none'.

---

Nitpick comments:
In `@packages/slides/src/model/migrate.ts`:
- Around line 47-53: Change the parameter type of migrateGuide from any to
unknown and keep the existing runtime narrowing checks (typeof g?.id ===
'string', g?.axis === 'y', typeof rawPos === 'number' &&
Number.isFinite(rawPos)) so the function remains a safe migration boundary;
update the signature migrateGuide(g: unknown): Guide and refer to g only after
those guards (no unchecked property access or type assertions) so the current
logic that computes id, axis, and position is preserved while using unknown for
type safety.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89b9f4ad-e2c7-4a70-aa1b-957a802dc882

📥 Commits

Reviewing files that changed from the base of the PR and between 14817f4 and a0003f1.

📒 Files selected for processing (9)
  • docs/design/slides/slides-ruler.md
  • packages/frontend/src/app/slides/slides-view.tsx
  • packages/frontend/src/app/slides/yorkie-slides-store.ts
  • packages/slides/src/model/migrate.ts
  • packages/slides/src/store/memory.ts
  • packages/slides/src/view/editor/notes-panel.ts
  • packages/slides/src/view/editor/overlay.ts
  • packages/slides/src/view/editor/ruler/interactions.ts
  • packages/slides/test/view/editor/snap.test.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/design/slides/slides-ruler.md

@hackerwins
hackerwins merged commit eb79963 into main May 24, 2026
4 checks passed
@hackerwins
hackerwins deleted the slides-ruler-extract-core branch May 24, 2026 03:30
hackerwins added a commit that referenced this pull request May 24, 2026
The root "build" script never built @wafflebase/docs and ran every
package build concurrently with no ordering. Since cross-package type
resolution reads each dependency's built dist/*.d.ts (gitignored, often
stale) rather than its source, consumers compiled against outdated
declarations: slides failed on missing docs ruler exports and backend
failed on the slides Guide/guides symbols added in #285.

Build in topological order instead: (docs, sheets) -> slides ->
(frontend, backend, cli), so every package compiles against freshly
built dependency declarations. Verified from a clean dist state.
hackerwins added a commit that referenced this pull request May 24, 2026
These tasks' features merged to main (PRs #267 #271 #277 #284 #285)
but their plan checkboxes were never ticked, so the todo files
lingered in tasks/active. Added a status note citing each merge
commit, marked the checkboxes complete, and ran tasks:archive to
move them under archive/2026/05 with regenerated indexes.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
@hackerwins hackerwins mentioned this pull request May 24, 2026
2 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