diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index b1fc6bb..bb1d565 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -12,6 +12,8 @@ --layout-max-width: 1400px; --layout-wide-max-width: 1520px; --chat-column-width: clamp(360px, 28vw, 420px); + --font-primary: var(--font-outfit), -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-mono: var(--font-mono), "Menlo", "Monaco", "Courier New", monospace; } /* ── Accessibility: sr-only ──────────────────────────────────── */ @@ -41,15 +43,7 @@ } body { - font-family: - var(--font-outfit), - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - Helvetica, - Arial, - sans-serif; + font-family: var(--font-primary); background-color: var(--bg-dark); background-image: radial-gradient( @@ -64,6 +58,49 @@ body { display: flex; flex-direction: column; overflow-x: hidden; + position: relative; +} + +body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient( + circle at 20% 60%, + rgba(139, 92, 246, 0.08), + transparent 35% + ), radial-gradient( + circle at 80% 20%, + rgba(6, 182, 212, 0.08), + transparent 35% + ); + animation: gradient-drift 20s ease-in-out infinite; + pointer-events: none; + z-index: -1; +} + +@media (prefers-reduced-motion: reduce) { + body::before { + animation: none; + } +} + +@keyframes gradient-drift { + 0% { + opacity: 0.6; + transform: translateY(0px); + } + 50% { + opacity: 0.8; + transform: translateY(-20px); + } + 100% { + opacity: 0.6; + transform: translateY(0px); + } } /* Dashboard Header */ @@ -912,6 +949,7 @@ h2 { font-size: 2.25rem; font-weight: 700; font-variant-numeric: tabular-nums; + font-family: var(--font-mono); display: flex; align-items: baseline; gap: 0.5rem; @@ -1628,7 +1666,7 @@ h2 { .status-indicator { display: inline-flex; align-items: center; - gap: 6px; + gap: 8px; padding: 6px 12px; border-radius: 20px; font-size: 0.85rem; @@ -1636,24 +1674,43 @@ h2 { margin-top: 8px; } +.status-indicator-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + .status-indicator.falling-behind { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.3); } +.status-indicator.falling-behind .status-indicator-dot { + background: #ef4444; +} + .status-indicator.on-track { background: rgba(34, 197, 94, 0.1); color: #22c55e; border: 1px solid rgba(34, 197, 94, 0.3); } +.status-indicator.on-track .status-indicator-dot { + background: #22c55e; +} + .status-indicator.ahead { background: rgba(59, 130, 246, 0.1); color: #3b82f6; border: 1px solid rgba(59, 130, 246, 0.3); } +.status-indicator.ahead .status-indicator-dot { + background: #3b82f6; +} + /* ── Skeleton loaders ────────────────────────────────────────── */ .skeleton-bone { background: rgba(255, 255, 255, 0.03); @@ -2013,12 +2070,25 @@ h2 { .goal-title { font-size: 1.25rem; margin-bottom: 4px; + font-weight: 600; } .goal-subtitle { font-size: 0.9rem; } +.goal-metric { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-weight: 600; + color: var(--text-main); +} + +.goal-header-content { + min-width: 0; + flex: 1; +} + .goal-status-text { font-size: 0.85rem; font-weight: 600; @@ -2028,11 +2098,30 @@ h2 { .progress-stats { display: flex; justify-content: space-between; + gap: 1.5rem; font-size: 0.85rem; - color: var(--text-muted); font-weight: 500; } +.progress-stat-label { + display: block; + color: var(--text-muted); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.25rem; + font-weight: 600; +} + +.progress-stat-value { + display: block; + color: var(--text-main); + font-size: 1.1rem; + font-weight: 700; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} + .allocation-title { display: flex; align-items: center; diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 0862dcd..fdff06e 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next'; -import { Outfit } from 'next/font/google'; +import { Outfit, JetBrains_Mono } from 'next/font/google'; import './globals.css'; import { WalletProvider } from '../components/features/wallet/WalletContext'; import { ErrorBoundary } from '../components/feedback/ErrorBoundary'; @@ -12,6 +12,13 @@ const outfit = Outfit({ variable: '--font-outfit', }); +const jetbrainsMono = JetBrains_Mono({ + subsets: ['latin'], + weight: ['400', '500', '600', '700'], + display: 'swap', + variable: '--font-mono', +}); + export const metadata: Metadata = { title: 'Smasage | AI Portfolio Manager', description: 'AI Portfolio Manager natively on Stellar', @@ -23,7 +30,7 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + diff --git a/frontend/src/components/features/portfolio/GoalTracker.tsx b/frontend/src/components/features/portfolio/GoalTracker.tsx index 9673cee..33aa79f 100644 --- a/frontend/src/components/features/portfolio/GoalTracker.tsx +++ b/frontend/src/components/features/portfolio/GoalTracker.tsx @@ -23,6 +23,19 @@ function getStatusClass(status: ProjectionResult["status"]): string { } } +function getStatusLabel(status: ProjectionResult["status"]): string { + switch (status) { + case "Ahead": + return "Ahead of schedule"; + case "On Track": + return "On track"; + case "Falling Behind": + return "Falling behind"; + default: + return "On track"; + } +} + function formatCurrency(value: number): string { return new Intl.NumberFormat("en-US", { style: "currency", @@ -53,20 +66,22 @@ export function GoalTracker({ }: GoalTrackerProps) { const statusColor = getStatusColor(status); const clampedProgress = Math.max(0, Math.min(100, progressPercentage)); + const statusLabel = getStatusLabel(status); return (
-
+

{goalName}

- Target: {formatCurrency(targetAmount)} by {formatTargetDate(targetDate)} + Target: {formatCurrency(targetAmount)} by

-
- Status: {status} +
+ + {statusLabel}
- +
@@ -77,13 +92,19 @@ export function GoalTracker({ aria-valuenow={Math.round(clampedProgress)} aria-valuemin={0} aria-valuemax={100} - aria-label="Savings goal progress" + aria-label={`Savings goal progress: ${Math.round(clampedProgress)}% complete`} >
- {Math.round(clampedProgress)}% Completed - {formatCurrency(remainingAmount)} Remaining +
+ Completed + {Math.round(clampedProgress)}% +
+
+ Remaining + {formatCurrency(remainingAmount)} +
); diff --git a/frontend/src/components/features/portfolio/PortfolioStats.tsx b/frontend/src/components/features/portfolio/PortfolioStats.tsx index d780388..9108529 100644 --- a/frontend/src/components/features/portfolio/PortfolioStats.tsx +++ b/frontend/src/components/features/portfolio/PortfolioStats.tsx @@ -9,41 +9,48 @@ export interface PortfolioStatsProps { valueChange: number; } +const formatMetric = (value: number, style: 'currency' | 'percent'): string => { + if (style === 'currency') { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); + } + return value.toFixed(1) + '%'; +}; + export const PortfolioStats: React.FC = ({ totalValue, apy, valueChange, }) => { - const formattedValue = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(totalValue); - + const formattedValue = formatMetric(totalValue, 'currency'); const changeLabel = `${valueChange >= 0 ? '+' : ''}${valueChange.toFixed(1)}%`; + const apyLabel = formatMetric(apy, 'percent'); return (
- - Total Value +
- {formattedValue} - {changeLabel} + {formattedValue} + {changeLabel}
- - Est. Monthly APY +
- {apy.toFixed(1)}% - Active + {apyLabel} + Active