144 lines
9.3 KiB
Markdown
144 lines
9.3 KiB
Markdown
---
|
||
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 140–147) 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 140–147) and `@keyframes shimmer` (~131–138); 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 ~357–363) 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 ~357–363) — 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>
|