feat(frontend): stock-detail hero rework — MoS gauge sign-aware sweep · container-query desktop split · drop hero price line#332
Merged
Conversation
On the stock detail hero, move the Margin-of-Safety donut into the SAME row
as the Composite Score donut (mobile portrait) and give it the score gauge's
sweep + count-up motion — direction-aware on the sign:
- MoS >= 0 -> arc sweeps clockwise, the same direction as the score gauge
- MoS < 0 -> arc sweeps counter-clockwise, mirrored (overvalued "runs the
other way"). 329/502 of the current universe is negative, so the
counter-clockwise case is the common one, not an edge.
- frontend/components/MoSBadge.tsx -> client component; adds usePlayOnMount +
useCountUp + the gauge-sweep keyframe + --gauge-from (mirrors ScoreGauge).
Direction via -scale-x-100 on the gauge container when mos<0 (reflects the
rendered ring CW->CCW), centered number mirrored back so it stays readable.
Count-up handles negatives; hooks run before the null early-return.
- frontend/app/stock/[ticker]/page.tsx -> gauge pair flex-wrap -> grid-cols-2
so the two donuts share one row at every width; 1fr tracks bound each badge
so its label wraps within its track (no 320px flex-nowrap clip).
tsc --noEmit clean; next build 506 routes; ruff clean. Reduced-motion path
preserved via the shared hooks + globals.css guard.
https://claude.ai/code/session_0148EoMmL6zakDWqHXjqQ9yq
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…R) every width Spot-check follow-up: the two hero donuts were left-aligned in their grid tracks (score hugged the card's left edge, empty gap on the right). Center each donut inside its own 1fr half via `w-full grid-cols-2 justify-items-center` so the pair is symmetric about the card centerline with equal outer margins at EVERY breakpoint (`w-full` also overrides the parent's `xl:items-end` shrink for this row). Also documents the MoSBadge center-label `text-sm` divergence from ScoreGauge (the +/- sign char needs the room) per the frontend-design-reviewer WARN. tsc --noEmit clean; next build 506 routes. https://claude.ai/code/session_0148EoMmL6zakDWqHXjqQ9yq
Spot-check follow-up: justify-items-center spread each donut to its own half-center (too far apart). Keep grid-cols-2 (1fr tracks still bound each label's wrap so nothing clips at 320px) but pull the pair to the centerline -- score justify-self-end in the left track, MoS justify-self-start in the right -- so the two sit adjacent in the middle with symmetric outer margins. tsc --noEmit clean; next build 506 routes. https://claude.ai/code/session_0148EoMmL6zakDWqHXjqQ9yq
…etail Per spot-check: drop the <CurrentPriceLine> (live price + past-day change) from the hero, just below the company name. The hero now reads ticker → name → (score + MoS donuts), with current price still surfaced inside the price chart + fair-price sections below. Removed the now-unused import; `formatPrice` + `detail.current_price` stay (still used by Fair value / Target / chart / loss-chance). `CurrentPriceLine` component itself is kept — RankingTable still uses it. tsc --noEmit clean; next build 506 routes. https://claude.ai/code/session_0148EoMmL6zakDWqHXjqQ9yq
… block Per user direction: on desktop, lay out score · MoS · fair value · target · loss chance in the top-RIGHT, opposite the logo / ticker / company-name block on the left. The hero already had this 2-column split but gated it at xl (1280px), so common desktop widths (and any width with the sidebar expanded) still stacked it. Lower the split lg(1024)<-xl(1280): `xl:flex-row/items-start/items-end/max-w-2xl` -> `lg:`. Safe now because the ~360px live-price block was removed from the hero last commit, so the right stats block is narrower than when the xl gate was set (2026-05-29 responsive-density audit, which measured the price-carrying hero). Guards retained: left `min-w-0` (long name wraps, no chip-overflow onto gauges) + `lg:max-w-2xl` (no ultrawide spread); right `min-w-0` shrink guard. Comment documents the gotcha-override + the bump-back-to-xl escape hatch. tsc --noEmit clean; next build 506 routes. https://claude.ai/code/session_0148EoMmL6zakDWqHXjqQ9yq
Per user: on wide desktop the stats block should sit flush to the RIGHT edge, opposite the name block. Add `lg:justify-between` to the hero flex row — without it the stats block sat just after the `lg:max-w-2xl`-capped left block with trailing space on the right (>=1440px). Also corrects the comment: the removed live-price block lived in the LEFT column (under the name), not the right. tsc --noEmit clean; next build 506 routes. https://claude.ai/code/session_0148EoMmL6zakDWqHXjqQ9yq
The desktop "stats top-right opposite the name" split was gated on a VIEWPORT
breakpoint (lg/md), but the left sidebar (expanded 240px / collapsed 64px /
mobile-drawer 0) eats a viewport-variable slice — so there was a dead band
where the sidebar was already a desktop rail but the hero still stacked
(user-reported: stats not moving to the top-right on desktop).
Switch the hero to a CSS CONTAINER QUERY that measures the hero's OWN width
after the sidebar takes its cut:
- frontend/app/stock/[ticker]/page.tsx -> hero hooks `hero-card` / `hero-split`
/ `hero-left` / `hero-right`; the JSX default is the stacked `flex flex-col`
(mobile-portrait), the query only ADDS the row behaviour.
- frontend/app/globals.css -> `.hero-card { container: hero / inline-size }` +
`@container hero (min-width: 46rem)` flips to a `justify-between` row, caps the
left at max-w-2xl, right-aligns the stats column.
Result: stats sit top-right whenever the hero actually has room (any sidebar
state, any device), and when space is squeezed it falls back to the SAME
vertical mobile-portrait stack -- per the user's "if it's too squeezed, drop to
the mobile layout" direction. Native CSS @container (Chrome/Safari/Firefox
2023+); pre-2023 degrades to the safe stacked layout. No new dependency.
tsc --noEmit clean; next build 506 routes (@container compiled into built CSS).
https://claude.ai/code/session_0148EoMmL6zakDWqHXjqQ9yq
dackclup
added a commit
that referenced
this pull request
May 31, 2026
…t + sign-aware MoS) (#333) PR #332 (stock-detail hero rework) merged as a fast spot-check-driven UI iteration and skipped the CLAUDE.md/AGENTS.md substance lockstep. Add the two invariants future editors must not regress: 1. The hero's two-column (name-left / stats-top-right) vs stacked decision is driven by a CSS CONTAINER QUERY on the hero's own inline-size, NOT a viewport md:/lg: breakpoint — because the sidebar eats a viewport-variable width slice (the dead-band bug that prompted the rework). JSX default is the stacked flex-col; @container only adds the row; raw CSS, no plugin/dep. 2. The MoS gauge arc is sign-aware: >=0 clockwise (like the score gauge), <0 counter-clockwise via -scale-x-100 on the container with the number span mirrored back. 329/502 of the universe is negative, so CCW is the common case. CLAUDE.md §Gotchas holds the full rationale; AGENTS.md §Code style mirrors each as a pointer (PR #327 precedent). PHASE_STATUS_INFLIGHT.md entry per the ship-with-every-PR convention. Doc-only — the hero code already shipped in #332. https://claude.ai/code/session_0148EoMmL6zakDWqHXjqQ9yq Co-authored-by: Claude <noreply@anthropic.com>
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.
Summary
On the stock detail hero (mobile portrait), the Margin of Safety donut now sits in the same row as the Composite Score donut and gets the score gauge's sweep + count-up motion — direction-aware on the sign.
Direction (the careful part)
The MoS arc starts at 12 o'clock like the score gauge. Unlike the score (0–100, always one way), MoS is signed:
329 / 502 of the current universe is negative MoS, so the counter-clockwise case is the common one, not an edge.
Changes (2 files)
frontend/components/MoSBadge.tsx→ now a client component; addsusePlayOnMount+useCountUp+ thegauge-sweepkeyframe +--gauge-from(mirrorsScoreGauge.tsxexactly, so the two hero donuts share one motion family). Direction handled by-scale-x-100on the gauge container whenmos < 0(reflects the rendered ring CW→CCW — robust, no fragilerotate ∘ scalecomposition with the svg's internal-rotate-90); the centered number span is mirrored back so it stays readable. Count-up handles negatives (counts down); hooks run before the null early-return (Rules of Hooks). Accent stays emerald (≥0) / rose (<0).frontend/app/stock/[ticker]/page.tsx→ the gauge-pair containerflex flex-wrap→grid grid-cols-2so the two donuts share one row at every width. The1frtracks bound each badge so its label wraps within its track — this is what fixes the 320px clip the oldflex-nowraphad (EIX-style long "UNDERVALUED" label) without falling back to the verticalflex-wrapstack.Verification
tsc --noEmitnext buildruff check .useMotionhooks +globals.cssprefers-reduced-motionguardWhat this PR does NOT touch
compute// schema / scoring / valuation — frontend-only, no JSON shape change.MoSCell, and thesm/mdScoreBadgevariants are untouched.📱 Please spot-check the Vercel preview on device
/stock/CF(+15%)/stock/UPS(−7.9%) or/stock/AWK(−8.6%); deep/clamped/stock/TSLA(−1362% → full ring + "<−99")frontend-design-reviewer(sonnet) is reviewing the diff + responsive behavior in parallel; findings will land as follow-up commits.https://claude.ai/code/session_0148EoMmL6zakDWqHXjqQ9yq
Generated by Claude Code