feat(frontend): app-wide tasteful motion — gauge sweep · row stagger · veto pulse#312
Merged
Conversation
Tasteful-motion program, commit 1 of 3 (grill-confirmed spec: tasteful not playful · CSS/Tailwind only, no framer-motion · whole-app · play-once-per- session · signature = score gauge sweep). LedgerCraft stays flat; motion is the ENTRANCE, not a permanent flourish. Foundation: - tailwind.config.ts: rise-in / chip-pop / flag-pulse keyframes + animation tokens (transform+opacity only; compositor-friendly; ≤320ms micro-budget). - globals.css: keyframe bodies + stagger-1..12 delay utils + .gauge-arc (800ms stroke-dashoffset ease) + .hover-lift + EXTENDED prefers-reduced- motion guard covering every new util (snaps to static end-state). - lib/useMotion.ts: useCountUp (rAF easeOutCubic, inits at TARGET so SSR/ no-JS/reduced-motion render the correct number — count-up is progressive enhancement only) · useInViewOnce (IntersectionObserver, fires once) · usePlayedOnce (sessionStorage — entrances play once per session). Signature moment: - ScoreGauge.tsx (new client component): the composite-score radial gauge on the detail header sweeps 0→value (arc + count-up in sync over 800ms) on first view this session. Two-phase double-rAF render gives the CSS transition two paints to ease between. - ScoreBadge.tsx: 'lg' branch delegates to ScoreGauge; 'sm'/'md' stay server-rendered (no client JS across the 502 table cells). Verified via headless Playwright: arc sweeps (42.7→163.4→ease→42.7 via MutationObserver), count-up 0→73.9, SSR prerender shows real number (not 0.0), reduced-motion renders final immediately. next build clean (505 routes). ruff check . clean (whole repo). https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
Tasteful-motion commit 2 of 3 — the home page. - RankingTable.tsx: rows (desktop <tr> + mobile cards) cascade in with animate-rise-in + stagger-1..12 on the first home view this session, gated to unfiltered page 1 so sort/filter/paginate never re-trigger the cascade (would feel sluggish on every interaction). Uses effect-based usePlayedOnce so the animate class is added CLIENT-SIDE — never baked into the static prerender (which would replay on every full load + cause a hydration mismatch leaving rows stuck mid-fade; caught + fixed via Playwright before commit). - useMotion.ts: removed the unused usePlayOnceSync variant (the SSR-baked approach it enabled is wrong for static export — documented why on usePlayedOnce). - docs/design.md: new ## Motion section — token table + 5 non-negotiable rules (transform/opacity-only · play-once-per-session · reduced-motion mandatory · never gate content on JS · add-classes-client-side-for- static-export) + the signature gauge note. Locks the vocabulary for docs-reviewer. Verified via headless Playwright: first-view cascades + all rows end opacity 1; return-visit (client nav) suppressed, opacity 1 instantly; static HTML carries 0 baked animate classes; reduced-motion static. next build clean (505 routes). ruff check . clean. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
Tasteful-motion commit 3 of 3 — the stock detail page.
- RiskFlagsCard.tsx: veto rows enter with animate-flag-pulse (rise + one
rose attention-ring pulse, single iteration — "look here" without a
permanent blink), staggered so a multi-veto stock (SMCI: 3) cascades.
Gated client-side via usePlayedOnce keyed by the flag set (once per
session per distinct stock); hook hoisted above the early return per
rules-of-hooks.
Deliberately NOT changed:
- PriceHistoryChart isAnimationActive={false} stays OFF — that chart
re-renders on every period toggle (1M/3M/1Y…); enabling animation would
re-sweep the price line on every click (the replay-on-interaction
anti-pattern). Correct as-is.
- FairPriceBarChart + PillarRadarChart have no interactive state and no
explicit flag → Recharts already animates them on mount (draw-in) for
free. No change needed.
Verified via headless Playwright: EIX veto row pulses + ends opacity 1;
static HTML bakes 0 pulse classes (client-only); 17 chart SVGs render;
reduced-motion neutralizes the pulse (opacity 1). next build clean (505
routes). ruff check . clean.
https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…+ hover-lift frontend-design-reviewer (opus) flagged a self-inflicted contradiction: I wrote Motion Rule 1 as "transform + opacity ONLY" then animated box-shadow in flag-pulse (78%/100% stops) and transitioned box-shadow in hover-lift. Rather than carve a box-shadow exception into the rule, honor the rule: - flag-pulse: box-shadow ring stops → a tiny scale settle (0.99→1.012→1) paired with the existing rise. transform+opacity only. The rose tone the veto row already carries supplies the "look here" color; the keyframe supplies the motion. Verified still animates (tight-poll caught animationName + class from first frame; ends opacity 1). - hover-lift: transition drops `box-shadow 160ms` → `transform` only (the row's slate hover-bg already carries the hover state). Was defined but not yet consumed, so fixed pre-emptively before any consumer ships. Also removes the only raw rgb() the reviewer WARNed on (it lived in the deleted box-shadow stops). chip-pop + hover-lift remain declared-but-not- yet-consumed vocabulary tokens (documented in docs/design.md §Motion table, same pattern as the 4-tier shadow tokens that predated their consumers). next build clean (506 routes). ruff check . clean (whole repo). https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
…er-explorer)
expert-user-explorer experiential pass verdict: motion achieves "fun but
trustworthy" (Stripe/Linear register, 0 looping anims, not toy-like). Two
MINOR findings fixed:
1. Score-gauge sweep was gated by usePlayedOnce(`score-gauge:${score}`) —
but ~269 unique scores cover 502 stocks, so 46% of tickers shared a
session key and SILENTLY SKIPPED the signature sweep on a comparison run
(verified: AIZ + QCOM both score 66.5 → QCOM never swept). Now keyed per
TICKER (threaded score-badge → gauge): both same-score stocks sweep on
first view, neither re-sweeps on revisit. 502 keys, contract preserved.
2. CLS 0.026 on the detail page: the count-up number's column reflowed as
digit count changed (0.0 → 73.9). Added min-w-[3.5rem] to the text
column to reserve the widest-value width. CLS 0.026 → 0.0214 (the
residual is the client-island hydration settle — comfortably under the
0.1 "good" threshold; a Suspense-fallback further-fix was judged not
worth the complexity for 0.02).
Verified via headless Playwright (same-tab session): AIZ swept / QCOM swept
(same score) / AIZ no re-sweep / CLS 0.0214. next build clean (506 routes).
ruff check . clean.
https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
§Conventions code-PR lockstep for the motion feature: CLAUDE.md §Phase status in-flight entry + AGENTS.md §Phase+version mirror + PHASE_STATUS_ INFLIGHT.md full entry. (docs/design.md §Motion already landed in commit 2/3.) Frontend-only feature; no schema/compute/scoring change. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
dackclup
added a commit
that referenced
this pull request
May 29, 2026
…ame sweep (#313) Post-merge animation audit (2-agent: frontend-design-reviewer code + expert-user-explorer live Playwright) + the user's "play every time" direction, in one PR: 1. Audit fixes (vs #312): root-caused the "stagger replays on sort" to StockLogo's inline fade-in restarting on row-move (removed it + interacted latch); closed the reduced-motion gap in the play gate; removed dead useInViewOnce; wired the declared-but-dead hover-lift + chip-pop tokens; fixed design.md drift. 2. Play every visit: usePlayedOnce (once/session) → usePlayOnMount (every mount) — gauge re-sweeps each stock visit, home re-staggers on return; sort/filter still don't re-stagger (interacted latch); reduced-motion still static. 3. BLOCKER fix: the signature gauge arc sweep was INVISIBLE (transition+double-rAF batched away by Chromium — caught by the audit's animation-event probe, which my earlier attribute-based checks had missed). Rewrote as a CSS @Keyframes gauge-sweep added via class — verified firing on visit + revisit (getAnimations poll + 58%→74% screenshots), reduced-motion safe. Every animation verified actually animating via headless Playwright (animation events / getAnimations, not just attribute mutations). next build clean (505 routes), tsc + ruff clean. Frontend-only, no schema/compute change. https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
dackclup
added a commit
that referenced
this pull request
May 30, 2026
…#326 §Gotchas (#327) Full docs-reviewer substance pass on CLAUDE.md → 5 MUST-FIX + 4 SHOULD-FIX, all fixed. CLAUDE.md only (+ PHASE_STATUS_INFLIGHT.md lockstep entry); no code/schema/compute/frontend change. MUST-FIX: §Phase status "In flight" marked PR #312 (tasteful-motion) as "THIS PR" → merged (e602485); "Recently merged" frozen at #310 → drained #311–#326 (16 entries, SHAs verified); "Next deliverables" Issue #67 flip listed pending → DONE (PR #294, USE_SECTOR_COE=True); "Section A-H/A-J" → "A-L" (helper is A-L since PR #221). SHOULD-FIX: §Stack "Phase 3b on this PR" → "(merged)" + edgartools 5.31→5.32; §Gotchas compute/main.py line refs re-anchored (840→879 · 1965-66→2084-85 · 717→728 · 785→805 · 972→1025); +2 §Gotchas for PR #326 invariants (sidebar data-rail↔globals.css pre-paint lockstep; AreaChart re-park debounce ≥300ms). Confirmed no drift: schema 0.10.11-phase4.6 · skills 46 · agents 19 · hooks 3. docs-reviewer re-check: DOCS-CLEAN (all 16 SHAs match, 5 line refs accurate, 2 gotchas code-backed). Lockstep via PHASE_STATUS_INFLIGHT.md side-file (PR #237).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
App-wide tasteful motion — entrance + micro-interaction animation that makes QuantRank feel alive and rewarding without undermining the trust a finance tool needs. Fulfils the request "ทำ animation ให้...ดูสนุกสนาน น่าตื่นเต้น ดูแล้วไม่น่าเบื่อ."
Spec resolved via a 5-question grill:
The motion
0→scorewith a synchronized count-up over 800ms on first view (keyed per-ticker). The app's headline number earns the one longer beat.The 5 non-negotiable rules (now locked in
docs/design.md§Motion)usePlayedOnce/sessionStorage).prefers-reduced-motionmandatory — every token snaps to the static end-state.useCountUpinits at the target so SSR/no-JS show the correct number, never a stuck0.0.Files (9, +503/−50, frontend-only)
tailwind.config.ts·app/globals.css·lib/useMotion.ts(new hooks) ·ScoreGauge.tsx(new) ·ScoreBadge.tsx·RankingTable.tsx·RiskFlagsCard.tsx·app/stock/[ticker]/page.tsx·docs/design.md. No schema / compute / scoring change.Review gates (both passed)
flag-pulse/hover-lift) — fixed by honoring the rule.Verification
Every animation verified actually animating via headless Playwright (not just "build passes") — gauge sweep, row stagger, veto pulse, per-ticker gate, reduced-motion static + correct data, CLS measured.
next buildclean (506 routes) ·tscclean ·ruff check .clean.🤖 Built with the agent team: grill-me spec → inline implement → frontend-design-reviewer → expert-user-explorer → fixes.
https://claude.ai/code/session_0144kHrCYNaamMPH57b7xdM7
Generated by Claude Code