docs(06): create ux-polish phase plan (6 plans, 2 waves)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-06-10 10:25:41 -04:00
co-authored by Claude Opus 4.8
parent d52acad54c
commit 456121969f
7 changed files with 1086 additions and 4 deletions
+143
View File
@@ -0,0 +1,143 @@
---
phase: 06-ux-polish
plan: 04
type: execute
wave: 1
depends_on: []
files_modified:
- apps/pwa/src/styles/tokens.css
- apps/pwa/src/components/PushPermissionPrompt.tsx
autonomous: false
requirements: []
must_haves:
truths:
- "Sync indicators actually animate — the SyncStateToast spinner spins and the LiveSyncIndicator reconnecting dot pulses (D-13)"
- "@keyframes pulse exists globally in tokens.css so LiveSyncIndicator's reconnecting dot animates regardless of which components are mounted (D-13)"
artifacts:
- path: "apps/pwa/src/styles/tokens.css"
provides: "global @keyframes pulse (added) alongside the existing @keyframes spin"
contains: "@keyframes pulse"
- path: "apps/pwa/src/components/PushPermissionPrompt.tsx"
provides: "redundant local @keyframes spin <style> block removed"
key_links:
- from: "apps/pwa/src/components/LiveSyncIndicator.tsx"
to: "apps/pwa/src/styles/tokens.css"
via: "animation: 'pulse 1.4s ease-in-out infinite' resolves to the global keyframe"
pattern: "@keyframes pulse"
---
<objective>
Make every sync indicator actually animate (D-13). RESEARCH corrected the original CONTEXT.md assumption: `@keyframes spin` is ALREADY global in `tokens.css` (lines 140147) and loads before any component mounts — so the spinner works. The real bugs are (1) `@keyframes pulse` is MISSING, so `LiveSyncIndicator`'s reconnecting dot (`animation: 'pulse 1.4s ease-in-out infinite'`) never animates, and (2) `PushPermissionPrompt.tsx` carries a redundant local `<style>` redefinition of `@keyframes spin` that should be removed for hygiene.
Purpose: Pure CSS/markup fix — no business logic, so a standard (non-TDD) plan. The animation presence is verified with `playwright-cli` (desktop Chromium) per the CLAUDE.md verification convention, with a grep gate confirming the keyframe is in the stylesheet.
Output: `@keyframes pulse` added to `tokens.css`; redundant `<style>` block removed from `PushPermissionPrompt.tsx`.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/06-ux-polish/06-RESEARCH.md
@.planning/phases/06-ux-polish/06-PATTERNS.md
@.planning/phases/06-ux-polish/06-UI-SPEC.md
</context>
<artifacts_this_plan_produces>
NEW symbols introduced here (exclude from drift/convergence checks):
- `@keyframes pulse` in `apps/pwa/src/styles/tokens.css` (0%,100% opacity:1 / 50% opacity:0.4)
- Removal of the redundant local `@keyframes spin` `<style>` block in `PushPermissionPrompt.tsx` (deletion, not a new symbol)
</artifacts_this_plan_produces>
<tasks>
<task type="auto">
<name>Task 1: Add @keyframes pulse globally; remove redundant spin redefinition</name>
<files>apps/pwa/src/styles/tokens.css, apps/pwa/src/components/PushPermissionPrompt.tsx</files>
<read_first>
- apps/pwa/src/styles/tokens.css — existing `@keyframes spin` (lines 140147) and `@keyframes shimmer` (~131138); match their format (no vendor prefixes, no `animation-fill-mode` inside the keyframe block)
- apps/pwa/src/components/PushPermissionPrompt.tsx — the redundant local `<style>{` @keyframes spin ... `}</style>` block (lines ~357363) to delete; the inline `animation: 'spin 1s linear infinite'` stays
- apps/pwa/src/components/LiveSyncIndicator.tsx — `animation: 'pulse 1.4s ease-in-out infinite'` (~line 69) — the consumer that needs the new keyframe
- apps/pwa/src/components/SyncStateToast.tsx — `animation: 'spin 1s linear infinite'` (~line 158) — already-working consumer, confirm unchanged
- .planning/phases/06-ux-polish/06-RESEARCH.md §"Focus 5" (the corrected diagnosis) + .planning/phases/06-ux-polish/06-UI-SPEC.md §"Animation Contract" (exact pulse keyframe)
</read_first>
<action>
In `tokens.css`, add a `@keyframes pulse` block (0%,100% opacity 1; 50% opacity 0.4) directly after the existing `@keyframes spin` block, matching the surrounding format. In `PushPermissionPrompt.tsx`, delete ONLY the redundant local `<style>` block that redefines `@keyframes spin` (lines ~357363) — leave the component's inline `animation: 'spin ...'` style and all other markup intact, since the global definition in `tokens.css` already covers it. Do NOT touch `LiveSyncIndicator.tsx` or `SyncStateToast.tsx` (their inline `animation` references are correct and now resolve to global keyframes). Commit: `fix(06-04): add global pulse keyframe and drop redundant spin redefinition`.
</action>
<verify>
<automated>grep -v '^#' apps/pwa/src/styles/tokens.css | grep -c '@keyframes pulse' | grep -qx 1 && ! grep -q '@keyframes spin' apps/pwa/src/components/PushPermissionPrompt.tsx && cd apps/pwa && pnpm test -- run 2>&1 | tail -3</automated>
</verify>
<acceptance_criteria>
- `@keyframes pulse` is present exactly once in tokens.css (grep gate passes).
- No `@keyframes spin` remains in PushPermissionPrompt.tsx (the redundant block is gone).
- The `@keyframes spin` block in tokens.css is unchanged; LiveSyncIndicator.tsx and SyncStateToast.tsx are unmodified.
- Existing PWA test suite still passes (no regression).
</acceptance_criteria>
<done>Global pulse keyframe added; redundant spin redefinition removed; PWA tests green.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: playwright-cli — confirm spinner spins and reconnecting dot pulses</name>
<files>(verification only — no files modified)</files>
<read_first>
- .claude/skills/playwright-cli/SKILL.md — how to drive desktop Chromium and observe computed styles / animation state
- docs/deployment.md §"Running locally (host-side, no Docker)" — the two-terminal dev run command (DEV_AUTH_BYPASS=true) to bring up the PWA for browser checks
- apps/pwa/src/components/SyncStateToast.tsx + apps/pwa/src/components/LiveSyncIndicator.tsx — how to trigger the syncing / reconnecting states
</read_first>
<action>
Verification task (no code changes). Using the playwright-cli skill against desktop Chromium: (1) start the dev stack host-side per docs/deployment.md with DEV_AUTH_BYPASS=true; (2) trigger a sync so SyncStateToast renders its Loader2 spinner and observe it rotating (computed `animationName === 'spin'`, transform changing over time); (3) force LiveSyncIndicator into the reconnecting state (drop the SSE connection) and observe the reconnecting dot's opacity pulsing (`animationName === 'pulse'`, not 'none'); (4) confirm PushPermissionPrompt's spinner still rotates after its local keyframe block was removed. Capture the observed `animationName` for both indicators in the summary. This is a blocking human-verify checkpoint — pause for the operator's confirmation.
</action>
<what-built>
Global `@keyframes pulse` in tokens.css and removal of the redundant local spin keyframe. Both animations now resolve from the global stylesheet for every consumer regardless of mount order.
</what-built>
<how-to-verify>
1. Start the dev stack host-side per docs/deployment.md (DEV_AUTH_BYPASS=true), then open the PWA in desktop Chromium via playwright-cli.
2. Trigger a sync state so SyncStateToast renders its Loader2 spinner; observe the spinner is visibly rotating (computed transform changes over time / animationName === 'spin').
3. Force the LiveSyncIndicator into the reconnecting state (e.g. drop the SSE connection) and observe the reconnecting dot's opacity pulsing (animationName === 'pulse', not 'none').
4. Confirm PushPermissionPrompt's spinner (if surfaced) still rotates after removing its local keyframe block.
</how-to-verify>
<verify>
<human-check>Spinner rotates and reconnecting dot pulses in desktop Chromium; both animationName values are non-'none'.</human-check>
</verify>
<resume-signal>Type "approved" or describe which indicator did not animate.</resume-signal>
<acceptance_criteria>
- SyncStateToast spinner shows a live rotation (animationName 'spin').
- LiveSyncIndicator reconnecting dot shows a live opacity pulse (animationName 'pulse').
- No console error about an undefined keyframe.
</acceptance_criteria>
<done>Both animations verified live in desktop Chromium via playwright-cli.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| (none new) | UI/CSS only. No data, no network, no input, no auth surface touched. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-06-04 | (n/a) | tokens.css keyframe + style-block deletion | accept | No new trust boundary — purely a CSS keyframe addition and removal of a redundant inline style. No input, no data flow, no auth path affected. |
</threat_model>
<verification>
- `grep -v '^#' apps/pwa/src/styles/tokens.css | grep -c '@keyframes pulse'` returns 1.
- `grep -q '@keyframes spin' apps/pwa/src/components/PushPermissionPrompt.tsx` returns nothing.
- playwright-cli confirms both animations run.
</verification>
<success_criteria>
- D-13: pulse keyframe present globally; reconnecting dot animates; spinner confirmed animating; redundant redefinition removed.
</success_criteria>
<output>
Create `.planning/phases/06-ux-polish/06-04-SUMMARY.md` when done (note the playwright-cli observation of both animations).
</output>