Files
pansy/web/src/styles/index.css
T
steveandClaude Opus 4.8 bfc5d9a871
Build image / build-and-push (push) Successful in 9s
Gadfly review (reusable) / review (pull_request) Successful in 7m40s
Adversarial Review (Gadfly) / review (pull_request) Successful in 7m41s
Make the canvas keyboard-reachable + trap focus in dialogs (#84)
The arrow-key nudge handler existed but only ever acted on a POINTER
selection, and nothing could select without a mouse — so the feature was
unusable by exactly the keyboard users it's for. This is the scoped first
slice: give the canvas a keyboard path in, and fix the Modal focus trap that
every destructive confirmation goes through.

Canvas:
- The <svg> gets role="application" + an aria-label describing the controls,
  and a <title> naming the garden — a screen reader now announces an
  interactive canvas rather than an empty graphic.
- Each object <g> is a focusable role="button" with an aria-label (name +
  kind) and aria-pressed reflecting selection. Enter/Space selects it — the
  step that was missing — which makes the existing arrow-key nudge reachable.
- A :focus-visible CSS rule draws a dashed accent ring on keyboard focus (and
  NOT on a mouse click, which is the point of :focus-visible). CSS rather than
  React state because onFocus on an SVG <g> is unreliable, and a CSS rule
  cleanly overrides the shape's inline stroke.

Modal (blast radius: DeleteGarden/ClearBed/DeletePlant/DeleteSeedLot/Share):
- Tab is trapped inside the dialog and wraps at the ends, instead of walking
  out into the page behind the backdrop.
- On close, focus returns to the element that opened the dialog rather than
  landing on <body>.

Verified live against the built binary with real keyboard input: Tab focuses
an object (SVG <g tabindex> genuinely takes focus), Enter flips aria-pressed
false→true, the focus-visible dash renders (computed stroke-dasharray "5px,
4px"), the dialog traps focus through 5 Tabs, and Escape closes it and
restores focus to the opener.

Follow-ups noted, not done here: object dimensions in the aria-label (needs the
garden's unit context this component doesn't hold), roving-tabindex between
plops inside a focused bed, and the EditorRail tablist semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
2026-07-21 23:20:47 -04:00

60 lines
1.5 KiB
CSS

@import 'tailwindcss';
/* Garden-planner theme tokens. Each --color-* becomes a Tailwind utility
(bg-*, text-*, border-*). Light by default; dark via prefers-color-scheme. */
@theme {
--color-bg: #f8faf7;
--color-surface: #ffffff;
--color-border: #e3e8e0;
--color-muted: #61705d;
--color-fg: #1c2419;
--color-accent: #3f8f4f;
--color-accent-strong: #2f7a3e;
--color-accent-contrast: #ffffff;
--font-sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
@layer base {
html,
body,
#root {
height: 100%;
}
body {
background-color: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
}
/* Keyboard focus on a canvas object (#84). :focus-visible shows the ring for
keyboard focus but not a mouse click; the dashed accent ring distinguishes
"focused" from the solid ring that marks "selected". A CSS rule overrides
the shape's inline stroke presentation attributes. */
.object-shape {
outline: none;
}
.object-shape:focus-visible :is(rect, ellipse) {
stroke: var(--color-accent-strong);
stroke-width: 2;
stroke-dasharray: 5 4;
}
}
/* Dark theme: override the same tokens so utilities recolor automatically. */
@media (prefers-color-scheme: dark) {
@theme {
--color-bg: #10140f;
--color-surface: #171c15;
--color-border: #2a3226;
--color-muted: #8a967f;
--color-fg: #e7ede2;
--color-accent: #5bb06a;
--color-accent-strong: #6fc07d;
--color-accent-contrast: #0c1109;
}
}