Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions docs/stories/US-005-ux-polish-accessibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# User Story: UX Polish & Accessibility Hardening

**Story ID:** US-005
**Epic:** Platform Quality (cross-feature)
**Status:** In progress

---

## 1. Description

**As a** visitor using the clock, timer, or alarms,
**I want** the panels to be legible, proportional, and fully accessible,
**So that** the product feels deliberate and cohesive, and future features
build on a design system instead of fighting one.

Priority owner decision (2026-09-13): this story **takes priority over
US-002 (recurring alarms) and US-004 (multi-clock)**. It absorbs backlog
US-003 items 1–3.

---

## 2. Context & Scope

- **In Scope:**
- Visible, persistent field labels for every input (placeholder alone is
not a label — current placeholders are browser-default gray on `#111`)
- One shared control scale across alarm and timer panels (the alarm create
row currently mixes 1.2em inputs, an 0.8em/0.7-opacity Add button, and
~20px tap targets)
- WCAG AA contrast for all text/background pairs, enforced by computed
ratios (error `#f00` on `#1a1a1a` is ≈4.35:1 today; note text at opacity
0.55 ≈4.5:1; disabled rows ≈3:1)
- Semantic design tokens to replace the per-element "opacity soup"
(0.3–0.75) — text tiers, placeholder, input surface, focus ring, danger
- Keyboard + screen-reader behavior: focus moves into opened panels,
returns to the toggle on close; ring overlay traps focus and is
`aria-modal`; toggles expose `aria-expanded`/`aria-controls`; alarm list
changes announced
- `prefers-reduced-motion` support for pulses and panel/FLIP animations
- Outside-click no longer closes the alarm panel while the ring overlay is
up (absorbed US-003 item 2)
- FLIP panel helper extracted to `shared/dom/` (absorbed US-003 item 1)
- **Out of Scope:**
- Retheming (the green/dark terminal aesthetic stays — this story
normalizes within it)
- New features, behavior changes to the pure cores, storage format changes
- Sound design (alarm tone is `playBeep()`, unchanged; audible verification
stays a manual QA step)

---

## 3. Acceptance Criteria

- [x] **AC-1:** Every input has a persistent visible label; placeholder text
uses a themed token.
- [x] **AC-2:** All text/background pairs pass WCAG AA (4.5:1 normal text,
3:1 large text), verified by computed ratios recorded in this story.
- [x] **AC-3:** Alarm and timer panels share one control scale; the alarm
create section uses a labeled layout with comfortable spacing.
- [x] **AC-4:** Keyboard-only pass: opening a panel moves focus into it,
closing restores focus to the toggle, the ring overlay traps focus and
is `aria-modal` (Esc/Enter dismiss), and no keyboard trap exists.
- [x] **AC-5:** Panel toggles expose `aria-expanded`/`aria-controls`; alarm
list changes are announced via a polite live region; all rendered row
controls are labeled.
- [x] **AC-6:** `prefers-reduced-motion` disables pulses, panel open/close,
and FLIP animations.
- [x] **AC-7:** Interactive targets are ≥24×24 px (WCAG 2.5.8), ~44 px where
space allows.
- [x] **AC-8:** Outside click does not close the alarm panel while ringing.

---

## 4. Technical Guidance & Architectural Constraints

- **Token layer first:** `variables.css` gains semantic tokens (text tiers,
input surface, placeholder, focus ring, AA-safe danger, control sizes);
component CSS consumes tokens — no raw opacities for text tiers.
- **Markup:** `index.html` gains `<label>` elements (or equivalent durable
text) and a panel heading; row markup changes are limited to what ACs
require.
- **UI wiring only:** `alarm.ui.ts` / `timer.ui.ts` handle focus management,
live regions, and the ring guard; `clock.ui.ts` untouched.
- **New pure module:** `shared/dom/flip.ts` (the FLIP helper from both UI
files) with mirrored unit tests; jsdom tests for focus/aria behavior.
- **Verification:** screenshots (desktop/mobile, before/after, error, ring,
reduced-motion) stored under `docs/stories/US-005/screenshots/` and
reviewed per phase; `make check` must pass; 135 existing tests must keep
passing unchanged.
- No new npm dependencies; no ADR required (token pattern already exists).

---

## 5. Definition of Done Checklist

- [x] All ACs verified (screenshots + computed contrast + Playwright
assertions + jsdom FLIP unit tests).
- [x] `make check` green; prettier clean.
- [x] Backlog updated (US-003 absorbed; priorities adjusted).
- [ ] QA assessment passed; code review approved.

---

## 6. Verification Record (2026-09-13)

### Computed WCAG contrast (all AA-pass)

| Pair | Ratio |
| ------------------------------------------------------------- | ------- |
| primary `#0f0` on surface `#1a1a1a` (rows, inputs) | 12.68:1 |
| text-secondary (0.72 composite) on surface (labels, times) | 7.01:1 |
| text-muted (0.60 composite) on surface (notes, disabled rows) | 5.21:1 |
| placeholder (0.55 composite) on surface-alt `#111` (inputs) | 4.71:1 |
| danger `#ff6b6b` on surface (error text) | 6.27:1 |
| primary on page bg `#222` (clock, toggles) | 11.59:1 |

Before-state failures that motivated the tokens: error `#f00` on surface
≈4.35:1, placeholder (browser default) ≈3.5:1, note text at opacity 0.55
≈4.5:1, disabled rows at opacity 0.4 ≈3:1.

### Automated behavior assertions (Playwright, 20/20 PASS)

`aria-expanded` on both toggles (initial + open); focus moves into the
panel on open; both fields have exactly one visible `<label>`; Escape and
outside-click close with focus restored to the toggle; ring overlay is
`aria-modal`, receives focus on ring, Tab is trapped between Snooze/Dismiss;
outside click does not close the panel while ringing; Enter dismisses and
focus returns to the toggle; reduced-motion produces no FLIP transform.

### Unit tests

`tests/unit/shared/dom/flip.test.ts` — 5 tests (reduced-motion bypass, null
container, invert-then-cleanup, no-delta cleanup, absent matchMedia).
Full suite: 140 passing (135 pre-existing + 5 new).

### Screenshots

`docs/stories/US-005/screenshots/before|after/` — 14 shots each: desktop
idle/panel/rows/error, ring overlay, keyboard focus walk, timer panel
(incl. running toggle), mobile (375px) and narrow (360px) panel states,
reduced-motion panel. Reviewed per phase; notable before→after fixes:
red ⏰ / blue 💤 emoji replaced with monochrome line icons, buttons now
inherit the page font, timer −/+ steppers always visible (were hover-only,
unusable on touch), alarm create row de-cramped into labeled stacked fields.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 27 additions & 26 deletions memory-bank/activeContext.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
# Active Context

Updated: 2026-09-13
Updated: 2026-09-13 (evening)

## Current focus

**Repo housekeeping — DONE on branch `chore/repo-housekeeping`**
(5 commits off `dev`, awaiting PR into `dev` + review):

1. ✅ Memory bank initialized (HK-4).
2. ✅ README corrections (HK-1): `robots.txt` removed from `src/public/` tree,
build-time generation note added, alarm storage test added to tests tree,
`tasks/` added to the repo-layout table.
3. ✅ Actions bump (HK-2): checkout@v7, setup-node@v7, setup-buildx@v4,
login@v4, build-push@v7, hadolint-action@v3.5.0. All four Dockerfiles
pre-validated locally with hadolint 2.15.1 (repo config: only info-level
DL3059 in the webhook image).
4. ✅ Husky guard (HK-3): hook skips lint-staged unless staged files match
`^web/.*\.(ts|js|json|css|md)$`; verified silent on yml- and md-only
commits (noise previously confirmed on both).
5. ✅ `tasks/` close-out: US-001 marked merged+deployed (Release #5,
healthz 200); HK-1..4 removed; HK-5 (nginx 1.27 EOL re-pin) added as a
backlog candidate.
**US-005 — UX polish & a11y hardening: implemented on `feat/us-005-ux-polish`**
(off `dev`), awaiting owner QA/review → PR into `dev`. Commits: story +
baseline shots → token layer → alarm redesign → timer normalization → a11y
wiring/FLIP → AA placeholder fix → focus-restore fix → verification record.

- Verify with the story's §6 record
(`docs/stories/US-005-ux-polish-accessibility.md`): contrast table (all
AA), 20/20 Playwright a11y assertions, 140 unit tests, before/after
screenshots.
- Design decisions worth knowing: green/dark aesthetic kept; emoji glyphs
replaced by inline SVG (`currentColor`); text-tier tokens replace the
opacity soup (never use raw opacity for text); `panelHadFocus` pattern in
both UI files because mousedown blurs before outside-click close runs;
timer toggle renders icon + text span (never replace button content).
- Screenshot tooling lives in `/tmp/ux-shots/` (Playwright 1.63 — NOT a repo
dependency): shoot.mjs (14-shot matrix), a11y.mjs (20 assertions),
contrast.mjs (WCAG math). Vite dev server on :5173.

## Environment state

- `chore/repo-housekeeping` = dev + 5 commits; `dev` = `origin/dev` = `223d41f`.
- Local `main` = `origin/main` = `c7ecdca` (post-rebase-merge sync).
- US-001 deployed; homepage edge cache may lag up to 2 h.
- Branch `feat/us-005-ux-polish` = dev + 8 commits; `dev`/`main` synced.
- `make check` green (140 tests). Vite dev server may still be running on
:5173 (started for screenshots).

## Next steps

- Open PR `chore/repo-housekeeping` → `dev`; merge (no production risk —
workflow changes ride CI on next dev push).
- Owner: production audio check (alarm tone) → US-001 fully Done.
- Pick next epic: US-002 (recurring alarms) vs US-004 (multi-clock).
- Schedule HK-5 (nginx 1.27 → current stable, tag + digest together).
- Owner: review US-005 (screenshots + behavior), then PR into `dev` →
`main` (rebase-merge; the ruleset requires PRs for `main`).
- Owner: audible alarm-tone check → US-001 fully Done.
- Pick next epic: US-002 (deprioritized) vs US-004 (blocker cleared by
US-005's shared FLIP helper).
- Schedule HK-5 (nginx re-pin) / HK-6 (cache purge or asset retention).
18 changes: 16 additions & 2 deletions memory-bank/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@

US-001 (Multiple Alarms) is **deployed to production** (PR #13 merged
2026-09-13 21:17Z; Release run #5 succeeded; `healthz` verified 200).
US-005 (UX polish & a11y hardening) is **implemented on
`feat/us-005-ux-polish`** — all ACs verified, awaiting owner QA/review.
Site is live at `clock.taylormadetech.net`.

## What works

- Clock (tick, title mirror, timezone), timer (presets, persistence, beep),
alarms (multi, snooze, persistence, ring overlay) — 135 unit tests green.
alarms (multi, snooze, persistence, ring overlay) — 140 unit tests green.
- **Design system (US-005):** semantic text-tier tokens (secondary 7.0:1,
muted 5.2:1, placeholder 4.7:1 — never raw opacity for text), AA-safe
danger `#ff6b6b`, global `:focus-visible` ring, monochrome inline-SVG
icons, shared FLIP helper (`shared/dom/flip.ts`, reduced-motion aware),
focus management + `aria-modal` ring overlay with Tab trap,
`prefers-reduced-motion` global kill switch, `panelHadFocus` focus-restore
pattern in both UI features.
- Full CI gate: prettier, eslint, typecheck, vitest+coverage, build,
`nginx -t` × 2, compose config × 2, hadolint × 4.
- Automated deploys: `main` → GHCR → webhook → swap with auto-rollback.
Expand All @@ -20,8 +29,13 @@ Site is live at `clock.taylormadetech.net`.

- Production audio QA: alarm tone not yet verified by a human (jsdom can't
test audio) — last open US-001 DoD item.
- **Stale-HTML → dead-asset window after every deploy** (HK-6): a rebuild can
change asset hashes; the 2 h edge-cached homepage then references assets
that 404 on origin, and the 404 gets edge-cached too (observed 2026-09-13).
Fix candidates: CF API purge post-deploy, retain old assets in `deploy.sh`,
or shorter HTML TTL.
- Edge-cached homepage can lag up to 2 h behind a deploy (expected; judge
freshness via `/healthz` or hashed assets).
freshness via `/healthz` or a query-string cache-bust, which works).
- CI actions pinned to Node-20-runtime majors (HK-2 — fix in progress).
- `nginxinc/nginx-unprivileged:1.27-alpine` is an EOL mainline branch
(HK-5 candidate: re-pin to current stable + digest).
Expand Down
40 changes: 24 additions & 16 deletions tasks/backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,19 @@ sized/sequenced by the SDM.

- Status: Candidate — needs a user story from the Product Owner before work.
- Source: MVP cut recorded in US-001 ("Out of Scope": daily-repeat alarms).
- Priority: High — most requested natural follow-up to US-001.
- Priority: **Deprioritized behind US-005** (owner decision 2026-09-13) —
still the most requested natural follow-up to US-001.
- Dependencies: none (builds directly on the US-001 alarm core).

### US-003 — Alarm ergonomics follow-ups

- Status: Candidate — small items culled from the US-001 code review (non-blocking).
- Contents:
1. Extract the FLIP panel-open helper duplicated between timer.ui.ts and
alarm.ui.ts into `shared/dom/` (refactor; both features keep behavior).
2. Guard the document-level outside-click handler so the alarm panel does
not close while the ringing overlay is up.
3. Overlay focus management: move keyboard focus into the ring overlay
when ringing starts (a11y pass).
- Priority: Medium — bundle with the next alarm-adjacent story or the
multi-clock epic to avoid a dedicated PR.

## Epic: Multi-clock

### US-004 — Wire up the multi-clock registry

- Status: Candidate — `clock-registry.ts` scaffold exists but is not wired.
- Source: README ("Multi-clock scaffold — not wired up yet"); recommended as
the next major epic after the Alarms epic.
- Dependencies: US-003 (shared FLIP helper) would reduce duplicated wiring.
a major epic after the Alarms epic.
- Dependencies: the shared FLIP helper (formerly US-003 item 1) is DONE via
US-005 (`shared/dom/flip.ts`) — the sequencing blocker is cleared.
- Note: likely requires an ADR if the page layout/composition root changes
meaningfully, plus a Product Owner story defining what a "clock" is.

Expand All @@ -51,3 +40,22 @@ sized/sequenced by the SDM.
the image tag in `.github/workflows/ci.yml` (nginx-config-check), and run
`nginx -t` locally on both configs.
- Priority: Medium — security updates are accruing on the EOL branch.

### HK-6 — Eliminate the stale-HTML / dead-asset window after deploys

- Status: Candidate (observed live 2026-09-13 ~22:00Z).
- Finding: after a deploy that changes asset hashes, the edge-cached homepage
(2 h Cache Rule TTL, per ADR 0006) references assets that no longer exist
on origin; requests 404, and **Cloudflare caches the 404 itself**
(`cf-cache-status: HIT` on a 404). Until the HTML entry expires, the
homepage is broken. ADR 0006's assumption that old hashed assets remain
edge-resident does not hold in practice.
- Fix candidates (pick one, or combine):
1. Purge the CF cache after a successful deploy (needs a CF API token —
scope decision + secret management).
2. Retain the previous build's `assets/` on origin in `scripts/deploy.sh`
so old hashes keep resolving (simplest, no CF dependency).
3. Shorten the HTML Edge TTL (e.g. 5-10 min) to shrink the window.
- Also consider making builds hash-deterministic for unchanged code.
- Priority: Medium-High — every deploy currently risks a broken homepage for
up to 2 h.
46 changes: 26 additions & 20 deletions tasks/sprint.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,39 @@ Sprint window: active (no fixed calendar sprint; pipeline-driven).

## In flight

### Housekeeping branch `chore/repo-housekeeping` (→ PR into `dev`)

- HK-1 README corrections, HK-2 Actions bump to Node-24 majors
(checkout@v7, setup-node@v7, buildx@v4, login@v4, build-push@v7,
hadolint-action@v3.5.0), HK-3 husky lint-staged guard, HK-4 memory bank
init, plus US-001 sprint close-out (this file).
- All committed on the branch; local `main` reset to `origin/main` after the
rebase-merge rewrote SHAs.
### US-005 — UX Polish & Accessibility Hardening

- Status: **Implementation complete on `feat/us-005-ux-polish`** — all 8 ACs
verified; awaiting owner QA + code review, then PR into `dev`.
- Delivered: visible labeled fields (alarm create redesign), semantic
WCAG-AA design tokens (all pairs computed ≥4.7:1), monochrome SVG icons
(red ⏰ / blue 💤 emojis removed), shared control scale across alarm +
timer panels, always-visible timer steppers (were hover-only), focus
management (into panels on open, restored to toggles on close), ring
overlay `aria-modal` + focus trap + outside-click guard (US-003 items 2–3
absorbed), shared FLIP helper with unit tests (US-003 item 1 absorbed),
`prefers-reduced-motion` support. 140 tests passing; 20/20 Playwright
a11y assertions; 14 before/after screenshots in the story folder.
- Note: alarm/timer/clock **pure cores untouched** — behavior tests pass
unchanged.

## Ready (not started)

Nothing. Next sprint content comes from the backlog once the owner picks the
next epic: US-002 (recurring alarms) vs US-004 (multi-clock).
After US-005 closes: owner picks the next epic — US-002 (recurring alarms,
deprioritized) vs US-004 (multi-clock, blocker cleared). HK-5/HK-6 are
standalone housekeeping candidates.

## Dependencies & sequencing

- US-002 can start any time — builds directly on the alarm core.
- US-003 (review follow-ups) is independent; best bundled with US-002 or
US-004.
- US-004 should wait for US-003 item 1 (shared FLIP helper) and needs a PO
story + possibly an ADR.
- HK-5 (nginx 1.27 re-pin, see backlog) is independent of the epics.
- US-004's sequencing blocker (shared FLIP helper) is resolved by US-005;
still needs a PO story + possibly an ADR.
- HK-5/HK-6 are independent of the epics.

## Risks

| Risk | Likelihood | Impact | Mitigation |
| ------------------------------------------------------- | ---------- | ----------------------- | --------------------------------- |
| Production alarm audio unverified | Medium | Medium (US-001 DoD gap) | Owner manual check; tracked above |
| nginx 1.27 base gets no security updates (EOL mainline) | High | Medium | HK-5: re-pin to current stable |
| Edge cache hides new deploys for up to 2 h | By design | Low | Judge freshness via `/healthz` |
| Risk | Likelihood | Impact | Mitigation |
| ----------------------------------------------------------------------------------------- | ---------- | --------------------------------------------- | -------------------------------------------- |
| Production alarm audio unverified | Medium | Medium (US-001 DoD gap) | Owner manual check; tracked above |
| Stale edge HTML references dead assets after each deploy; cached 404s extend the breakage | High | Medium (broken homepage up to 2 h per deploy) | HK-6: purge, asset retention, or shorter TTL |
| nginx 1.27 base gets no security updates (EOL mainline) | High | Medium | HK-5: re-pin to current stable |
Loading