Redesign diagram controls as a centred icon toolbar #432
- MA
## What changed
SeeCode diagram controls are now one compact, quiet toolbar centred under the drawing, using icons instead of text buttons.
- **Playback:** **Play/Pause**, then ← `3 / 9` →, then **Restart**, all in one group. - The diagram plays on load, and the counter shows the step being played. - The arrows are disabled while it plays. - **Pause** stops on the current step and shows its number. - **Play** resumes from the step on screen, or from the start once it has reached the last step. - The arrows are disabled at each end. The count uses tabular numbers. - **Restart** replaces both Replay and Reset (see below). It replays from step 1. - **Zoom** (−, `85%`, +, **Fit to width**, **Actual size**) moves into the same bar after a hairline divider. - **Spacing:** diagram blocks (SeeCode, and Mermaid with its source hidden) get `calc(var(--spacing) * 10)` of trailing margin. - **States:** hover, pressed (`scale: 0.96`, which reduced motion turns off), disabled at `--opacity-quiet`, the global focus ring, and coarse-pointer targets. Tooltips come from the existing `IconTooltip`. - **Icons:** new `@chopin/icons` glyphs `ArrowLeftIcon`, `ArrowRightIcon`, `RestartIcon`, `MinusIcon`, `FitWidthIcon`, `ActualSizeIcon`, `PlayIcon` and `PauseIcon`. `@chopin/diagrams` now depends on `@chopin/icons`.
### Playback model
`packages/diagrams/src/playback.ts` is a small pure state machine with unit tests.
- **Playing** has a start step and the step reached so far. It is running until it reaches the last step. - **Paused** holds one step. - The arrows only move playback that isn't running, so a click can never race the animation. - **Counting during animation:** the counter follows the drawing itself. It advances on each step's CSS `animationstart` instead of keeping a second clock. - **Resuming:** resuming from step N remounts the drawing with `--sc-from: N`. Steps 1…N are marked `.is-done` before paint, so they stay drawn, and step N+1 starts one stagger later. - **Finishing:** once the last step has finished drawing, playback settles into "paused on the last step". A later redraw of the same diagram, such as the layout recomputed on resize, therefore does not replay the animation, and "Step N of N" is announced.
**Reduced motion:** nothing plays on load. The diagram opens on the whole drawing, showing `9 / 9`, and Play is available. Play and Restart step through the stepped view one step per stagger, with no animation. Reduced motion is about motion, not about whether information is revealed in sequence, so a discrete reveal that the reader asks for stays available.
## Replay and Reset are merged
- **Replay** remounted the drawing so the entrance animation played again. It kept the selection and was hidden under reduced motion. - **Reset** cleared the selection and showed a still image.
Both just took you back from stepping to the whole diagram, so I merged them into **Restart**: it clears the selection and replays from step 1. On diagrams without steps, Reset's only job was clearing a selection, and the details panel's Close button already does that.
## Bugs from testing, and their root causes
I reproduced these with headless Playwright against the real app. I ran a server with `NO_PLANNER=1` and the fake-GitHub preload, and opened a document with stepped SeeCode diagrams between long prose.
1. **The counter showed `– / 8` while playing.** The old model had no notion of the step being played; "playing" was a single state. Playing now carries a step, advanced from `animationstart`. 2. **Clicking the arrows mid-animation glitched and moved the scroll.** The arrows switched to step mode while the CSS entrance was still running. The arrows are now disabled until playback is paused or finished. 3. **Clicks moved the document by roughly 100–470px.** - **Cause:** `[data-plan-scroll]` has `scroll-padding-block: 4rem 30%`, which keeps the caret clear of the window edges. Any scroll-into-view of an element in the lower 30% of the window, or the top 4rem, moves the document. - **What triggered it:** when a focused arrow became `disabled` at the end of its range, the browser blurred it. The previous commit then called `button.focus()` without `preventScroll`, and that scrolled the document (reproduced: 564 → 1033). - **Fix:** disabled controls now use `aria-disabled` with no-op handlers. They keep focus, so no programmatic `focus()` call is needed. There are no `focus()` or `scrollIntoView` calls left in the diagram. - **Not reproduced:** my synthetic pointer clicks never move the document by themselves, in either headless Chromium or Chrome. The "each click scrolls up ~100px" pattern did not appear in any scenario I tried. - **Testing pitfall:** Playwright's own `locator.click()` scrolls its target into view and trips the same scroll padding. The regression test uses pointer clicks at the control's position for that reason. 4. **Restart only seemed to jump the page up without restarting.** The scroll jump above made it look that way. Restart does remount the drawing, and the e2e now checks that the last step's opacity is back to `0` right after Restart. 5. **Stepped view could ghost the whole drawing.** I found this while testing reduced motion. When `availableWidth` changes, the renderer produces a new body, and the step classes were not re-applied to it. As a result every step could stay ghosted and ignore pointer input. The step-visibility effect now depends on `body`.
## Screenshots
Local captures are in `/tmp/diagram-shots/`: - Before: `before-*.png` - First pass: `after-*.png` - Real app: `after2-playing.png` and `after2-paused.png`
## Tests
- **Unit:** `packages/diagrams/src/playback.test.ts` covers counting, pausing, the arrows' ends, resuming, restarting after the end, and reduced motion. The `diagram.test.tsx` checks cover labelled icon buttons, a single Restart, and the counter starting at `1 / N`. - **e2e/diagram-gallery:** checks the bar is centred under the drawing; the counter during playback; arrows disabled while playing; Pause holding its step; stepping to both ends while keeping focus; Play resuming from the current step; finishing at `N / N`; and Restart from step 1. The reduced-motion test checks there is no autoplay, that Play steps discretely, and Restart. - **e2e/seecode (real app):** a stepped diagram between long prose, with the caret left in prose and the control bar in the lower 30% of the window. Previous, Next at the end, Restart, Pause and Play leave the document and window scroll unchanged. Restart replays from step 1. - **Local runs:** - `bun test` passes (4227 tests). - `bun run types` passes. - `bun run diagrams:test` passes (11 tests, run several times). - `seecode.e2e.ts` passes against a local server (4 tests, 3 runs). - I didn't run the full `bun run e2e` locally because of port collisions; CI covers it.
## Needs a design-contract review
- `packages/icons/src/line.tsx` has eight new `LineIcon` wrappers that spread props, the same pattern as the existing icons. I added their exact cases to `scripts/design-contract/exceptions/dynamic-packages.json`. I did **not** renew the group's `sourceHash`; the new file sha256 is `0d8001ef2cb9f09496fd1502e0bf42e531eea31f78f8298bdacbce0b3cafb9b4`. Until it is renewed, `check-design-contract` fails with "reviewed dynamic owner changed". Every other CI check passes locally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Unknown GitHub usercommitted eb05826de5e0last month on GitHub
Unknown GitHub usercommitted fbe2e75e29aalast month on GitHub
The preview deployment for **chopin** failed. 🔴
[Open Build Logs](https://next-appserver.tail49836a.ts.net/project/l9buznc9ua1if0vflxrjwajs/environment/lxqsvczv19eopxk29vfb46mj/application/hb85gdd9mwqqlfwcteflvpbj/deployment/nzb0huu1ez6gs9iytwsuscup) | [Open Application Logs](https://next-appserver.tail49836a.ts.net/project/l9buznc9ua1if0vflxrjwajs/environment/lxqsvczv19eopxk29vfb46mj/application/hb85gdd9mwqqlfwcteflvpbj/logs)
Last updated at: 2026-10-09 14:48:03 CET
MaggieAppletonreferenced this in Draw wireframe fences as chromeless, inert blueprints #436last month on GitHub
Unknown GitHub usercommitted 880f1929b37flast month on GitHub
Unknown GitHub usercommitted 62f2fabe3987last month on GitHub