From f9e12fc78cfcfb1face01e83fb5997f1eec0b1ae Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Fri, 10 Jul 2026 07:59:13 +0200 Subject: [PATCH] Ticker page: mockup drill-down layout Restructures TickerDetailPage to match the Horizon mockup's ticker drill-down while keeping every existing feature: - Glass header card: eyebrow + big symbol/company, price with change and freshness, chips (composite score, next earnings, top-pick / open-trade pills) on the left; watchlist + Fetch All actions and the score fingerprint radar on the right (hover a corner for values). - Data freshness bar (per-source age + refresh buttons) moves to the foot of the chart card, like the mockup's panel footer. - Tabs extended to five: Analysis, Indicators, S/R Levels, Sentiment, Fundamentals - sentiment and fundamentals no longer stack below the analysis content, they are one tab away. - ScoreCard gains showRadar prop so the fingerprint is not duplicated in the Dimensions section. Co-Authored-By: Claude Fable 5 --- frontend/src/components/ui/ScoreCard.tsx | 8 +- frontend/src/pages/TickerDetailPage.tsx | 170 ++++++++++++++--------- 2 files changed, 110 insertions(+), 68 deletions(-) diff --git a/frontend/src/components/ui/ScoreCard.tsx b/frontend/src/components/ui/ScoreCard.tsx index e02719a..a932b9a 100644 --- a/frontend/src/components/ui/ScoreCard.tsx +++ b/frontend/src/components/ui/ScoreCard.tsx @@ -10,6 +10,8 @@ interface ScoreCardProps { /** Hide the composite ring/header when the composite is shown elsewhere * (e.g. the Standing matrix) and this card only carries the dimension detail. */ showComposite?: boolean; + /** Hide the radar fingerprint when the page shows it elsewhere (ticker header). */ + showRadar?: boolean; } function scoreColor(score: number): string { @@ -55,7 +57,7 @@ function ScoreRing({ score }: { score: number }) { ); } -export function ScoreCard({ compositeScore, dimensions, compositeBreakdown, showComposite = true }: ScoreCardProps) { +export function ScoreCard({ compositeScore, dimensions, compositeBreakdown, showComposite = true, showRadar = true }: ScoreCardProps) { const [expanded, setExpanded] = useState>({}); const toggleExpand = (dimension: string) => { @@ -64,7 +66,7 @@ export function ScoreCard({ compositeScore, dimensions, compositeBreakdown, show return (
- {(showComposite || dimensions.length >= 3) && ( + {(showComposite || (showRadar && dimensions.length >= 3)) && (
{showComposite && (compositeScore !== null ? ( @@ -90,7 +92,7 @@ export function ScoreCard({ compositeScore, dimensions, compositeBreakdown, show )}
)} - {dimensions.length >= 3 && ( + {showRadar && dimensions.length >= 3 && (
import('../components/ticker/StandingMatrix')); -const detailTabs = ['Analysis', 'Indicators', 'S/R Levels'] as const; +const detailTabs = ['Analysis', 'Indicators', 'S/R Levels', 'Sentiment', 'Fundamentals'] as const; type DetailTab = (typeof detailTabs)[number]; function SectionError({ message, onRetry }: { message: string; onRetry?: () => void }) { @@ -272,70 +273,94 @@ export default function TickerDetailPage() { return (
- {/* Header */} -
-
-

{symbol.toUpperCase()}

- {companyName && ( - {companyName} - )} - {priceInfo && ( -
- {formatPrice(priceInfo.price)} - {priceInfo.change !== null && ( - = 0 ? 'text-emerald-400' : 'text-red-400'}`}> - {priceInfo.change >= 0 ? '+' : ''}{priceInfo.change.toFixed(2)}% + {/* Drill-down header — identity + chips left, score fingerprint right */} +
+
+
+

Ticker

+
+

+ {symbol.toUpperCase()} +

+ {companyName && ( + {companyName} + )} +
+ {priceInfo && ( +
+ {formatPrice(priceInfo.price)} + {priceInfo.change !== null && ( + = 0 ? 'text-emerald-300' : 'text-red-300'}`}> + {priceInfo.change >= 0 ? '+' : ''}{priceInfo.change.toFixed(2)}% + + )} + last close · {timeAgo(priceInfo.date)} +
+ )} +
+ {scores.data?.composite_score != null && ( + + composite score {Math.round(scores.data.composite_score)} )} - last close · {timeAgo(priceInfo.date)} + {fundamentals.data?.next_earnings_date && ( + + earnings {new Date(fundamentals.data.next_earnings_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} + + )} + {isTopPick && ( + + )} + {hasOpenTrade && ( + + )}
- )} -
-
- {isTopPick && ( - - )} - {hasOpenTrade && ( - - )} - - -
-
+
- {/* Data freshness bar */} - +
+
+ + +
+ {scores.data && scores.data.dimensions.length >= 3 && ( + ({ + label: d.dimension.length > 9 ? `${d.dimension.slice(0, 8)}.` : d.dimension, + full: d.dimension, + value: d.score, + }))} + size={112} + /> + )} +
+
+ + {/* Data freshness — per-source age + refresh, at the panel foot like the mockup */} +
+ +
)} @@ -444,17 +478,20 @@ export default function TickerDetailPage() { )} -
-
+
{scores.isLoading && } {scores.isError && ( scores.refetch()} /> )} {scores.data && ( - + )}
+
+ )} + {activeTab === 'Sentiment' && ( +
{sentiment.isLoading && } {sentiment.isError && ( @@ -462,7 +499,11 @@ export default function TickerDetailPage() { )} {sentiment.data && }
+
+ )} + {activeTab === 'Fundamentals' && ( +
{fundamentals.isLoading && } {fundamentals.isError && ( @@ -470,7 +511,6 @@ export default function TickerDetailPage() { )} {fundamentals.data && }
-
)}