Skip to content

Slides smart guides (equal-spacing / distance / size)#322

Merged
hackerwins merged 18 commits into
mainfrom
feat/slides-smart-guides
May 31, 2026
Merged

Slides smart guides (equal-spacing / distance / size)#322
hackerwins merged 18 commits into
mainfrom
feat/slides-smart-guides

Conversation

@hackerwins

@hackerwins hackerwins commented May 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds PowerPoint / Google Slides-style smart guides during element drag and resize in the slides editor:
    • Equal-spacing — middle and end-trio detection on row and column; gaps balance when within 8 px.
    • Equal-distance — pair-to-pair gap matching so a new placement reuses an existing gap.
    • Equal-size — on resize, w/h snaps to a peer with handle-aware origin compensation.
  • Renders red double-headed arrows for spacing/distance (with a dark translucent distance label matching the existing rotate-tooltip style) and a 1 px dashed outline around equal-size peer frames.
  • Detection lives in a new pure module packages/slides/src/view/editor/smart-guides.ts; overlay primitives in overlay.ts; wired into editor.ts move-drag and startResize reusing the existing collectSnapCandidates pipeline (no extra candidate work).

Design: docs/design/slides/slides-smart-guides.md
Plan: docs/tasks/active/20260531-slides-smart-guides-todo.md
Lessons: docs/tasks/active/20260531-slides-smart-guides-lessons.md

Test plan

  • 22 new unit tests in smart-guides.test.ts cover middle/end trio, pair-to-pair distance, all 8 ResizeHandle origin-compensation paths, multi-peer equal-size, and threshold/perpendicular boundaries
  • pnpm verify:fast green (56 files / 871 tests)
  • pnpm --filter @wafflebase/slides build clean
  • Manual smoke in pnpm dev:
    • Drag a middle element of 3 in a row → arrows show on both sides, distance labels equal, snap engages near balanced position
    • Drag toward a neighbour with an existing pair-gap elsewhere → both arrow pairs show, dragged element snaps to the matched distance
    • Resize a shape's e handle to within 8 px of another shape's w → snap + dashed red outline on the peer; release → outline clears
    • Resize with Shift → preserve-aspect wins, equal-size snap is bypassed
    • Drag with Shift (axis-lock) → smart-guide arrows still visible on the locked-out axis, but delta stays zero
    • Release after any smart-guide snap → arrows/outline clear instantly (no fade)

Known cut from v1 (tracked in lessons)

  • Integration test in editor.test.ts (unit tests cover the algorithm; manual smoke covers wiring)
  • Screenshot diffs in pnpm verify:browser:docker
  • N > 30 viewport culling — O(N²) is fine for typical decks; revisit on PPTX imports with 40+ elements
  • Rotated-resize equal-size matching
  • MobileSlidesView light edit (touch ergonomics need separate tuning)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added smart guides with visual overlays (arrows and dashed outlines) that activate during object dragging and resizing, highlighting equal spacing, equal distance, and matching dimensions between elements.
  • Tests

    • Added test suite covering smart guide detection and resize snapping scenarios.
  • Documentation

    • Added design specifications and implementation documentation for the smart guides system.

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@hackerwins, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 19 minutes and 50 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0fa64752-1e0d-4d08-a74f-4a121e230423

📥 Commits

Reviewing files that changed from the base of the PR and between 59b842b and 01100a6.

📒 Files selected for processing (9)
  • docs/design/README.md
  • docs/design/slides/slides-smart-guides.md
  • docs/tasks/README.md
  • docs/tasks/active/20260531-slides-smart-guides-lessons.md
  • docs/tasks/active/20260531-slides-smart-guides-todo.md
  • packages/slides/src/view/editor/editor.ts
  • packages/slides/src/view/editor/overlay.ts
  • packages/slides/src/view/editor/smart-guides.ts
  • packages/slides/test/view/editor/smart-guides.test.ts
📝 Walkthrough

Walkthrough

This PR implements smart snapping guides for the slides editor, adding equal-spacing, equal-distance (during drag), and equal-size (during resize) snapping with visual arrow and dashed-box overlays. The feature includes core detection algorithms, comprehensive tests, overlay rendering, and full editor integration.

Changes

Slides Smart Guides Feature

Layer / File(s) Summary
Design specification and task planning
docs/design/README.md, docs/design/slides/slides-smart-guides.md, docs/tasks/README.md, docs/tasks/active/20260531-slides-smart-guides-lessons.md, docs/tasks/active/20260531-slides-smart-guides-todo.md
Design document introduces three snapping patterns using 8px threshold; task README updated with latest active task; detailed implementation plan outlines 9 checkpoint tasks covering core logic, tests, overlay, and editor wiring; lessons document records implementation discoveries and deferred v1 items.
Smart guides detection and matching
packages/slides/src/view/editor/smart-guides.ts
New module exports Span and SmartGuide types; smartGuides(bbox, dx, dy, others) evaluates equal-spacing and equal-distance candidates and returns refined deltas; matchSize(bbox, handle, others) snaps resize dimensions to peer frames and adjusts origin per handle direction.
Smart guides unit tests
packages/slides/test/view/editor/smart-guides.test.ts
Comprehensive test suite covering smartGuides behavior (empty neighbors, equal-spacing middle/end cases, threshold rejection, row-overlap validation, smallest-adjustment selection) and matchSize behavior (all resize handles, origin compensation, peer aggregation, no-match fallback).
Smart guides overlay rendering
packages/slides/src/view/editor/overlay.ts
OverlayOptions.guides type broadened to accept SmartGuide | SnapGuide; renderOverlay dispatches on g.kind to render double-headed arrows with labels for spacing/distance and dashed rectangles for equal-size; new DOM helpers and color constant for smart-guide visuals.
Smart guides editor integration
packages/slides/src/view/editor/editor.ts
Imports smartGuides and matchSize; move-drag computes snap results then smart-guides, re-applies Shift axis-lock, and merges guides for overlay; resize applies matchSize when Shift not held, adjusts live frame, and calls repaintOverlay() on completion; paintLiveScoped and paintMoveGhost parameter types accept mixed guide sources.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • wafflebase/wafflebase#209: Both extend the slides editor's snap-guide and overlay pipeline; #209 introduces SnapGuide rendering during drag, and this PR adds parallel SmartGuide rendering via the same infrastructure.

  • wafflebase/wafflebase#267: Both modify the editor's move-preview/ghost-painting path; #267 introduces ghost preview infrastructure, and this PR extends it to handle SmartGuide in addition to SnapGuide.

  • wafflebase/wafflebase#306: Both refine the slides editor's Shift axis-lock behavior; #306 adds lock-axis helpers, and this PR uses them to re-apply constraints after smart-guides so snaps respect the same Shift modifier.

Poem

🐰 Smart guides align with grace,
Three snapping patterns find their place—
Spacing, distance, matching size,
Arrows point, rectangles advise.
Drag and resize, now smooth and wise! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% 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
Title check ✅ Passed The title 'Slides smart guides (equal-spacing / distance / size)' clearly and concisely describes the main feature addition to the codebase.
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.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/slides-smart-guides

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.

hackerwins and others added 14 commits May 31, 2026 20:13
Spec and bite-sized implementation plan for PowerPoint-style smart
guides during element drag and resize. Detection module pure and
unit-testable; overlay rendering reuses the existing HTML/CSS
guide layer; drag and resize handlers gain a single composed call
after the existing snapDelta / resizeFrameWorld passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
First step toward PowerPoint-style equal-spacing / equal-distance /
equal-size guides during shape drag and resize. This commit lands
just the SmartGuide type and an identity-return smartGuides() so the
later tasks can layer detection on top one pattern at a time.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
When the dragged element sits between two others on the same row or
column, snap the middle gap to balance with the outer one. Same 8 px
band the existing snapDelta uses; per-axis smallest-adjust tie-break
keeps the choice deterministic against multiple qualifying trios.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Extend the equal-spacing pattern so the dragged element snaps when
it sits on either END of a pair, not only between them — matches
PowerPoint behaviour. Same 8 px threshold and smallest-adjust
tie-break as the middle-trio case.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
When the dragged element sits to the side of a neighbour and the
resulting gap matches a gap already formed by two other elements,
snap the dragged element to that distance. Reuses the existing
tryX/tryY selectors — smallest |adjust| wins regardless of kind,
so a precise distance match isn't unseated by a coarser spacing one
in coincidental setups.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Pure resize-side smart-guide entry point: snaps the bbox's width or
height to a peer's when within 8 px, with handle-aware origin
compensation so the anchored corner / edge stays put. Collects every
peer that shares the matched dimension so the overlay can highlight
the full group.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Adds two HTML/CSS overlay primitives next to makeGuide — paired
double-headed arrows for equal-spacing / equal-distance, and 1 px
dashed boxes for equal-size matched peers. Dispatched from
renderOverlay by guide.kind so the existing SnapGuide rendering
stays unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Composes smartGuides() onto the snap-corrected (dx, dy), then merges
the resulting guides with the existing edge / centre / user-guide
set. Shift's axis re-lock continues to have the last word on the
delta. Reuses the otherFrames already collected for snapDelta — no
extra candidate work.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Runs matchSize after resizeFrameWorld inside startResize. Same 8 px
band; handle-aware origin compensation keeps the anchored edge fixed.
Shift (preserve-aspect) intentionally bypasses equal-size matching so
the two modes do not fight.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Final review found that matchSize's corner-handle origin compensation
(nw compensates both axes, ne y-only, sw x-only) was implemented but
not directly tested. Add three tests so the branch logic is locked in
against accidental regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Plan and design doc updated to reflect what shipped: smallest |adjust|
wins across equal-spacing and equal-distance candidates, no priority
between kinds. The original priority rule produced test contradictions
where a precise distance match lost to a coarser spacing match in
coincidental setups.

Also adds the paired lessons file capturing the workspace-dist rebuild
trap, subagent scaffolding pitfall, and follow-up items (N>30 culling,
integration tests, visual diffs) deliberately cut from v1.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The resize onUp handler skipped repaintOverlay, so equal-size dashed
outlines lingered after the mouse released. Mirror the move-drag
onUp pattern. Also documents the matchSize peer-collection semantics
(replacement-on-better-target is intentional, not a dropped-peers bug).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Adds a small white pill with the rounded pixel distance at the
midpoint of every equal-spacing and equal-distance arrow. Matches
Google Slides; PowerPoint omits them. Equal-size dashed outlines
stay unlabeled — the highlighted peer IS the answer to "same as
what."

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Use the slides editor's rotation-angle tooltip styling (dark
translucent pill with white text) for the equal-spacing /
equal-distance distance labels instead of the earlier white-on-red
pill — keeps a single visual language for overlay annotations.

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: 4

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/editor.ts (1)

3317-3329: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Skip the resize commit when the frame never changed.

This path always writes updateElementFrame(...), so a mousedown/mouseup on a resize handle with no actual resize still creates a no-op mutation and can pollute undo history.

Possible fix
     const onUp = () => {
       document.removeEventListener('pointermove', onMove);
       document.removeEventListener('pointerup', onUp);
+      if (
+        live.worldFrame.x === startWorldFrame.x &&
+        live.worldFrame.y === startWorldFrame.y &&
+        live.worldFrame.w === startWorldFrame.w &&
+        live.worldFrame.h === startWorldFrame.h &&
+        live.worldFrame.rotation === startWorldFrame.rotation
+      ) {
+        this.renderer.markDirty();
+        this.render();
+        this.repaintOverlay();
+        return;
+      }
       // Convert world frame back to scope-local before committing.
       const localFrame = fromWorldFrame(live.worldFrame, scope, startSlide);
       this.options.store.batch(() => {
         this.options.store.updateElementFrame(startSlide.id, elementId, localFrame);
       });
🤖 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 3317 - 3329, The onUp
handler currently always calls this.options.store.updateElementFrame(...)
causing no-op mutations; compute the element's original frame (from startSlide
or the saved start frame for elementId) and compare it to the computed
localFrame (fromWorldFrame(live.worldFrame, scope, startSlide)); only call
this.options.store.updateElementFrame(startSlide.id, elementId, localFrame)
inside the batch if the frames differ (use an appropriate deep/frame equality
check), otherwise skip the store update to avoid polluting undo history while
keeping renderer.markDirty(), render(), and repaintOverlay() behavior unchanged.
🤖 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-smart-guides.md`:
- Around line 281-287: Update the risk-table row to reflect the current
tie-break rule by replacing the text that reads "edge > equal-spacing >
equal-distance and smallest `|adjust|` tie-break" with a single clear rule that
the smallest |adjust| wins (and remove the contradictory priority phrasing);
escape the literal pipes so the Markdown table parses (e.g., write the tie-break
as smallest \|adjust\| wins or use HTML entity &`#124`;adjust&`#124`;), and ensure
the row text mentions the strict priority order only if it matches the Priority
section above.

In `@packages/slides/src/view/editor/editor.ts`:
- Around line 2465-2473: The assembled guides array uses pre-lock results
(snapped.guides and smart.guides) but the final movement may have one axis
zeroed by lockAxis; update the assembly so after computing final = ev.shiftKey ?
lockAxis(smart.dx, smart.dy) : smart you filter the combined guides to remove
any guide that reports/depends on movement along an axis that was zeroed (e.g.
drop guides whose axis === 'x' when final.dx === 0, and whose axis === 'y' when
final.dy === 0), then use that filtered list for the overlay instead of the raw
[...snapped.guides, ...smart.guides]; keep references to smartGuides, lockAxis,
snapped.guides, smart.guides, final, dx, dy to locate the change.

In `@packages/slides/src/view/editor/smart-guides.ts`:
- Around line 77-80: The Cand type currently contains a fully materialized guide
that bakes in d.centerXPx/d.centerYPx too early, so when X and Y adjustments are
resolved separately the rendered guides use stale centers; update the candidate
construction to store only the guide template/metadata (not the computed
geometry) and the adjust value, and then rebuild the guide geometry from the
final snapped bounding box (final d.centerXPx and d.centerYPx) when you choose
the winning candidate or render guides; change places that create Cand instances
(references to Cand, the guide property, and code paths that build SmartGuide
instances) to stop precomputing center-based fields and instead compute those
fields from the final centers before rendering or returning the selected guide.
- Around line 95-119: The equal-spacing/distance candidate code currently only
checks overlaps between each non-dragged frame and the dragged frame (using
overlapsRow/overlapsCol) which permits staircase false positives; add an
additional check that the two candidate frames overlap each other on the
perpendicular axis (i.e., call overlapsRow(a, b) in the X-axis branch and
overlapsCol(a, b) in the Y-axis branch) before creating the guide (the tryX/tryY
calls that build guides with kind 'equal-spacing' or 'equal-distance'); apply
this same extra overlap check in the other analogous loops that use
overlapsRow(d, ...) and overlapsCol(d, ...) and add a regression test with a
staggered/stair-step layout to ensure no guides are produced for non-overlapping
pairs.

---

Outside diff comments:
In `@packages/slides/src/view/editor/editor.ts`:
- Around line 3317-3329: The onUp handler currently always calls
this.options.store.updateElementFrame(...) causing no-op mutations; compute the
element's original frame (from startSlide or the saved start frame for
elementId) and compare it to the computed localFrame
(fromWorldFrame(live.worldFrame, scope, startSlide)); only call
this.options.store.updateElementFrame(startSlide.id, elementId, localFrame)
inside the batch if the frames differ (use an appropriate deep/frame equality
check), otherwise skip the store update to avoid polluting undo history while
keeping renderer.markDirty(), render(), and repaintOverlay() behavior unchanged.
🪄 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: 00a58db3-480b-4376-8840-caa017a1c9da

📥 Commits

Reviewing files that changed from the base of the PR and between 15f84a8 and 59b842b.

📒 Files selected for processing (9)
  • docs/design/README.md
  • docs/design/slides/slides-smart-guides.md
  • docs/tasks/README.md
  • docs/tasks/active/20260531-slides-smart-guides-lessons.md
  • docs/tasks/active/20260531-slides-smart-guides-todo.md
  • packages/slides/src/view/editor/editor.ts
  • packages/slides/src/view/editor/overlay.ts
  • packages/slides/src/view/editor/smart-guides.ts
  • packages/slides/test/view/editor/smart-guides.test.ts

Comment thread docs/design/slides/slides-smart-guides.md
Comment thread packages/slides/src/view/editor/editor.ts Outdated
Comment thread packages/slides/src/view/editor/smart-guides.ts
Comment thread packages/slides/src/view/editor/smart-guides.ts
@hackerwins
hackerwins force-pushed the feat/slides-smart-guides branch from 59b842b to beea0a5 Compare May 31, 2026 11:16
@github-actions

github-actions Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 259.0s

Lane Status Duration
tokens:build ✅ pass 2.2s
sheets:build ✅ pass 13.7s
docs:build ✅ pass 12.6s
slides:build ✅ pass 14.7s
verify:fast ✅ pass 172.2s
frontend:build ✅ pass 19.0s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 5.1s
cli:build ✅ pass 2.1s
verify:entropy ✅ pass 17.1s

Verification: verify:integration

Result: ✅ PASS

@codecov

codecov Bot commented May 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@hackerwins
hackerwins merged commit 0d5034b into main May 31, 2026
1 check passed
@hackerwins
hackerwins deleted the feat/slides-smart-guides branch May 31, 2026 11:50
hackerwins added a commit that referenced this pull request May 31, 2026
PRs #257/#301/#303/#304/#305/#306/#307/#309/#315/#316/#317/#321/#322
shipped between v0.4.2 and the v0.4.3 cut, leaving their paired todo
files in active with the process checkboxes still open. Flip the
remaining boxes for the work that landed, then move the paired
todo/lessons into docs/tasks/archive/2026/05/ via pnpm tasks:archive.
Active count drops from 17 to 6; archive count grows by 21 files.

Patch release covering 29 commits since v0.4.2. Headline theme is
Slides editing & UX polish toward Google Slides / PowerPoint parity:
smart guides (equal-spacing / distance / size), Shift drag modifiers,
text inside shapes, a Format options right panel, tier-1 universal
toolbar controls, text autofit (shrink + grow), group-selection
visuals, and several smaller text-box / selection fixes. PPTX import
gained paragraph-level bullet styles, body anchor (vertical
alignment), and stretched blipFill cover-crop.

Docs added a pending-inline-style at collapsed caret, a caret that
tracks the resolved text color, bullet indent on Tab / Shift+Tab,
font / size / line-spacing / clear formatting toolbar controls, an
inline-style-attribute clear that actually lands in the CRDT, and a
polished toolbar trigger / color swatch pass shared across docs /
sheets / slides. Sheets gained .xlsx import.

Infrastructure: Docker images now build on native arm64 runners,
fixing the release hang seen on v0.4.2. No Prisma migration, no new
backend env vars — the devops bump is a routine image-tag change.

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