Mark up a live slide: the pen, and three deck fixes - #195
Conversation
Three reported bugs on slide decks, plus the presenter's pen. Hovering a Mermaid diagram on a slide showed a full-canvas expand icon: `.mermaid-diagram svg` is a descendant selector, so it also matched the expand button's own icon and sized it to the slide (356x356 on a stage slide, now 16x16). Same over-reach fixed in document mode. While there, deck diagrams were painting the document's framed panel from the host's --color-* tokens, putting an app-dark box behind a light deck. Clicking "Attachments" in the left column reloaded the page and left you at the top. turbo-rails 2.0.23's `locationWithActionIsSamePage` returns false unconditionally, so every same-page fragment link is a full visit — and the scroll it then performs races the deck's async slide layout. The back-matter links now jump in place, the way the outline links already do. Marking up a slide mid-show, two gestures that share one drag: - Stowed, a drag highlights text in the deck's own accent as inverse video — it has to read from the back of a room, on a projector, in every theme. - `d` arms the pen (DeckInk): a drag paints, and each stroke fades on its own a couple of seconds later. Nothing is saved, nothing is broadcast, there is no undo. A laser pointer with a short memory. Either way a still click stays "next", and the mark goes with the slide it was drawn on. Two gotchas worth knowing: preventing pointerdown's default suppresses the compatibility mousedown, so a pen stroke leaves no travel for the drag guard to measure (the pen reports its own gesture); and the stroke node must not be created until the gesture has travelled, or a tap paints a one-frame dot under every advance click. The pen's color is a new required theme token, --deck-pen (plus optional --deck-title-pen), because the design system's rule is that deck colors come only from --deck-* tokens: rose on coplan, amber on graphite, teal on poster — each picked so it can't be mistaken for that theme's accent. Also fixes a bug found on the way: the deck outline listed only slide 1's headings, because buildToc scanned the first .markdown-rendered and a deck renders one per slide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 32c3772a57
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (this.armed) return | ||
|
|
||
| this.armed = true | ||
| // Capture phase on the document: a stroke must not depend on what the |
There was a problem hiding this comment.
Route pen events through Stimulus actions
Move these stable document-level pointer bindings into the presenter’s Stimulus data-action declarations (for example, pointermove@document) and delegate to DeckInk. The dynamically created stroke paths do not receive these listeners, so the documented exception does not apply; this imperative wiring bypasses the repository’s required controller binding convention.
AGENTS.md reference: AGENTS.md:L90-L90
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Not changed — leaving this one for @hamptonlintorncatlin to rule on, since it is a deliberate deviation from a written convention rather than an oversight.
The pen listens on document, and its listeners exist only while the pen is armed: arm() adds them, stow() removes them, so nothing pen-related runs when the pen is stowed. data-action bindings are always live, so complying would mean an always-bound pointermove@document on every plan page with a deck, plus four pass-through methods on the presenter whose only job is to satisfy the binding style. (The CPU cost of that is negligible — the argument is lifecycle scoping, not perf.)
It also matches the controller it lives in: deck_presenter_controller already adds and removes document-level keydown, click, mousedown, and mouseup in capture phase for exactly the same reason — the show owns the keyboard and pointer only while it is running. Happy to convert if you would rather the convention hold strictly here; it is a mechanical change.
🤖 Addressed by Claude Code
Two ways the presenter's drag guard could swallow a click that wasn't a drag, both from state outliving the gesture that set it. The mouse origin was never cleared. After any physical click it stayed set, so the next click synthesized without coordinates — Space on a focused checkbox, or assistive technology — measured (0, 0) against the last mouse position, read as a drag, and was swallowed before the interactive-control pass-through could run. Ticking a task box mid-show silently did nothing. The origin is now consumed by the click its own press produced. A cancelled pointer claimed a click that never came. pointercancel ran the same path as pointerup and set the painted flag, but a cancellation synthesizes no click, so the flag sat there until some later unrelated click ate it instead of advancing the show. Only a lifted pointer claims a click now, which also fixes the same leak on Escape mid-stroke; a cancelled stroke is discarded, because it was never made. Both are pinned by system specs that fail without the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three reported deck bugs, and the thing they turned into: a presenter can now mark up a live slide.
The pen
Mid-show there are two ways to put the room's eyes on something, and they share one drag:
darms the pen — a drag paints, and each stroke fades on its own a couple of seconds later. Nothing is saved, nothing is broadcast, there is no undo. A laser pointer with a short memory.Either way a still click stays "next", and the mark goes with the slide it was drawn on. A small badge bottom-left (pen glyph +
d) mirrors the slide number's corner — without it, "why did my drag stop selecting text?" is a mystery. Escape peels one layer at a time: popover → pen → end show.The pen lives in
engine/app/javascript/coplan/deck_ink.js, owned bycoplan--deck-presenter, which keeps the gesture arbitration.Fixes
Giant Mermaid expand icon on a slide.
.mermaid-diagram svgis a descendant selector, so it also matched the expand button's own icon and sized it to the canvas — 356×356 on a stage slide. Scoped to> svg; the icon is 16×16 again with the chip at 26×26. Same over-reach fixed in document mode. While there: deck diagrams were painting the document's framed panel from the host's--color-*tokens, which flip with the reader's light/dark mode, putting an app-dark box behind a light deck's diagram."Attachments" in the left column reloaded the page and left you at the top. turbo-rails 2.0.23's
Navigator#locationWithActionIsSamePageisreturn falseunconditionally, so every same-page fragment link is a full visit — and the scroll it then performs races the deck's async slide layout. The back-matter links now jump in place, the way the outline links above them already do.The deck outline listed only slide 1's headings.
buildTocscanned the first.markdown-rendered; a document renders one, a deck renders one per slide. Found on the way; mechanical fix only — the outline still lists headings, not slide numbers, which is the intended design.Theme tokens
The pen's color is a new required token,
--deck-pen, plus optional--deck-title-pen. The design system's rule is that deck colors come only from--deck-*tokens, so a hardcoded ink would have broken the contract. Rose on coplan, amber on graphite, teal on poster — each picked so it can't be mistaken for that theme's accent. Poster's title canvas is its accent, so the pen there is the paper color.docs/SLIDE_SPEC.md's token table is updated; all three shipped themes verified on screen.Two gotchas worth knowing
Both cost me a real bug during verification:
preventDefault()onpointerdownsuppresses the compatibilitymousedown. The presenter's drag-vs-click guard measures mousedown→click travel, so it was blind to a pen stroke — a drag drew and advanced the slide. The pen now reports its own gesture (DeckInk#consumePainted).Verification
New
spec/system/deck_ux_spec.rb— 6 browser examples covering all of it: drag-highlights / click-advances, the expand chip staying chip-sized, the attachments jump asserting noturbo:visitfires, and the pen (paints-and-holds-then-fades, tap-advances-with-no-dot, Escape-stows-without-ending).Full suite 1772 examples, 0 failures. Also driven by hand in a real browser: all three themes, document mode, the deck scroll view, and present mode.