Skip to content

Make slides shape insertion feel like a real tool#239

Merged
hackerwins merged 3 commits into
mainfrom
slides-click-insert-cursor
May 14, 2026
Merged

Make slides shape insertion feel like a real tool#239
hackerwins merged 3 commits into
mainfrom
slides-click-insert-cursor

Conversation

@hackerwins

@hackerwins hackerwins commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

The slides editor's "insert a shape" flow gets four coupled UX improvements so insert mode feels like a coherent tool instead of a toolbar checkbox:

  • Per-kind default click-insert size — picking a shape and clicking on the canvas (without dragging) now drops the shape at a sensible size chosen per category (lines 400×0, basic 320×200, square shapes 200×200, stars 240×240, horizontal arrows 320×160, vertical arrows 160×320, banners 480×140, action buttons 140×140, etc.). Previously every click produced a fixed 200×100 frame regardless of kind, which crushed action buttons and stretched rectangles.
  • Crosshair cursor during insert modesetInsertMode(kind) toggles cursor: 'crosshair' on the slide canvas + overlay; disarm restores to ''. Matches Google Slides and PowerPoint — without it the user has no signal that the next click will place a shape.
  • Hover ghost + drag ghost — while insert is armed and the pointer is over the canvas, a semi-transparent (alpha 0.4) preview of the to-be-inserted shape follows the cursor. The drag-to-size live preview uses the same alpha channel (previously full opacity), so the in-flight shape stays translucent until commit on mouseup. Lets the user see the kind, size, and position before committing.
  • Escape disarms insert mode (and cancels in-flight drags) — a new ESC key rule disarms when armed; during an active drag-to-size, a capture-phase ESC listener aborts the drag without writing anything to the store.

Drive-by fixes surfaced during browser smoke

  • Toggle pressed visual was broken when wrapped in <TooltipTrigger asChild> — Tooltip's data-state="closed"/"open" cloned onto the trigger element clobbered Toggle's data-state="on", so data-[state=on]:bg-accent never matched. Now also matches aria-pressed=true (set reliably by Radix Toggle regardless of Tooltip wrapping). Affects Select / Text toggle buttons in the slides toolbar.
  • Shape picker dropdown didn't auto-close on selection — entries are plain <button>s, not DropdownMenuItems. The picker now controls its own open state.

Known limitations / non-blocking

  • ESC inside an open context menu / Radix popover also disarms insert mode if both are layered. Acceptable today ("ESC dismisses the topmost layer"); if it ever feels wrong, the ESC rule can guard on [data-state="open"][role="menu"].
  • The drag-cancel cancelled flag is a closure-local boolean shared between two listeners. Pattern works but is fragile if a future exit path is added without going through cleanup().

Test plan

  • pnpm verify:fast (lint + 764 frontend / 710 slides tests) green
  • Pre-push verify:self (all builds + entropy) green
  • Browser smoke (Puppeteer against a logged-in dev session):
    • Cursor toggles to crosshair on every shape arm; resets to '' on Select / ESC.
    • Click without drag inserts the per-kind size for 8 representative shapes (rect 320×200, line 400×0, right-arrow 320×160, up-arrow 160×320, star5 240×240, ribbon 480×140, home action button 140×140, plus 200×200).
    • Hover ghost paints at [74, 92, 121] (accent1 × 0.4) over [32, 33, 36] slide bg; clears on mouseleave, ESC, Select, and on shape-kind switch.
    • Drag-to-size shows the same alpha pixel mid-drag; ESC mid-drag returns canvas to bg color and commits zero elements.
    • Select / Text toolbar toggles now show the pressed accent background when active.
    • Shape picker closes on selection.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Insert-mode hover preview for shapes with semi-transparent display
    • Crosshair cursor display when insert mode is active
    • Per-shape default sizing for quick click-inserts
    • ESC key support to cancel insert mode and in-progress drags
  • Bug Fixes

    • Shape picker dropdown now auto-closes after selection
    • Fixed toggle visual "pressed" state when inside a Tooltip

Review Change Stack

Click-anchored default-size insert per kind, crosshair cursor while
insert is armed, hover + drag ghost previews, and Escape to disarm
(or cancel an in-flight drag). Together these give the user feedback
at every step — they can see what they are about to insert, where it
will land, and that the editor is in a special mode they can back out
of — instead of relying on the toolbar button alone.

Drag preview now flows through the same forceRender(slide, doc, ghost)
channel as the hover ghost, so the in-flight shape stays semi-
transparent until commit on mouseup. ESC during a drag aborts without
writing anything to the store.

Drive-by fixes surfaced during browser smoke: Toggle pressed visual
broke when wrapped in TooltipTrigger asChild (Tooltip's data-state
clobbered Toggle's data-state=on); now also matches on aria-pressed.
Shape picker dropdown didn't auto-close on shape select since its
entries are plain buttons, not DropdownMenuItem; the picker now
controls its own open state and closes on select.
Review surfaced two real cleanup gaps in the hover-ghost path:

  - detach() left a pending rAF in flight, painting into a torn-down
    canvas. Now cancels and clears hoverPreview.
  - setInsertMode swapping shape kind A → B kept the old kind in
    hoverPreview until the next mousemove, so a queued rAF could
    briefly paint a kind-A ghost after the user picked kind B. Now
    clears on every insert-mode change; only repaints when leaving
    a ghost-eligible mode to avoid flashing between shape kinds.

Also adds the two missing test paths flagged in review: ESC mid-drag
abort (no element committed, mode disarmed) and disarm-while-hovered
(cleanup path is safe, detach() doesn't throw).

Minor: Size interface in insert.ts was exported but never imported
anywhere — narrowed to module-local.
@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (8)
  • packages/frontend/tests/visual/baselines/harness-visual.browser.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.png is excluded by !**/*.png

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7eabf4ae-f2ec-45f2-a0cf-30fe56780719

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements shape-insert UX improvements for the slides editor: click-triggered inserts with per-kind default sizing, hover and drag ghost previews, cursor feedback (crosshair during insert mode), and Escape-key disarm/abort behavior. It also fixes related UI regressions in shape picker and toggle styling.

Changes

Slides Editor Insert Mode UX

Layer / File(s) Summary
Insert sizing model and click-vs-drag logic
packages/slides/src/view/editor/interactions/insert.ts, packages/slides/src/view/editor/interactions/insert.test.ts, packages/slides/src/view/editor/editor.ts
Introduces defaultInsertSize() backed by DEFAULT_INSERT_SIZE map and CLICK_THRESHOLD_PX_SQ constant. buildInsertElement now branches on pointer movement: small movement (click) uses per-kind defaults anchored at start; larger movement (drag) uses the drag rectangle. insertAt now passes zero-size endpoints to trigger this sizing. Tests verify per-kind frame sizes and threshold behavior.
Canvas ghost element rendering
packages/slides/src/view/canvas/slide-renderer.ts
Exports GHOST_ALPHA = 0.4 and extends SlideRenderer.forceRender() and drawSlide() to accept optional ghost?: Element, rendered on top of the slide with transparency for preview overlays.
Insert mode lifecycle, keyboard context, and cursor management
packages/slides/src/view/editor/editor.ts, packages/slides/src/view/editor/interactions/keyboard.ts
Adds hoverPreview state tracking. Extends KeyboardContext with getInsertMode()/setInsertMode(kind). setInsertMode toggles canvas and overlay cursors (crosshair when armed, default when disarmed), clears hover ghost state, and forces immediate repaint when leaving ghost-eligible modes. detach() cleanup cancels pending hover renders.
Hover ghost preview rendering
packages/slides/src/view/editor/editor.ts
Registers mousemove/mouseleave listeners on canvas to drive hover preview during shape insert modes. Coalesces updates via rAF into paintWithHoverGhost() which renders a translucent shape preview. Tests verify cursor styling changes.
Drag-to-size insert with ghost preview
packages/slides/src/view/editor/editor.ts
Introduces insertDragging flag and refactors drag-to-size flow: suppresses hover ghost, renders semi-transparent preview via forceRender(..., ghost) on each mousemove, commits finalized buildInsertElement() result on mouseup. Removes old synthetic-slide preview approach. Tests cover ESC abort (no commit) and hover-disarm cleanup.
Escape-key disarm and drag abort
packages/slides/src/view/editor/editor.ts, packages/slides/src/view/editor/interactions/keyboard.ts
Adds keyboard rule: Escape disarms active insert mode (prevents default). During drag, a capturing keydown listener aborts in-flight insert, cancels rendering, disarms insert mode, and repaints, avoiding double-disarm from keyrules.
Frontend shape picker and toggle UI fixes
packages/frontend/src/app/slides/shape-picker.tsx, packages/frontend/src/components/ui/toggle.tsx
ShapePicker now uses controlled DropdownMenu open state, closing immediately after selection. toggle.tsx clarifies that pressed styling must rely on aria-pressed as well as data-state=on, since TooltipTrigger asChild can overwrite the latter.
Task documentation
docs/tasks/active/20260514-slides-click-insert-cursor-todo.md
Records the insert UX work including goals, sizing table, implementation references, test/verification results, follow-up hover and drag ghost behavior, Escape handling details, and related toolbar regressions fixed (toggle styling and dropdown auto-close).

Sequence Diagram

sequenceDiagram
  participant User
  participant Editor
  participant Canvas as Canvas/Ghost
  participant Renderer as SlideRenderer
  participant Insert as insert.ts
  
  User->>Editor: Click shape button (arm insert mode)
  Editor->>Renderer: setInsertMode('rect')<br/>cursor=crosshair
  
  User->>Canvas: Hover over canvas
  Canvas->>Editor: mousemove
  Editor->>Renderer: schedule hover preview via rAF
  Renderer->>Canvas: forceRender(slide, doc, ghost)<br/>with ghost alpha=0.4
  
  User->>Canvas: Drag to size
  Canvas->>Editor: mousemove (drag)
  Editor->>Insert: buildInsertElement with start/end
  Insert->>Insert: compute dx, dy
  Insert->>Renderer: renders ghost preview per frame
  
  User->>Canvas: Release (mouseup)
  Editor->>Insert: buildInsertElement (finalized)
  Insert->>Editor: returns committed Element
  Editor->>Editor: setInsertMode(null)<br/>cursor=default
  
  User->>Canvas: Press Escape
  Canvas->>Editor: keydown (capture)
  Editor->>Editor: abort drag, clear ghost
  Editor->>Editor: setInsertMode(null)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • wafflebase/wafflebase#238: Both PRs update Escape-key handling in keyboard.ts, with PR #239 adding insert-mode disarm and PR #238 addressing escape selection/shortcuts.
  • wafflebase/wafflebase#190: PR #239 directly extends the insert-mode interactions foundation implemented in PR #190 (buildInsertElement, keyboard context, etc.).

Poem

🐇 A click inserts a shape so small,
And hovering ghosts preview it all,
Drag to resize with alpha's gleam,
Escape aborts the dragging dream—
Insert mode shines with crosshair's art! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements slides shape insertion UX improvements but the linked issue #39 requires migrating API URL to api.wafflebase.io, which is completely unrelated to the PR changes. This PR should not be linked to issue #39. Either relink to the correct shape insertion issue or verify the linked issues are accurate.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Make slides shape insertion feel like a real tool" clearly summarizes the main objective of adding UX polish to the shape insertion workflow.
Out of Scope Changes check ✅ Passed The PR contains changes focused on shape insertion UX (cursors, hover previews, defaults, ESC handling) and incidental fixes to Toggle/Tooltip and shape picker, which align with the stated PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ 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 slides-click-insert-cursor

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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 14, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 191.0s

Lane Status Duration
sheets:build ✅ pass 13.1s
docs:build ✅ pass 12.5s
slides:build ✅ pass 10.6s
verify:fast ✅ pass 108.6s
frontend:build ✅ pass 19.6s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.9s
cli:build ✅ pass 2.1s
verify:entropy ✅ pass 19.2s

Verification: verify:integration

Result: ✅ PASS

@codecov

codecov Bot commented May 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/slides/src/view/editor/editor.ts (1)

894-952: 💤 Low value

Note: Closure-local cancelled flag relies on explicit cleanup calls.

The cancelled flag (line 904) and the cleanup() helper (lines 911-916) correctly handle the current exit paths (mouseup, ESC). However, if future code adds new exit paths (e.g., visibility change, window blur), those paths must also call cleanup() to avoid leaking listeners and correctly reset insertDragging. This is noted as a known limitation in the PR objectives.

Current implementation is correct; this is a maintainability note for future changes.

🤖 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 894 - 952, The
drag-to-size flow relies on the closure-local cancelled flag and the cleanup()
helper to remove listeners and reset this.insertDragging; ensure any future new
exit paths (e.g., visibilitychange, blur, beforeunload, or programmatic aborts)
call cleanup() and, if they can cancel the insert, set cancelled = true before
calling cleanup(); reference the drag handlers and helpers: cancelled,
cleanup(), onMove, onUp, onKey, this.insertDragging, and any new abort handler
so listeners aren’t leaked and insert mode is properly reset.
🤖 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.

Nitpick comments:
In `@packages/slides/src/view/editor/editor.ts`:
- Around line 894-952: The drag-to-size flow relies on the closure-local
cancelled flag and the cleanup() helper to remove listeners and reset
this.insertDragging; ensure any future new exit paths (e.g., visibilitychange,
blur, beforeunload, or programmatic aborts) call cleanup() and, if they can
cancel the insert, set cancelled = true before calling cleanup(); reference the
drag handlers and helpers: cancelled, cleanup(), onMove, onUp, onKey,
this.insertDragging, and any new abort handler so listeners aren’t leaked and
insert mode is properly reset.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5605d8aa-b03f-43e9-9f10-7c12b87d3930

📥 Commits

Reviewing files that changed from the base of the PR and between 2c319e5 and fbaa67c.

📒 Files selected for processing (9)
  • docs/tasks/active/20260514-slides-click-insert-cursor-todo.md
  • packages/frontend/src/app/slides/shape-picker.tsx
  • packages/frontend/src/components/ui/toggle.tsx
  • packages/slides/src/view/canvas/slide-renderer.ts
  • packages/slides/src/view/editor/editor.test.ts
  • packages/slides/src/view/editor/editor.ts
  • packages/slides/src/view/editor/interactions/insert.test.ts
  • packages/slides/src/view/editor/interactions/insert.ts
  • packages/slides/src/view/editor/interactions/keyboard.ts

The Toggle aria-pressed CSS fix restores the pressed visual on the
Select / Text toggle buttons, which the previous baselines captured
in their broken (unhighlighted) state. CI's verify-browser job
flagged the 8 affected snapshots (light/dark × desktop/mobile ×
full-harness/slides-toolbar). Regenerated through the canonical
docker path so the host environment can't drift them.
@hackerwins
hackerwins merged commit 940fbbf into main May 14, 2026
4 checks passed
@hackerwins
hackerwins deleted the slides-click-insert-cursor branch May 14, 2026 14:17
hackerwins added a commit that referenced this pull request May 14, 2026
Followup to #239: move the task doc into archive/2026/05 and refresh
the active/archive index files.
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