From d69df5df27b3d9ca0473cffd72e232dce0514d71 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Wed, 10 Jun 2026 14:52:56 +0200 Subject: [PATCH] UI/UX redesign: unified refined-glass design system - Add shared UI primitives: Button, Field/Input/Select, PageHeader, Section, Callout, Tabs, Disclosure - Replace gradient buttons with single blue-accent btn-primary - Reserve gradient text for the brand wordmark only - Rework Scanner page onto the glass system; collapse explainer and glossary into a disclosure, move filters into a glass toolbar - Restructure Ticker Detail into tabs (Analysis / Indicators / S/R) with chart and recommendation always visible - Align Watchlist, Rankings, Admin, Login/Register to shared primitives - Unify stray indigo/violet/gray accents into the blue family Co-Authored-By: Claude Fable 5 --- frontend/src/components/admin/JobControls.tsx | 2 +- .../admin/RecommendationSettings.tsx | 2 +- .../src/components/admin/SettingsForm.tsx | 6 +- .../src/components/admin/TickerManagement.tsx | 2 +- .../admin/TickerUniverseBootstrap.tsx | 2 +- frontend/src/components/admin/UserTable.tsx | 4 +- .../src/components/rankings/WeightsForm.tsx | 4 +- .../src/components/scanner/TradeTable.tsx | 2 +- .../components/ticker/RecommendationPanel.tsx | 2 +- frontend/src/components/ui/Badge.tsx | 2 +- frontend/src/components/ui/Button.tsx | 54 +++++ frontend/src/components/ui/Callout.tsx | 33 ++++ frontend/src/components/ui/Disclosure.tsx | 19 ++ frontend/src/components/ui/Field.tsx | 31 +++ frontend/src/components/ui/PageHeader.tsx | 20 ++ frontend/src/components/ui/Section.tsx | 20 ++ frontend/src/components/ui/Tabs.tsx | 28 +++ .../components/watchlist/AddTickerForm.tsx | 2 +- frontend/src/pages/AdminPage.tsx | 24 +-- frontend/src/pages/LoginPage.tsx | 4 +- frontend/src/pages/RankingsPage.tsx | 44 ++--- frontend/src/pages/RegisterPage.tsx | 6 +- frontend/src/pages/ScannerPage.tsx | 116 +++++------ frontend/src/pages/TickerDetailPage.tsx | 186 +++++++++--------- frontend/src/pages/WatchlistPage.tsx | 23 +-- frontend/src/styles/globals.css | 40 +--- frontend/tsconfig.tsbuildinfo | 2 +- 27 files changed, 405 insertions(+), 275 deletions(-) create mode 100644 frontend/src/components/ui/Button.tsx create mode 100644 frontend/src/components/ui/Callout.tsx create mode 100644 frontend/src/components/ui/Disclosure.tsx create mode 100644 frontend/src/components/ui/Field.tsx create mode 100644 frontend/src/components/ui/PageHeader.tsx create mode 100644 frontend/src/components/ui/Section.tsx create mode 100644 frontend/src/components/ui/Tabs.tsx diff --git a/frontend/src/components/admin/JobControls.tsx b/frontend/src/components/admin/JobControls.tsx index 01a2440..959c4df 100644 --- a/frontend/src/components/admin/JobControls.tsx +++ b/frontend/src/components/admin/JobControls.tsx @@ -188,7 +188,7 @@ export function JobControls() { type="button" onClick={() => triggerJob.mutate(job.name)} disabled={triggerJob.isPending || !job.enabled || anyJobRunning} - className="btn-gradient px-3 py-1.5 text-xs disabled:opacity-50 disabled:cursor-not-allowed" + className="btn-primary px-3 py-1.5 text-xs disabled:opacity-50 disabled:cursor-not-allowed" > {job.running diff --git a/frontend/src/components/admin/RecommendationSettings.tsx b/frontend/src/components/admin/RecommendationSettings.tsx index ec557d1..65ebce3 100644 --- a/frontend/src/components/admin/RecommendationSettings.tsx +++ b/frontend/src/components/admin/RecommendationSettings.tsx @@ -89,7 +89,7 @@ export function RecommendationSettings() {
- diff --git a/frontend/src/components/admin/TickerManagement.tsx b/frontend/src/components/admin/TickerManagement.tsx index b4d5750..d8d319b 100644 --- a/frontend/src/components/admin/TickerManagement.tsx +++ b/frontend/src/components/admin/TickerManagement.tsx @@ -36,7 +36,7 @@ export function TickerManagement() { diff --git a/frontend/src/components/admin/TickerUniverseBootstrap.tsx b/frontend/src/components/admin/TickerUniverseBootstrap.tsx index 9838983..53de13c 100644 --- a/frontend/src/components/admin/TickerUniverseBootstrap.tsx +++ b/frontend/src/components/admin/TickerUniverseBootstrap.tsx @@ -78,7 +78,7 @@ export function TickerUniverseBootstrap() {
@@ -103,7 +103,7 @@ export function UserTable() { placeholder="new password" className="w-32 input-glass px-2 py-1 text-xs" /> diff --git a/frontend/src/components/scanner/TradeTable.tsx b/frontend/src/components/scanner/TradeTable.tsx index 3fa9765..95ee87f 100644 --- a/frontend/src/components/scanner/TradeTable.tsx +++ b/frontend/src/components/scanner/TradeTable.tsx @@ -104,7 +104,7 @@ export function TradeTable({ trades, sortColumn, sortDirection, onSort }: TradeT
- {recommendationActionLabel(trade.recommended_action)} + {recommendationActionLabel(trade.recommended_action)} {recommendationActionDirection(trade.recommended_action) !== 'neutral' && recommendationActionDirection(trade.recommended_action) !== trade.direction && (
Alternative setup (not preferred)
)} diff --git a/frontend/src/components/ticker/RecommendationPanel.tsx b/frontend/src/components/ticker/RecommendationPanel.tsx index 31bfc9c..c9db944 100644 --- a/frontend/src/components/ticker/RecommendationPanel.tsx +++ b/frontend/src/components/ticker/RecommendationPanel.tsx @@ -127,7 +127,7 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup }: Recommend

Recommendation

- {recommendationActionLabel(action)} + {recommendationActionLabel(action)} Risk: {summary?.risk_level ?? '—'} diff --git a/frontend/src/components/ui/Badge.tsx b/frontend/src/components/ui/Badge.tsx index 6618911..203b7e4 100644 --- a/frontend/src/components/ui/Badge.tsx +++ b/frontend/src/components/ui/Badge.tsx @@ -1,6 +1,6 @@ const variantStyles: Record = { auto: 'bg-blue-500/15 text-blue-400 border-blue-500/20', - manual: 'bg-violet-500/15 text-violet-400 border-violet-500/20', + manual: 'bg-sky-500/15 text-sky-400 border-sky-500/20', default: 'bg-white/[0.06] text-gray-400 border-white/[0.08]', }; diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx new file mode 100644 index 0000000..9233cb6 --- /dev/null +++ b/frontend/src/components/ui/Button.tsx @@ -0,0 +1,54 @@ +import type { ButtonHTMLAttributes, ReactNode } from 'react'; + +type Variant = 'primary' | 'ghost' | 'danger'; +type Size = 'sm' | 'md'; + +const variantClasses: Record = { + primary: 'btn-primary', + ghost: + 'border border-white/[0.08] bg-white/[0.03] text-gray-300 hover:bg-white/[0.07] hover:text-gray-100 rounded-lg', + danger: + 'border border-red-500/30 bg-red-500/10 text-red-400 hover:bg-red-500/20 rounded-lg', +}; + +const sizeClasses: Record = { + sm: 'px-3 py-1.5 text-xs', + md: 'px-4 py-2 text-sm', +}; + +export function Spinner({ className = 'h-4 w-4' }: { className?: string }) { + return ( + + ); +} + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: Variant; + size?: Size; + loading?: boolean; + children: ReactNode; +} + +export function Button({ + variant = 'primary', + size = 'md', + loading = false, + disabled, + className = '', + children, + ...rest +}: ButtonProps) { + return ( + + ); +} diff --git a/frontend/src/components/ui/Callout.tsx b/frontend/src/components/ui/Callout.tsx new file mode 100644 index 0000000..2fe45f4 --- /dev/null +++ b/frontend/src/components/ui/Callout.tsx @@ -0,0 +1,33 @@ +import type { ReactNode } from 'react'; + +type Variant = 'info' | 'warning' | 'error' | 'empty'; + +const variantClasses: Record = { + info: 'border-blue-500/20 bg-blue-500/10 text-blue-300', + warning: 'border-amber-500/20 bg-amber-500/10 text-amber-300', + error: 'border-red-500/20 bg-red-500/10 text-red-400', + empty: 'border-white/[0.06] bg-white/[0.02] text-gray-400 py-8 text-center', +}; + +interface CalloutProps { + variant?: Variant; + children: ReactNode; + onRetry?: () => void; +} + +/** Standard inline message box for info banners, errors, and empty states. */ +export function Callout({ variant = 'info', children, onRetry }: CalloutProps) { + return ( +
+ {children} + {onRetry && ( + + )} +
+ ); +} diff --git a/frontend/src/components/ui/Disclosure.tsx b/frontend/src/components/ui/Disclosure.tsx new file mode 100644 index 0000000..7d596e5 --- /dev/null +++ b/frontend/src/components/ui/Disclosure.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from 'react'; + +interface DisclosureProps { + summary: string; + children: ReactNode; +} + +/** Collapsible help/explainer block — keeps secondary content out of the way. */ +export function Disclosure({ summary, children }: DisclosureProps) { + return ( +
+ + + {summary} + +
{children}
+
+ ); +} diff --git a/frontend/src/components/ui/Field.tsx b/frontend/src/components/ui/Field.tsx new file mode 100644 index 0000000..9ca880e --- /dev/null +++ b/frontend/src/components/ui/Field.tsx @@ -0,0 +1,31 @@ +import type { InputHTMLAttributes, ReactNode, SelectHTMLAttributes } from 'react'; + +interface FieldProps { + label: string; + htmlFor?: string; + children: ReactNode; +} + +/** Labeled form control wrapper for filter bars and forms. */ +export function Field({ label, htmlFor, children }: FieldProps) { + return ( +
+ + {children} +
+ ); +} + +export function Input({ className = '', ...rest }: InputHTMLAttributes) { + return ; +} + +export function Select({ className = '', children, ...rest }: SelectHTMLAttributes) { + return ( + + ); +} diff --git a/frontend/src/components/ui/PageHeader.tsx b/frontend/src/components/ui/PageHeader.tsx new file mode 100644 index 0000000..b87e9da --- /dev/null +++ b/frontend/src/components/ui/PageHeader.tsx @@ -0,0 +1,20 @@ +import type { ReactNode } from 'react'; + +interface PageHeaderProps { + title: string; + subtitle?: string; + actions?: ReactNode; +} + +/** Standard page heading: plain title, muted subtitle, actions on the right. */ +export function PageHeader({ title, subtitle, actions }: PageHeaderProps) { + return ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {actions &&
{actions}
} +
+ ); +} diff --git a/frontend/src/components/ui/Section.tsx b/frontend/src/components/ui/Section.tsx new file mode 100644 index 0000000..ac02919 --- /dev/null +++ b/frontend/src/components/ui/Section.tsx @@ -0,0 +1,20 @@ +import type { ReactNode } from 'react'; + +interface SectionProps { + title: string; + hint?: string; + children: ReactNode; +} + +/** Content section with the standard uppercase tracking label. */ +export function Section({ title, hint, children }: SectionProps) { + return ( +
+

+ {title} + {hint && {hint}} +

+ {children} +
+ ); +} diff --git a/frontend/src/components/ui/Tabs.tsx b/frontend/src/components/ui/Tabs.tsx new file mode 100644 index 0000000..b77a563 --- /dev/null +++ b/frontend/src/components/ui/Tabs.tsx @@ -0,0 +1,28 @@ +interface TabsProps { + tabs: readonly T[]; + active: T; + onChange: (tab: T) => void; +} + +/** Glass pill tab bar. */ +export function Tabs({ tabs, active, onChange }: TabsProps) { + return ( +
+ {tabs.map((tab) => ( + + ))} +
+ ); +} diff --git a/frontend/src/components/watchlist/AddTickerForm.tsx b/frontend/src/components/watchlist/AddTickerForm.tsx index 13244f6..10adaa6 100644 --- a/frontend/src/components/watchlist/AddTickerForm.tsx +++ b/frontend/src/components/watchlist/AddTickerForm.tsx @@ -24,7 +24,7 @@ export function AddTickerForm() { diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index f83f0ab..75a26f7 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -7,6 +7,8 @@ import { SettingsForm } from '../components/admin/SettingsForm'; import { TickerManagement } from '../components/admin/TickerManagement'; import { TickerUniverseBootstrap } from '../components/admin/TickerUniverseBootstrap'; import { UserTable } from '../components/admin/UserTable'; +import { PageHeader } from '../components/ui/PageHeader'; +import { Tabs } from '../components/ui/Tabs'; const tabs = ['Users', 'Tickers', 'Settings', 'Jobs', 'Cleanup'] as const; type Tab = (typeof tabs)[number]; @@ -16,27 +18,9 @@ export default function AdminPage() { return (
-
-

Admin

-

System management

-
+ - {/* Tab bar */} -
- {tabs.map((tab) => ( - - ))} -
+ {/* Tab content */}
diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index aa850cc..1b699c7 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -25,7 +25,7 @@ export default function LoginPage() {
{/* Ambient glow orbs */}
-
+
@@ -73,7 +73,7 @@ export default function LoginPage() { diff --git a/frontend/src/pages/RankingsPage.tsx b/frontend/src/pages/RankingsPage.tsx index 941aaf6..ecfbcd2 100644 --- a/frontend/src/pages/RankingsPage.tsx +++ b/frontend/src/pages/RankingsPage.tsx @@ -1,41 +1,29 @@ import { useRankings } from '../hooks/useScores'; import { RankingsTable } from '../components/rankings/RankingsTable'; import { WeightsForm } from '../components/rankings/WeightsForm'; +import { Callout } from '../components/ui/Callout'; +import { PageHeader } from '../components/ui/PageHeader'; import { SkeletonTable } from '../components/ui/Skeleton'; export default function RankingsPage() { const { data, isLoading, isError, error } = useRankings(); - if (isLoading) { - return ( -
-

Rankings

- -
- ); - } - - if (isError) { - return ( -
-

Rankings

-

- Failed to load rankings: {(error as Error).message} -

-
- ); - } - - if (!data) return null; - return (
-
-

Rankings

-

Composite scoring leaderboard

-
- - + + + {isLoading && } + + {isError && ( + Failed to load rankings: {(error as Error).message} + )} + + {data && ( + <> + + + + )}
); } diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/RegisterPage.tsx index d87614d..f8e8409 100644 --- a/frontend/src/pages/RegisterPage.tsx +++ b/frontend/src/pages/RegisterPage.tsx @@ -40,7 +40,7 @@ export default function RegisterPage() {
Go to Login @@ -51,7 +51,7 @@ export default function RegisterPage() { return (
-
+
@@ -104,7 +104,7 @@ export default function RegisterPage() { diff --git a/frontend/src/pages/ScannerPage.tsx b/frontend/src/pages/ScannerPage.tsx index c1296da..b5e44dc 100644 --- a/frontend/src/pages/ScannerPage.tsx +++ b/frontend/src/pages/ScannerPage.tsx @@ -4,6 +4,11 @@ import { useTrades } from '../hooks/useTrades'; import { TradeTable, type SortColumn, type SortDirection, computeTradeAnalysis } from '../components/scanner/TradeTable'; import { SkeletonTable } from '../components/ui/Skeleton'; import { useToast } from '../components/ui/Toast'; +import { Button } from '../components/ui/Button'; +import { Callout } from '../components/ui/Callout'; +import { Disclosure } from '../components/ui/Disclosure'; +import { Field, Input, Select } from '../components/ui/Field'; +import { PageHeader } from '../components/ui/PageHeader'; import { triggerJob } from '../api/admin'; import type { TradeSetup } from '../lib/types'; import { RECOMMENDATION_ACTION_GLOSSARY, RECOMMENDATION_ACTION_LABELS } from '../lib/recommendation'; @@ -127,66 +132,46 @@ export default function ScannerPage() { }, [trades, minRR, directionFilter, minConfidence, actionFilter, sortColumn, sortDirection]); return ( -
-
-

Trade Scanner

- -
+
+ scanMutation.mutate()} loading={scanMutation.isPending}> + {scanMutation.isPending ? 'Scanning…' : 'Run Scanner'} + + } + /> - {/* Explainer banner */} -
- The scanner identifies asymmetric risk-reward trade setups by analyzing S/R levels - as price targets and using ATR-based stops to define risk. - Click Run Scanner to scan all tickers now, - or wait for the scheduled run. -
- - {/* Filter controls */} -
-
- + {/* Filter toolbar */} +
+
1 : - setMinRR(Number(e.target.value) || 0)} - className="w-20 rounded border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-200 focus:border-blue-500 focus:outline-none transition-colors duration-150" + className="w-20" />
-
-
- - setDirectionFilter(e.target.value as DirectionFilter)} - className="rounded border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-200 focus:border-blue-500 focus:outline-none transition-colors duration-150" > - -
-
- - + + + setMinConfidence(Number(e.target.value) || 0)} - className="w-24 rounded border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-200 focus:border-blue-500 focus:outline-none transition-colors duration-150" + className="w-24" /> -
-
- - setActionFilter(e.target.value as ActionFilter)} - className="rounded border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-200 focus:border-blue-500 focus:outline-none transition-colors duration-150" > - - - - - - -
+ + + + + + +
-
-

Recommended Action Glossary (Ticker-Level Bias)

+ +

+ The scanner identifies asymmetric risk-reward trade setups by analyzing S/R levels as + price targets and using ATR-based stops to define risk. Click{' '} + Run Scanner to scan all tickers now, + or wait for the scheduled run. +

{RECOMMENDATION_ACTION_GLOSSARY.map((item) => (

- {RECOMMENDATION_ACTION_LABELS[item.action]}:{' '} + {RECOMMENDATION_ACTION_LABELS[item.action]}:{' '} {item.description}

))}
-
+ {/* Content */} {isLoading && } {isError && ( -
+ {error instanceof Error ? error.message : 'Failed to load trade setups'} -
+ )} {trades && processed.length === 0 && !isLoading && ( -
+ No trade setups match the current filters. Try lowering the Min R:R or click Run Scanner to refresh. -
+ )} {trades && processed.length > 0 && ( diff --git a/frontend/src/pages/TickerDetailPage.tsx b/frontend/src/pages/TickerDetailPage.tsx index be7f400..46292cc 100644 --- a/frontend/src/pages/TickerDetailPage.tsx +++ b/frontend/src/pages/TickerDetailPage.tsx @@ -1,4 +1,4 @@ -import { useMemo, useEffect } from 'react'; +import { useMemo, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { useTickerDetail } from '../hooks/useTickerDetail'; import { useFetchSymbolData } from '../hooks/useFetchSymbolData'; @@ -9,19 +9,21 @@ import { SentimentPanel } from '../components/ticker/SentimentPanel'; import { FundamentalsPanel } from '../components/ticker/FundamentalsPanel'; import { IndicatorSelector } from '../components/ticker/IndicatorSelector'; import { RecommendationPanel } from '../components/ticker/RecommendationPanel'; +import { Button } from '../components/ui/Button'; +import { Callout } from '../components/ui/Callout'; +import { Section } from '../components/ui/Section'; +import { Tabs } from '../components/ui/Tabs'; import { formatPrice } from '../lib/format'; import type { TradeSetup } from '../lib/types'; +const detailTabs = ['Analysis', 'Indicators', 'S/R Levels'] as const; +type DetailTab = (typeof detailTabs)[number]; + function SectionError({ message, onRetry }: { message: string; onRetry?: () => void }) { return ( -
-

{message}

- {onRetry && ( - - )} -
+ + {message} + ); } @@ -67,6 +69,7 @@ export default function TickerDetailPage() { const { symbol = '' } = useParams<{ symbol: string }>(); const { ohlcv, scores, srLevels, sentiment, fundamentals, trades } = useTickerDetail(symbol); const ingestion = useFetchSymbolData(); + const [activeTab, setActiveTab] = useState('Analysis'); const dataStatus: DataStatusItem[] = useMemo(() => [ { @@ -134,24 +137,14 @@ export default function TickerDetailPage() { return (
{/* Header */} -
+
-

{symbol.toUpperCase()}

+

{symbol.toUpperCase()}

Ticker Detail

- +
{/* Data freshness bar */} @@ -159,9 +152,8 @@ export default function TickerDetailPage() { - {/* Chart Section */} -
-

Price Chart

+ {/* Chart — always visible */} +
{ohlcv.isLoading && } {ohlcv.isError && ( {srLevels.isError && ( -

S/R levels unavailable — chart shown without overlays

+

S/R levels unavailable — chart shown without overlays

)}
)} - + - {/* Scores + Side Panels */} -
-
-

Scores

- {scores.isLoading && } - {scores.isError && ( - scores.refetch()} /> - )} - {scores.data && ( - - )} -
+ {/* Detail tabs */} + -
-

Sentiment

- {sentiment.isLoading && } - {sentiment.isError && ( - sentiment.refetch()} /> - )} - {sentiment.data && } -
+ {activeTab === 'Analysis' && ( +
+
+ {scores.isLoading && } + {scores.isError && ( + scores.refetch()} /> + )} + {scores.data && ( + + )} +
-
-

Fundamentals

- {fundamentals.isLoading && } - {fundamentals.isError && ( - fundamentals.refetch()} /> - )} - {fundamentals.data && } -
-
+
+ {sentiment.isLoading && } + {sentiment.isError && ( + sentiment.refetch()} /> + )} + {sentiment.data && } +
- {/* Indicators */} -
-

Technical Indicators

- -
+
+ {fundamentals.isLoading && } + {fundamentals.isError && ( + fundamentals.refetch()} /> + )} + {fundamentals.data && } +
+
+ )} - {/* S/R Levels Table — sorted by strength */} - {sortedLevels.length > 0 && ( -
-

- Support & Resistance Levels - sorted by strength -

-
- - - - - - - - - - - {sortedLevels.map((level) => ( - - - - - - - ))} - -
TypePrice LevelStrengthMethod
- {level.type} - {formatPrice(level.price_level)}{level.strength}{level.detection_method}
-
-
+ {activeTab === 'Indicators' && ( +
+
+ +
+
+ )} + + {activeTab === 'S/R Levels' && ( +
+
+ {sortedLevels.length === 0 ? ( + No S/R levels detected for this ticker yet. + ) : ( +
+ + + + + + + + + + + {sortedLevels.map((level) => ( + + + + + + + ))} + +
TypePrice LevelStrengthMethod
+ {level.type} + {formatPrice(level.price_level)}{level.strength}{level.detection_method}
+
+ )} +
+
)}
); diff --git a/frontend/src/pages/WatchlistPage.tsx b/frontend/src/pages/WatchlistPage.tsx index 24212f8..1a2dcfe 100644 --- a/frontend/src/pages/WatchlistPage.tsx +++ b/frontend/src/pages/WatchlistPage.tsx @@ -2,6 +2,9 @@ import { useMemo, useState } from 'react'; import { useWatchlist } from '../hooks/useWatchlist'; import { WatchlistTable } from '../components/watchlist/WatchlistTable'; import { AddTickerForm } from '../components/watchlist/AddTickerForm'; +import { Callout } from '../components/ui/Callout'; +import { Select } from '../components/ui/Field'; +import { PageHeader } from '../components/ui/PageHeader'; import { SkeletonTable } from '../components/ui/Skeleton'; import type { WatchlistEntry } from '../lib/types'; @@ -50,37 +53,27 @@ export default function WatchlistPage() { return (
-
-
-

Watchlist

-

Track your favorite tickers

-
- -
+ } /> {isLoading && } - {isError && ( -
- {error?.message || 'Failed to load watchlist'} -
- )} + {isError && {error?.message || 'Failed to load watchlist'}} {data && (
diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index d6a696c..5c17663 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -54,48 +54,26 @@ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); } - /* Gradient text */ + /* Gradient text — reserved for the brand wordmark only */ .text-gradient { - background: linear-gradient(135deg, #38bdf8, #818cf8, #a78bfa); + background: linear-gradient(135deg, #38bdf8, #60a5fa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } - /* Gradient buttons */ - .btn-gradient { - background: linear-gradient(135deg, #3b82f6, #8b5cf6); + /* Primary button — single blue accent */ + .btn-primary { + background: rgba(59, 130, 246, 0.85); color: white; - border: none; + border: 1px solid rgba(96, 165, 250, 0.35); border-radius: 0.5rem; font-weight: 500; transition: all 0.2s ease; - position: relative; - overflow: hidden; } - .btn-gradient::before { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(135deg, #60a5fa, #a78bfa); - opacity: 0; - transition: opacity 0.2s ease; - } - .btn-gradient:hover::before { - opacity: 1; - } - .btn-gradient > * { - position: relative; - z-index: 1; - } - - /* Glow accent for active states */ - .glow-blue { - box-shadow: 0 0 20px rgba(59, 130, 246, 0.3), 0 0 60px rgba(59, 130, 246, 0.1); - } - - .glow-green { - box-shadow: 0 0 20px rgba(16, 185, 129, 0.3), 0 0 60px rgba(16, 185, 129, 0.1); + .btn-primary:hover:not(:disabled) { + background: rgb(59, 130, 246); + box-shadow: 0 0 16px rgba(59, 130, 246, 0.25); } /* Glass input */ diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 8297776..dd16988 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/fundamentals.ts","./src/api/health.ts","./src/api/indicators.ts","./src/api/ingestion.ts","./src/api/ohlcv.ts","./src/api/scores.ts","./src/api/sentiment.ts","./src/api/sr-levels.ts","./src/api/tickers.ts","./src/api/trades.ts","./src/api/watchlist.ts","./src/components/admin/datacleanup.tsx","./src/components/admin/jobcontrols.tsx","./src/components/admin/pipelinereadinesspanel.tsx","./src/components/admin/recommendationsettings.tsx","./src/components/admin/settingsform.tsx","./src/components/admin/tickermanagement.tsx","./src/components/admin/tickeruniversebootstrap.tsx","./src/components/admin/usertable.tsx","./src/components/auth/protectedroute.tsx","./src/components/charts/candlestickchart.tsx","./src/components/layout/appshell.tsx","./src/components/layout/mobilenav.tsx","./src/components/layout/sidebar.tsx","./src/components/rankings/rankingstable.tsx","./src/components/rankings/weightsform.tsx","./src/components/scanner/tradetable.tsx","./src/components/ticker/dimensionbreakdownpanel.tsx","./src/components/ticker/fundamentalspanel.tsx","./src/components/ticker/indicatorselector.tsx","./src/components/ticker/recommendationpanel.tsx","./src/components/ticker/sroverlay.tsx","./src/components/ticker/sentimentpanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/confirmdialog.tsx","./src/components/ui/scorecard.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/toast.tsx","./src/components/watchlist/addtickerform.tsx","./src/components/watchlist/watchlisttable.tsx","./src/hooks/useadmin.ts","./src/hooks/useauth.ts","./src/hooks/usefetchsymboldata.ts","./src/hooks/usescores.ts","./src/hooks/usetickerdetail.ts","./src/hooks/usetickers.ts","./src/hooks/usetrades.ts","./src/hooks/usewatchlist.ts","./src/lib/format.ts","./src/lib/ingestionstatus.ts","./src/lib/recommendation.ts","./src/lib/types.ts","./src/pages/adminpage.tsx","./src/pages/loginpage.tsx","./src/pages/rankingspage.tsx","./src/pages/registerpage.tsx","./src/pages/scannerpage.tsx","./src/pages/tickerdetailpage.tsx","./src/pages/watchlistpage.tsx","./src/stores/authstore.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/fundamentals.ts","./src/api/health.ts","./src/api/indicators.ts","./src/api/ingestion.ts","./src/api/ohlcv.ts","./src/api/scores.ts","./src/api/sentiment.ts","./src/api/sr-levels.ts","./src/api/tickers.ts","./src/api/trades.ts","./src/api/watchlist.ts","./src/components/admin/datacleanup.tsx","./src/components/admin/jobcontrols.tsx","./src/components/admin/pipelinereadinesspanel.tsx","./src/components/admin/recommendationsettings.tsx","./src/components/admin/settingsform.tsx","./src/components/admin/tickermanagement.tsx","./src/components/admin/tickeruniversebootstrap.tsx","./src/components/admin/usertable.tsx","./src/components/auth/protectedroute.tsx","./src/components/charts/candlestickchart.tsx","./src/components/layout/appshell.tsx","./src/components/layout/mobilenav.tsx","./src/components/layout/sidebar.tsx","./src/components/rankings/rankingstable.tsx","./src/components/rankings/weightsform.tsx","./src/components/scanner/tradetable.tsx","./src/components/ticker/dimensionbreakdownpanel.tsx","./src/components/ticker/fundamentalspanel.tsx","./src/components/ticker/indicatorselector.tsx","./src/components/ticker/recommendationpanel.tsx","./src/components/ticker/sroverlay.tsx","./src/components/ticker/sentimentpanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/callout.tsx","./src/components/ui/confirmdialog.tsx","./src/components/ui/disclosure.tsx","./src/components/ui/field.tsx","./src/components/ui/pageheader.tsx","./src/components/ui/scorecard.tsx","./src/components/ui/section.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/watchlist/addtickerform.tsx","./src/components/watchlist/watchlisttable.tsx","./src/hooks/useadmin.ts","./src/hooks/useauth.ts","./src/hooks/usefetchsymboldata.ts","./src/hooks/usescores.ts","./src/hooks/usetickerdetail.ts","./src/hooks/usetickers.ts","./src/hooks/usetrades.ts","./src/hooks/usewatchlist.ts","./src/lib/format.ts","./src/lib/ingestionstatus.ts","./src/lib/recommendation.ts","./src/lib/types.ts","./src/pages/adminpage.tsx","./src/pages/loginpage.tsx","./src/pages/rankingspage.tsx","./src/pages/registerpage.tsx","./src/pages/scannerpage.tsx","./src/pages/tickerdetailpage.tsx","./src/pages/watchlistpage.tsx","./src/stores/authstore.ts"],"version":"5.6.3"} \ No newline at end of file