Accessibility: last-move crosshair#3584
Conversation
Spec for an opt-in setting that highlights the full row/column through the last played stone (under the stone) on all gobans, with color and thickness controls. Covers the goban submodule rendering (Canvas + SVG) and main-repo preference/UI wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TDD task-by-task plan across the goban submodule (callback API, Canvas drawingHash + draw + targeted invalidation, SVG dedicated layer) and the main repo (preferences, config wiring, Accessibility settings section, submodule bump). Each repo on its own branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reflect the final Canvas design (dedicated canvas behind the transparent stone layer, single strokes, cleared/redrawn wholesale) instead of the abandoned per-cell + drawingHash approach, and document why (discontinuous line + residual segments on navigation). Add the z-index variable step.
Mirrors the board-label-positioning setting: embeds a small live board with a centred last move (stones on its row and column) so the chosen crosshair colour and thickness are shown under the stones. Re-mounts via a key built from the colour/thickness so it reflects changes immediately.
…review label The preview sat below the colour picker and got hidden by the native colour popup. Lay the colour/thickness controls and the live preview side by side (controls left, board right) and remove the redundant 'Preview' label.
…y default MiniGoban (game thumbnails and setting sample boards) now sets the goban's dont_draw_last_move_crosshair flag, so the accessibility crosshair no longer clutters small preview boards (e.g. the stone-font-scale sample). The crosshair setting's own preview opts back in via lastMoveCrosshair.
Update spec + plan for the live MiniGoban preview in the setting and the per-board dont_draw_last_move_crosshair opt-out (crosshair shown on real boards, suppressed on MiniGoban thumbnails/previews, re-enabled on the setting's own preview). Refine the 'all gobans' scope note accordingly.
Used by the goban CrosshairLayer to sit between the shadow and stone layers.
…247) Points at the head of goban PR online-go#247 (not yet merged). Re-point to the merged commit on goban:main before this PR leaves draft.
Code Review FindingsBug:
|
|
(Note: anoek is away at the moment, please don't take lack of response as lack of interest. A screenshot would help with a quick appreciation of what this does, for the curious) |
- use pgettext (not llm_pgettext) for the static UI labels - MiniGoban: set dont_draw_last_move_crosshair after the json spread so a server-supplied config can't override the lastMoveCrosshair prop - sample preview board ends on a black stone at the centre
Drives the Accessibility setting and asserts the crosshair renders with the chosen colour and thickness on both the SVG and the old Canvas renderer.
|
Thanks for the review — all three addressed:
Also added a Playwright e2e test ( |
|
Reviewed for bugs and performance issues — nothing significant found. The implementation is clean and follows established codebase patterns throughout: preference keys, callback wiring, the MiniGoban key-based remounting for the live preview (matches the same pattern already used in ThemePreferences for stone-font-scale and other sliders), and the goban config flag placement after the json spread to prevent server-supplied config from overriding the prop. The getLastMoveCrosshair getter correctly reads fresh preference values on every draw without any caching hazard. No type safety gaps, no missing bounds checks that the HTML range input does not already enforce. The only open item is the submodule pointer, which the PR description already flags (waiting for goban#247 to merge before leaving draft). |
@GreenAsJade in attachment you will find 2 screenshots I hope it would make this PR better understood
|
|
Code review: two issues found - (1) E2E SVG selector likely broken: .crosshair-layer vs CrosshairLayer PascalCase mismatch; (2) MiniGoban re-mounts on every slider tick performance concern. See full comment below. |
|
Bug: E2E test SVG selector likely broken. In e2e-tests/accessibility/crosshair.spec.ts, SVG crosshair lines are queried via .crosshair-layer line. The Canvas renderer names its layer CrosshairLayer (PascalCase - see the submodule Goban.css and the canvas-phase assertion in the same test: canvas.CrosshairLayer). If the SVG layer follows the same PascalCase convention, the kebab-case selector .crosshair-layer will never match and the toBeAttached() assertion will fail. Please verify the actual class on the SVG layer and align the selector. |
|
Performance: MiniGoban re-mounts on every slider tick. The preview uses a key built from color and thickness to force re-mounts when preferences change. A range input fires onChange on every pixel during a drag (30-60x per second), so each slider position unmounts and remounts a full GobanController and writes to localStorage. For a 5x5 board the cost per mount is small, but a short debounce (~80ms) on the setThickness/setColor calls - keeping local controlled-input state separate from the persisted preference - would eliminate the churn without affecting perceived responsiveness. |
|
Thanks! Two clarifications on the latest automated review: 1. E2E SVG selector — not a mismatch (working as intended). The two renderers deliberately use different class conventions, matching their existing layers:
So 2. Slider re-mount — consistent with the established pattern. The key-based MiniGoban re-mount mirrors what Happy to revisit if you'd prefer the debounce here regardless. |
Code review: bugs and performance issues
|
|
On the slider re-mount / per-tick preference write: this is the established convention, not new behaviour. ThemePreferences' "Stone font scale" range slider does exactly the same — So the per-tick remount + write isn't introduced by this PR; it matches the surrounding code. Moving slider previews to local controlled state + a single |
|
Code Review: main-repo changes Reviewed: preferences.ts, configure-goban.tsx, AccessibilityPreferences.tsx, MiniGoban.tsx, Settings.tsx, 01_variables.css, e2e-tests. No bugs found. One performance concern: MiniGoban preview remounts on every slider tick The key prop (key=color-thickness) on the MiniGoban preview changes on every onChange from the thickness range input, which fires continuously while dragging. With step=0.01 over the range 0.02-0.4, a single drag creates and destroys ~38 goban instances in quick succession. Since getLastMoveCrosshair() reads from preferences.get() at draw time, a debounce on the key update would avoid the churn. The current approach works on a 5x5/150px board on modern hardware, but may cause visible jank on lower-end devices. Everything else looks correct: the dont_draw_last_move_crosshair override in MiniGoban is placed after the json spread (preventing server config from overriding it), the callback in configure-goban.tsx follows the established getter pattern, and the Settings registration matches all other preference pages. |
Performance: Excessive MiniGoban remounting during settings adjustmentsIn The architecture already avoids this: const gobanRef = useRef<Goban | null>(null);
// in the MiniGoban, expose a ref and call gobanRef.current?.redraw(true)
// whenever color or thickness changes, instead of changing the keyThe canvas allocation limit is a known concern in this codebase (see |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code review
The PR adds }, [props.game_id, props.review_id, props.width, props.height]);If a caller changes the Fix: add |
Clarify that dont_draw_last_move_crosshair is read once at GobanController construction (like connect_to_chat / enable_sounds in the same literal) and is therefore intentionally absent from the effect's board-identity dependency array. The sole opt-in caller passes a constant true and remounts via key. Address review (online-go#3584). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
On the missing
On the dynamic-toggle scenario: the only caller that passes the prop is I've added a short comment at the flag making this explicit (construction-time, why it's not a dep) so the intent is clear to future readers. If the team would rather move all settings previews to controlled local state + a single |
Code ReviewSummary: Adds an opt-in accessibility setting to draw a crosshair through the last-played stone, with color/thickness controls and a live MiniGoban preview. Performance: preview board re-mounts on every slider step
A simple debounce on the key — or on just the |
already answered at #3584 (comment) |


Fixes #
Proposed Changes
#1e6bff) and a relative thickness slider, with a liveMiniGobanpreview beside the controls.accessibility.last-move-crosshair{,-color,-thickness}) and wire them into the goban via a newgetLastMoveCrosshair()config callback inconfigure-goban.MiniGobanthumbnails/preview boards by default (via goban's newdont_draw_last_move_crosshairflag); the setting's own preview opts back in.--z-goban-crosshair-layerz-index variable used by goban's crosshair layer.docs/superpowers/.The rendering itself (Canvas + SVG, under-stone crosshair) is in goban#247.
Notes for reviewers