No image zoom or pan, and a failed image blanks the canvas permanently #99

Closed
opened 2026-07-28 06:03:07 +00:00 by claude-bot · 1 comment

Severity: MEDIUM

The problems

No zoom or pan. The image canvas is object-fit: contain only
(ReviewWorkspacePage.tsx:493-497, styles.css:471-492). docs/circa-ui-spec.md §8.4 requires
"quick toggle… zoom and pan." Reading faded pencil on the back of a print without zoom is not
realistic — and the OCR review UI (#21) will only raise the stakes.

One failed image blanks the canvas permanently. onError sets img.style.display = "none"
imperatively. Because the same <img> element is reused for front and back, a failed back image
blanks the canvas and switching back to Front never resets the style. The unused
.image-canvas-placeholder class shows an error state was intended and dropped.

Scope

  • Wheel zoom, drag pan, double-click to toggle fit versus 100%. A small dependency (a pan-zoom
    hook) is justified — this is the primary work surface.
  • Reset visibility on src change, or key the <img> by showBack.
  • Render "Back image unavailable" in the canvas rather than showing nothing.
  • Keep zoom state sensible across front/back toggling and across photos.

Done when

  • A reviewer can zoom and pan to read handwriting
  • A failed image shows an explicit error and does not affect the other side
  • Zoom is reachable from the keyboard

References

  • frontend/src/pages/ReviewWorkspacePage.tsx:493-497
  • frontend/src/styles.css:471-492
  • docs/circa-ui-spec.md §8.4
## Severity: MEDIUM ## The problems **No zoom or pan.** The image canvas is `object-fit: contain` only (`ReviewWorkspacePage.tsx:493-497`, `styles.css:471-492`). `docs/circa-ui-spec.md` §8.4 requires "quick toggle… zoom and pan." Reading faded pencil on the back of a print without zoom is not realistic — and the OCR review UI (#21) will only raise the stakes. **One failed image blanks the canvas permanently.** `onError` sets `img.style.display = "none"` imperatively. Because the same `<img>` element is reused for front and back, a failed back image blanks the canvas and **switching back to Front never resets the style**. The unused `.image-canvas-placeholder` class shows an error state was intended and dropped. ## Scope - Wheel zoom, drag pan, double-click to toggle fit versus 100%. A small dependency (a pan-zoom hook) is justified — this is the primary work surface. - Reset visibility on `src` change, or key the `<img>` by `showBack`. - Render "Back image unavailable" in the canvas rather than showing nothing. - Keep zoom state sensible across front/back toggling and across photos. ## Done when - [ ] A reviewer can zoom and pan to read handwriting - [ ] A failed image shows an explicit error and does not affect the other side - [ ] Zoom is reachable from the keyboard ## References - `frontend/src/pages/ReviewWorkspacePage.tsx:493-497` - `frontend/src/styles.css:471-492` - `docs/circa-ui-spec.md` §8.4
claude-bot added this to the v0.3.0 milestone 2026-07-28 06:03:07 +00:00
Author

Done in 97d5cdc, with #98 and #100. CI run #60 green.

Done when:

  • A reviewer can zoom and pan to read handwriting
  • A failed image shows an explicit error and does not affect the other side
  • Zoom is reachable from the keyboard

Wheel zoom (cursor-anchored), drag pan, double-click to toggle fit versus actual size, +/-/arrows/0 from the keyboard, and visible zoom buttons in the toolbar so the capability is discoverable rather than only bindable. Zoom resets when the photograph changes and when the side toggles — a reviewer who zoomed into the back of one print should not land mid-zoom on the next.

No dependency was added, against the issue's suggestion that one is justified. Roughly sixty lines, and two reasons: the keyboard requirement in the third done-when box is the part pan-zoom libraries do worst, and scripts/audit.mjs is a gate whose surface is worth keeping small. If this proves fiddly in real use the decision is cheap to revisit.

The failed-image bug was as described and slightly worse. onError set style.display = "none" imperatively on an <img> shared between front and back, so a failed back scan blanked the canvas and switching to Front never restored it — React had no idea the style had been touched. It is now state keyed by which side is showing, and the canvas says "Back image unavailable" rather than showing nothing.

A second bug surfaced while building it, which no issue named. Pointer capture retargets the compatibility click at the capturing element, so a pan begun on the "View original" link would have made that link dead — at exactly the zoom levels where a reviewer most wants the original. Guarded in onPointerDown.

One honest gap in the coverage. Pan clamping, cursor-anchored zoom and the double-click fit toggle are implemented but not unit-tested: jsdom performs no layout, so offsetWidth, naturalWidth and every getBoundingClientRect() are zero and there is nothing to assert geometry against. toggleActualSize deliberately no-ops when it cannot measure, which is also why double-click is inert under test. The wheel test proves the wheel zooms, not where it zooms to. Holding those properly needs a Playwright test — the same route #97's dock invariant took — and is worth adding if this area changes again.

The zoom readout announces itself with aria-live="polite". Doing that required scoping a #104 test which had been asserting that no status role existed anywhere on the page after the job confirmation cleared; that assertion was accidentally load-bearing, making every future live region on this page a test failure.

Done in 97d5cdc, with #98 and #100. CI run [#60](https://git.rhoving.com/rbrooks/Circa/actions/runs/60) green. **Done when:** - [x] A reviewer can zoom and pan to read handwriting - [x] A failed image shows an explicit error and does not affect the other side - [x] Zoom is reachable from the keyboard Wheel zoom (cursor-anchored), drag pan, double-click to toggle fit versus actual size, `+`/`-`/arrows/`0` from the keyboard, and visible zoom buttons in the toolbar so the capability is discoverable rather than only bindable. Zoom resets when the photograph changes and when the side toggles — a reviewer who zoomed into the back of one print should not land mid-zoom on the next. **No dependency was added**, against the issue's suggestion that one is justified. Roughly sixty lines, and two reasons: the keyboard requirement in the third done-when box is the part pan-zoom libraries do worst, and `scripts/audit.mjs` is a gate whose surface is worth keeping small. If this proves fiddly in real use the decision is cheap to revisit. **The failed-image bug was as described and slightly worse.** `onError` set `style.display = "none"` imperatively on an `<img>` **shared between front and back**, so a failed back scan blanked the canvas and switching to Front never restored it — React had no idea the style had been touched. It is now state keyed by which side is showing, and the canvas says "Back image unavailable" rather than showing nothing. **A second bug surfaced while building it, which no issue named.** Pointer capture retargets the compatibility `click` at the capturing element, so a pan begun on the "View original" link would have made that link dead — at exactly the zoom levels where a reviewer most wants the original. Guarded in `onPointerDown`. **One honest gap in the coverage.** Pan clamping, cursor-anchored zoom and the double-click fit toggle are implemented but **not unit-tested**: jsdom performs no layout, so `offsetWidth`, `naturalWidth` and every `getBoundingClientRect()` are zero and there is nothing to assert geometry against. `toggleActualSize` deliberately no-ops when it cannot measure, which is also why double-click is inert under test. The wheel test proves the wheel zooms, not where it zooms to. Holding those properly needs a Playwright test — the same route #97's dock invariant took — and is worth adding if this area changes again. The zoom readout announces itself with `aria-live="polite"`. Doing that required scoping a #104 test which had been asserting that **no** `status` role existed anywhere on the page after the job confirmation cleared; that assertion was accidentally load-bearing, making every future live region on this page a test failure.
Sign in to join this conversation.
No description provided.