Skip to content

Slides: variable pasteboard so off-slide shapes stay selectable#353

Merged
hackerwins merged 4 commits into
mainfrom
slides-pasteboard
Jun 10, 2026
Merged

Slides: variable pasteboard so off-slide shapes stay selectable#353
hackerwins merged 4 commits into
mainfrom
slides-pasteboard

Conversation

@hackerwins

@hackerwins hackerwins commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Off-slide shapes were unreachable: the slide canvas DOM was sized exactly to the slide rect, so anything dragged outside [0..SLIDE_WIDTH] × [0..SLIDE_HEIGHT] was both clipped from rendering AND outside the canvas's pointer-event box. Dragging a shape off the slide effectively lost it.
  • Grow the canvas / canvasWrap to max(slide, scrollHost) so the empty area inside scrollHost becomes a pasteboard. The slide keeps its original Fit-zoom size; off-slide shapes now paint onto the same canvas the slide does and pointer events on the surrounding band reach hit-test.
  • Slide elevation (1-px theme-aware hairline + soft drop shadow) is owned by a new transparent slideElevation absolute div pinned to the slide rect — survives every paint mode (no-pasteboard, mobile, presenter, pre-first-refit) and stays theme-reactive via --foreground.

Design

docs/design/slides/slides-pasteboard.md — DOM shape, renderer / editor / view contract, known v1 limit (no pasteboard at zoom > Fit).

Test plan

  • pnpm verify:fast green (904 passed)
  • Smoke (desktop, Fit zoom): drag a shape past the slide edge, release in the pasteboard band, click to reselect, drag it back onto the slide
  • Smoke (desktop, zoom > Fit): confirm slide still has hairline + drop shadow against both light and dark workspace backgrounds; documented limit that off-slide shapes are clipped at this zoom level
  • Smoke (mobile): unchanged behavior (no pasteboard)
  • Smoke (presenter / share-link viewer): unchanged behavior

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Slides canvas now supports an expanded pasteboard area around the slide, allowing shapes extending beyond the slide boundary to remain visible and selectable.
  • Documentation

    • Added design documentation and implementation task tracking for the slides pasteboard feature.

hackerwins and others added 2 commits June 10, 2026 21:43
Today the slide canvas DOM is sized exactly to the slide rect, so any
shape that lands outside [0..SLIDE_WIDTH] x [0..SLIDE_HEIGHT] is both
clipped from rendering AND unreachable for pointer events. Dragging a
shape off the slide effectively loses it: nothing paints into the
pasteboard band and no click can reach the geometry that's already
outside the canvas DOM box.

Treat the empty area inside scrollHost that surrounds the slide rect
as a pasteboard. The slide keeps its original Fit-zoom size; the
canvas / canvasWrap grow to max(slide, scrollHost), and the slide is
centered inside. Off-slide elements now paint onto the same canvas
the slide does and pointer events on the surrounding band reach
hit-test, so the user can grab them again.

  - SlideRenderer.drawSlide takes slideOffsetLogicalX/Y; when non-zero
    it translates world (0, 0) into the bigger bitmap and paints the
    slide background + drop shadow only in the slide rect.
  - Editor gets a setSlideOffset(x, y) entry point next to
    setHostSize; clientToLogical subtracts both offsets.
  - slides-view.tsx's refitCanvas reads scrollHost dimensions to
    derive canvas/offset on every tick.
  - canvasWrap background is transparent so the band blends into
    the surrounding workspace; the slide's drop shadow carries the
    visual edge.

Known v1 limit: at zoom > Fit the slide overflows scrollHost, canvas
equals slide, and there's no surrounding pasteboard. User drops to
Fit to recover off-slide shapes. Documented in the design doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Self-review of the pasteboard commit surfaced four issues:

1. The slide drop shadow only painted inside drawSlide's
   `hasPasteboard` branch, so any no-pasteboard call (zoom > Fit,
   mobile, presenter, or the first frame before refit) showed no
   shadow at all. The old CSS box-shadow on the canvas was already
   deleted, so the regression compounded.

2. The 1-px theme-aware hairline (`color-mix(in srgb, --foreground 25%,
   transparent)`) was dropped entirely. The original comment marked
   it as load-bearing in dark mode + Simple Dark, where the dark
   slide on a near-black workspace had no other visible edge.

3. `(canvasFullW - hostW) / 2` could land on 0.5 CSS px when the
   parity of canvas-fit and slide-fit differed. The fractional
   value sub-pixel-positioned the overlay (AA-blurring handles) and
   risked 1-px drift between canvas paint and absolute children.

4. Two test mocks at packages/frontend/tests/app/slides/toolbar/
   cast their fake editor as `as unknown as SlidesEditor`, hiding
   the missing `setSlideOffset` method — a latent runtime crash if
   any code path eventually calls it on the mock.

Fixes:

- Add a transparent `slideElevation` absolute div pinned to the
  slide rect with the original CSS box-shadow (hairline + drop
  shadow). The shadow extends into the pasteboard band; the canvas
  above is opaque inside the slide rect and transparent outside, so
  the ring renders exactly at the slide edge. Elevation now lives
  in CSS — present in every paint mode, theme-reactive, and
  constant in CSS-px size regardless of zoom.
- Remove the canvas-painted shadow from drawSlide.
- Math.floor the slide offset so overlay + elevation div sit on
  integer CSS pixels.
- Add `setSlideOffset() {}` to the two toolbar test mocks.

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

coderabbitai Bot commented Jun 10, 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 36 minutes and 8 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: 222f1ffb-c7ba-4ec5-b9e1-cdb2a53d3153

📥 Commits

Reviewing files that changed from the base of the PR and between 80fd5df and 4830e50.

📒 Files selected for processing (10)
  • docs/design/README.md
  • docs/design/slides/slides-pasteboard.md
  • docs/tasks/README.md
  • docs/tasks/archive/2026/06/20260608-slides-dark-theme-on-create-lessons.md
  • docs/tasks/archive/2026/06/20260608-slides-dark-theme-on-create-todo.md
  • docs/tasks/archive/2026/06/20260610-slides-pasteboard-lessons.md
  • docs/tasks/archive/2026/06/20260610-slides-pasteboard-todo.md
  • docs/tasks/archive/README.md
  • packages/frontend/src/app/slides/slides-view.tsx
  • packages/slides/test/view/editor/editor.test.ts
📝 Walkthrough

Walkthrough

This PR implements Slides Pasteboard v1, enabling shapes outside the slide canvas to remain visible and selectable. The feature expands the canvas beyond the slide rect, introduces offset tracking through the renderer and editor, and moves slide styling (hairline, shadow) to a separate DOM element while making canvas background transparent for pasteboard visibility.

Changes

Slides Pasteboard v1

Layer / File(s) Summary
Design Documentation and Task Tracking
docs/design/README.md, docs/design/slides/slides-pasteboard.md, docs/tasks/active/20260610-slides-pasteboard-todo.md
Design spec and task tracking document the variable pasteboard approach, coordinate/DOM/renderer behavior changes, implementation tasks, and known risks around canvas memory and shadow rendering differences.
Renderer Offset and Pasteboard Drawing
packages/slides/src/view/canvas/slide-renderer.ts
SlideRendererOptions gains optional slideOffsetLogicalX and slideOffsetLogicalY fields. drawSlide computes hasPasteboard flag and branches: fills entire bitmap with slide background when no offset, or clears to transparent and translates context by offsets when pasteboard is enabled, filling only the slide rect with padding.
Editor Slide Offset API and Coordinate Mapping
packages/slides/src/view/editor/editor.ts
SlidesEditor interface adds setSlideOffset(logicalX, logicalY) method. SlidesEditorImpl stores offsets, invalidates renderer, and repaints. clientToLogical now subtracts slide offsets so logical origin stays aligned to slide top-left within larger offset canvas.
View Canvas Setup, Elevation, and Offset Propagation
packages/frontend/src/app/slides/slides-view.tsx
Canvas wrapper setup introduces full canvas dimensions (canvasFullW/H), computed CSS offsets (slideOffsetCssX/Y), separate slideElevation div for hairline/shadow, and transparent canvas background. Editor initialization seeds offset values. refitCanvas expands canvasWrap to scrollHost bounds, computes centered offsets, repositions elevation/overlay/rulers, and synchronizes editor via setHostSize and setSlideOffset with logical coordinate conversion.
Test Mock Updates
packages/frontend/tests/app/slides/toolbar/state.test.ts, packages/frontend/tests/app/slides/toolbar/text-edit-section.test.ts
Test mocks add setSlideOffset() stub to SlidesEditor mock surface to complete interface contracts.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A pasteboard unfolds, so grand and so wide,
Where shapes dance off-slide but still glorified,
With offsets that flow through renderer and view,
The canvas expands—let your artistry brew!
Hairlines and shadows in CSS shine bright,
Off-slide content now claims the spotlight! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 title clearly summarizes the main change: introducing a variable pasteboard region that keeps off-slide shapes selectable, which is the core objective across all modified files.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slides-pasteboard

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 Jun 10, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 264.4s

Lane Status Duration
tokens:build ✅ pass 2.2s
sheets:build ✅ pass 14.6s
docs:build ✅ pass 13.1s
slides:build ✅ pass 15.6s
verify:fast ✅ pass 171.4s
frontend:build ✅ pass 19.8s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 5.2s
cli:build ✅ pass 2.2s
verify:entropy ✅ pass 20.1s

Verification: verify:integration

Result: ✅ PASS

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

- Bump `target-version` of docs/design/slides/slides-pasteboard.md
  from 0.2.0 to 0.4.5 to match the shipping release.
- Mark the task todo as `status: done`, record the smoke + review
  loop, link PR #353.
- Add the paired `*-lessons.md` capturing what the three iterations
  taught (separate reachability from rendering; keep theme-reactive
  styling in CSS; snap centering offsets to integer CSS px).
- pnpm tasks:archive — moves the pasteboard pair into
  docs/tasks/archive/2026/06/ and regenerates the task index.

The archive run also picked up an unrelated completed task
(20260608-slides-dark-theme-on-create) that had been left in active;
the rename is included so the index stays consistent.

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

🧹 Nitpick comments (3)
docs/design/slides/slides-pasteboard.md (2)

26-26: 💤 Low value

Minor style: "empty space" is redundant.

The phrase "empty space" can be simplified to "space" without loss of meaning.

📝 Optional simplification
-to fill `scrollHost`. Whatever empty space sits between the slide
+to fill `scrollHost`. Whatever space sits between the slide
🤖 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 `@docs/design/slides/slides-pasteboard.md` at line 26, Replace the phrase
"empty space" with "space" in the sentence containing "to fill `scrollHost`.
Whatever empty space sits between the slide" inside slides-pasteboard.md; update
that fragment so it reads "...to fill `scrollHost`. Whatever space sits between
the slide" to remove the redundancy.

67-67: 💤 Low value

Add language specifiers to fenced code blocks.

The markdown linter recommends adding language specifiers to fenced code blocks for better rendering and syntax highlighting. Consider adding text or plaintext for the diagram blocks.

Also applies to: 133-133

🤖 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 `@docs/design/slides/slides-pasteboard.md` at line 67, Replace the bare fenced
code blocks (the triple-backtick blocks used for diagrams) with
language-specified fences so the linter can apply proper rendering; for each
diagram block that currently uses ``` change it to a fenced block with a
language specifier such as ```text or ```plaintext so the diagram content is
annotated and will get syntax highlighting and satisfy the markdown linter.
packages/slides/src/view/editor/editor.ts (1)

1248-1260: ⚡ Quick win

Add a regression test for the slide-offset coordinate contract.

setSlideOffset(...) and clientToLogical(...) are now the core mapping that keeps off-slide hit-testing aligned with the translated renderer. A small editor test that exercises a non-zero offset would protect selection/drag flows from subtle math regressions. As per coding guidelines, "Write tests for critical business logic and edge cases".

Also applies to: 4553-4565

🤖 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 1248 - 1260, Add a
regression test that verifies the slide-offset coordinate contract by exercising
a non-zero offset and ensuring mappings and hit-testing remain aligned: in the
editor unit tests create an Editor instance (or the test harness used by
existing editor tests), call setSlideOffset(x, y) with non-zero values, then
invoke clientToLogical(clientX, clientY) and assert the returned logical
coordinates equal the expected values given the offset; also exercise an
off-slide hit-test or a simple selection/drag scenario (e.g., hitTest or the
selection logic used in tests) to confirm hit results respect the offset.
Reference the setSlideOffset and clientToLogical methods (and any off-slide
hit-testing helper like hitTest or selection/drag handlers) so the test guards
against regressions in the mapping math.

Source: Coding guidelines

🤖 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/README.md`:
- Line 76: The README entry for slides-pasteboard.md incorrectly calls the
design "fixed margin"; update that table row to describe the implemented
"variable pasteboard" approach (reference slides-pasteboard.md) by replacing
"fixed margin around the slide" with wording like "variable pasteboard — sized
from the surrounding empty area so off-slide shapes remain visible,
hit-testable, selectable; renderer translates origin, overlay stays at slide
rect, ruler/thumb unchanged" and ensure the wording aligns with the task doc
20260610-slides-pasteboard-todo.md which documents the move away from
fixed-margin behavior.

In `@docs/design/slides/slides-pasteboard.md`:
- Around line 156-157: The sentence claiming "the shadow is painted into the
canvas" is incorrect; update the sentence to state that the shadow is not
painted into the canvas but is applied as a CSS box-shadow on the slideElevation
element. Change the text referencing canvas.style.boxShadow to instead reference
slideElevation's CSS box-shadow behavior and, if helpful, cite the related
implementation in slides-view.tsx (see drawSlide and the comment "Painting the
elevation in CSS — rather than as canvas paint inside `drawSlide`") to ensure
consistency with lines 113–115 and the slideElevation usage.

In `@docs/tasks/active/20260610-slides-pasteboard-todo.md`:
- Around line 78-80: The risk statement is incorrect: the drop shadow was not
moved into the canvas but remains as a CSS box-shadow applied to the
slideElevation element; update the doc text to state that the drop shadow and
hairline are NOT painted into the canvas and instead live as a CSS box-shadow on
slideElevation (see slides-view.tsx and the slideElevation element) and adjust
the sentence that currently reads "Drop shadow now painted into canvas (was CSS
`box-shadow`)" to reflect this correct behavior.

In `@packages/frontend/src/app/slides/slides-view.tsx`:
- Around line 707-710: The early return when sameSlide and sameCanvas skips
updating the ruler origin; before returning, explicitly resync the ruler scroll
using the existing routine that updates ruler origin based on
scrollLeft/scrollTop (i.e., call the function you already use to update
rulers/scroll origin) so the ruler state is corrected even when hostW/hostH and
canvasFullW/canvasFullH are unchanged; apply the same change to the other
identical check later (the block around the second sameSlide/sameCanvas check).
- Around line 697-706: The pasteboard growth logic currently expands
nextCanvasW/nextCanvasH whenever either axis is smaller than the viewport, which
allows one-axis-only pasteboard at absolute zoom; change the calculation so you
only grow the canvas to the scrollRect size when the view is in Fit zoom (e.g.
guard the Math.max calls by the Fit mode check — use your app's Fit detector
such as isFitZoom() or zoomMode === 'Fit'); compute nextCanvasW = isFit ?
Math.max(nextW, Math.floor(scrollRect.width)) : nextW and likewise for
nextCanvasH, and keep nextOffsetX/nextOffsetY derived from those guarded canvas
sizes so offsets remain correct.

---

Nitpick comments:
In `@docs/design/slides/slides-pasteboard.md`:
- Line 26: Replace the phrase "empty space" with "space" in the sentence
containing "to fill `scrollHost`. Whatever empty space sits between the slide"
inside slides-pasteboard.md; update that fragment so it reads "...to fill
`scrollHost`. Whatever space sits between the slide" to remove the redundancy.
- Line 67: Replace the bare fenced code blocks (the triple-backtick blocks used
for diagrams) with language-specified fences so the linter can apply proper
rendering; for each diagram block that currently uses ``` change it to a fenced
block with a language specifier such as ```text or ```plaintext so the diagram
content is annotated and will get syntax highlighting and satisfy the markdown
linter.

In `@packages/slides/src/view/editor/editor.ts`:
- Around line 1248-1260: Add a regression test that verifies the slide-offset
coordinate contract by exercising a non-zero offset and ensuring mappings and
hit-testing remain aligned: in the editor unit tests create an Editor instance
(or the test harness used by existing editor tests), call setSlideOffset(x, y)
with non-zero values, then invoke clientToLogical(clientX, clientY) and assert
the returned logical coordinates equal the expected values given the offset;
also exercise an off-slide hit-test or a simple selection/drag scenario (e.g.,
hitTest or the selection logic used in tests) to confirm hit results respect the
offset. Reference the setSlideOffset and clientToLogical methods (and any
off-slide hit-testing helper like hitTest or selection/drag handlers) so the
test guards against regressions in the mapping math.
🪄 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: 337220ef-094a-4a33-9988-9516159da746

📥 Commits

Reviewing files that changed from the base of the PR and between cc65361 and 80fd5df.

📒 Files selected for processing (8)
  • docs/design/README.md
  • docs/design/slides/slides-pasteboard.md
  • docs/tasks/active/20260610-slides-pasteboard-todo.md
  • packages/frontend/src/app/slides/slides-view.tsx
  • packages/frontend/tests/app/slides/toolbar/state.test.ts
  • packages/frontend/tests/app/slides/toolbar/text-edit-section.test.ts
  • packages/slides/src/view/canvas/slide-renderer.ts
  • packages/slides/src/view/editor/editor.ts

Comment thread docs/design/README.md Outdated
Comment thread docs/design/slides/slides-pasteboard.md Outdated
Comment thread docs/tasks/active/20260610-slides-pasteboard-todo.md Outdated
Comment thread packages/frontend/src/app/slides/slides-view.tsx
Comment thread packages/frontend/src/app/slides/slides-view.tsx
CodeRabbit review surfaced four real defects and asked for a focused
regression test on the new coord contract.

- docs/design/README.md described the design as a "fixed margin"
  approach. That was the rejected first iteration; replace with the
  shipped variable-pasteboard wording.
- docs/design/slides/slides-pasteboard.md and the archived task todo
  both claimed the drop shadow moved into the canvas. It did briefly,
  but the review-fix commit had already migrated it back to a CSS
  box-shadow on the slideElevation div. Realign the doc text.
- The contract clause "no pasteboard above Fit" was over-restrictive;
  the implementation gives an asymmetric band whenever the slide
  fits inside scrollHost on a given axis. Restate the non-goal to
  match.
- refitCanvas ran editor.setRulerScroll only after the early-out, so
  a resize that did not change canvas/slide dims (devtools toggle,
  sidebar collapse, notes-pane drag) could leave the ruler origin
  stale: the browser silently clamps scrollLeft / scrollTop on
  layout changes without dispatching a scroll event. Move the
  resync ahead of the early return.

Add two regression tests in editor.test.ts:

- slideOffsetLogical shifts clientToLogical so off-slide shapes are
  hit-testable — initializes the editor with a non-zero offset,
  drops an element at negative logical x/y, asserts a pasteboard-band
  click selects it.
- setSlideOffset is live — switches the offset post-mount and
  verifies clientToLogical follows along.

Two CodeRabbit nitpicks (style "empty space" → "space", code-fence
language specifier) are intentionally skipped: low value, doc-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@hackerwins
hackerwins merged commit 1cbd3f8 into main Jun 10, 2026
4 checks passed
@hackerwins
hackerwins deleted the slides-pasteboard branch June 10, 2026 14:24
hackerwins added a commit that referenced this pull request Jun 11, 2026
Three slides-editor bugs surfaced while testing groups; they travel
together because the same `/shared/<id>` deck exercises them in
sequence.

1. Lines drift after grouping their endpoints (the reported bug).
   `MemSlidesStore.group()` keeps a connector as a group child when
   both endpoints reference candidates, but `drawElement`'s connector
   branch skipped the per-element frame transform — `drawConnector`
   expects world-coord endpoints and `buildElementWorldLookup` lifts
   grouped targets to world, so the group transform compounded on top
   of already-world endpoints and the line jumped by the group's
   offset.

   `drawElement` now threads a cumulative `parentTransform`; on a
   connector child it inverts the parent transform onto the ctx and
   hands `drawConnector` the lookup's view of the connector — free
   endpoints are world-coord via `walkWorld`, attached endpoints
   already resolve through the lookup, so both kinds agree. The
   "v1 group() never includes connectors" NOTE in
   `element-renderer.ts` was stale since #263; #320 then bolted the
   world-coord lookup on top, which is exactly what broke this.

2. Rotate angle tooltip drifts by the pasteboard offset.
   `acquireRotateTooltip` appends to `overlay.parentElement` so
   `renderOverlay`'s innerHTML reset can't wipe it mid-drag, but
   `showTooltip` was computing coords against `overlay`'s rect.
   These containers shared an origin until #353 gave the overlay a
   non-zero `slideOffsetCssX/Y` inside `canvasWrap`. Measure against
   the tooltip's actual parent rect instead.

3. Rotate tooltip flickers at the previous drag's last position on
   re-acquire. `acquireRotateTooltip` flipped `display: block`
   immediately while `transform` still held the previous drag's
   terminal coords. Acquire now returns the cached element hidden;
   `startRotate` calls `showTooltip(clientX, clientY, 0)` once
   immediately so `transform` and `display: block` land in the same
   paint frame. Bonus: a 0° readout pops at the click position with
   no input latency.

Pre-merge code review surfaced one blocking concern and seven
non-blocking follow-ups (tracked in the task doc).

Fixed before merge: `invertGroupTransform` now returns `null`
instead of throwing on a singular matrix, and the connector branch
skips the paint. A degenerate group (frame.w = 0 with refSize > 0,
reachable via PPTX import) used to throw, escape `drawElement`'s
try/finally, and blank every later element on the slide.

Deferred follow-ups: flip blindness in `groupToTransform`;
reference-equality identity gate; ghost connector inside a group
ghost; `tooltipContainer` cached once at `startRotate`;
`overlay.parentElement` null fallback inside `acquireRotateTooltip`;
three copies of the 6-coef affine inverse math
(`element-renderer.ts` + `model/group.ts`); acquire-then-show
invariant naming.

Regression coverage:
- `slide-renderer.test.ts` — attached connector: `ctx.transform`
  runs before `moveTo` with the expected inverse coefficients; free
  connector: pre/post-group `moveTo`/`lineTo` args match (the lookup
  swap); singular parent transform: skips that connector but keeps
  painting the rest of the slide.
- `editor.test.ts` — pasteboard layout: tooltip transform encodes
  parent-frame coords, not overlay-relative; two-cycle drag: second
  pointerdown's transform reflects the new click position, not the
  previous drag's last move.
@hackerwins hackerwins mentioned this pull request Jun 12, 2026
5 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