Files
familysync/.planning/phases/06-ux-polish/06-04-PLAN.md
T

144 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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>