diff --git a/features/status-dashboard/frontend/src/AdminDashboard.tsx b/features/status-dashboard/frontend/src/AdminDashboard.tsx index 49ce7031d..4b9cf1159 100644 --- a/features/status-dashboard/frontend/src/AdminDashboard.tsx +++ b/features/status-dashboard/frontend/src/AdminDashboard.tsx @@ -1,9 +1,24 @@ -import { Link } from 'react-router-dom'; +import { Badge } from '@lilith/ui-primitives'; import { useHealthMonitor } from '@/hooks/useHealthMonitor'; import { StatusBadge } from '@/components/StatusBadge'; import { ResourceCard } from '@/components/ResourceCard'; import { ContainerList } from '@/components/ContainerList'; import { ThemeSwitcher } from '@/components/ThemeSwitcher'; +import { + PageContainer, + Header, + HeaderContent, + HeaderActions, + MainContent, + PageTitle, + PageSubtitle, + SectionTitle, + Section, + Grid, + Footer, + LoadingState, + ErrorState, +} from '@/components/layouts'; import * as S from '@/components/AdminDashboard.styles'; // Map backend status to UI status @@ -15,73 +30,44 @@ export function AdminDashboard() { const { status, vpsResources, containers, connected, loading, error } = useHealthMonitor(); if (loading) { - return ( - - - - Loading status... - - - ); + return ; } if (error) { - return ( - - -

Connection Error

-

{error}

-
-
- ); + return ; } return ( - - {/* Header */} - - + +
+ - + Lilith Platform Status - Admin - - + Admin + + Real-time monitoring • Last updated: {new Date().toLocaleTimeString()} - + - + {status && } {connected ? 'Live' : 'Disconnected'} - - All Hosts → - + All Hosts → - + - - + +
- {/* Main Content */} - - {/* System Resources */} - - System Resources + +
+ System Resources {vpsResources ? ( - + - + ) : ( Loading resources... )} - +
- {/* Platform Summary */} {status && ( - +
Platform Summary @@ -143,19 +128,17 @@ export function AdminDashboard() { {status.message} )} - +
)} - {/* Container List */} - +
- - +
+
- {/* Footer */} - +

Powered by Lilith Platform Health Monitor • Updates every 5 seconds

- - +
+
); } diff --git a/features/status-dashboard/frontend/src/HostsPage.tsx b/features/status-dashboard/frontend/src/HostsPage.tsx index d35dc18c1..a37f422e0 100644 --- a/features/status-dashboard/frontend/src/HostsPage.tsx +++ b/features/status-dashboard/frontend/src/HostsPage.tsx @@ -1,5 +1,20 @@ import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; +import { Card, Badge, StatusBadge } from '@lilith/ui-primitives'; +import { ThemeSwitcher } from './components/ThemeSwitcher'; +import { + PageContainer, + Header, + HeaderContent, + HeaderActions, + MainContent, + PageTitle, + PageSubtitle, + Grid, + LoadingState, + ErrorState, +} from './components/layouts'; +import styled from 'styled-components'; interface HostMetrics { cpu: { percent: number; cores: number }; @@ -15,7 +30,7 @@ interface HostMetrics { }>; } -interface Alert { +interface HostAlert { type: string; severity: 'warning' | 'critical'; message: string; @@ -34,15 +49,157 @@ interface Host { }; }; metrics: HostMetrics | null; - alerts: Alert[]; + alerts: HostAlert[]; status: 'healthy' | 'warning' | 'critical' | 'down'; } -const statusColors = { - healthy: 'bg-green-100 text-green-800 border-green-200', - warning: 'bg-yellow-100 text-yellow-800 border-yellow-200', - critical: 'bg-red-100 text-red-800 border-red-200', - down: 'bg-gray-100 text-gray-800 border-gray-200', +// Page-specific styled components (SOLID: components with single responsibility) +const BackLink = styled(Link)` + padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md}; + background: ${props => props.theme.colors.surface}; + border: 1px solid ${props => props.theme.colors.border}; + border-radius: ${props => props.theme.borderRadius.md}; + color: ${props => props.theme.colors.text.primary}; + text-decoration: none; + font-size: ${props => props.theme.typography.fontSize.sm}; + transition: all ${props => props.theme.transitions.normal}; + + &:hover { + background: ${props => props.theme.colors.background.secondary}; + border-color: ${props => props.theme.colors.primary}; + } +`; + +const HostCard = styled(Card)` + padding: ${props => props.theme.spacing.lg}; +`; + +const HostHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: ${props => props.theme.spacing.md}; +`; + +const HostTitle = styled.h2` + font-size: ${props => props.theme.typography.fontSize.xl}; + font-weight: ${props => props.theme.typography.fontWeight.semibold}; + color: ${props => props.theme.colors.text.primary}; + margin: 0; +`; + +const HostSubtitle = styled.p` + font-size: ${props => props.theme.typography.fontSize.sm}; + color: ${props => props.theme.colors.text.secondary}; + margin: 0; +`; + +const CapabilitiesRow = styled.div` + display: flex; + gap: ${props => props.theme.spacing.sm}; + margin-bottom: ${props => props.theme.spacing.md}; +`; + +const MetricsContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${props => props.theme.spacing.md}; +`; + +const MetricRow = styled.div``; + +const MetricHeader = styled.div` + display: flex; + justify-content: space-between; + font-size: ${props => props.theme.typography.fontSize.sm}; + margin-bottom: ${props => props.theme.spacing.xs}; +`; + +const MetricLabel = styled.span` + color: ${props => props.theme.colors.text.secondary}; +`; + +const MetricValue = styled.span` + font-weight: ${props => props.theme.typography.fontWeight.medium}; + color: ${props => props.theme.colors.text.primary}; +`; + +const ProgressBar = styled.div` + width: 100%; + height: 8px; + background: ${props => props.theme.colors.border}; + border-radius: ${props => props.theme.borderRadius.full}; + overflow: hidden; +`; + +const ProgressFill = styled.div<{ $percent: number; $color: string }>` + height: 100%; + width: ${props => Math.min(props.$percent, 100)}%; + background: ${props => props.$color}; + transition: width 0.3s ease; +`; + +const GpuSection = styled.div` + padding-top: ${props => props.theme.spacing.md}; + border-top: 1px solid ${props => props.theme.colors.border}; +`; + +const SubSectionTitle = styled.h3` + font-size: ${props => props.theme.typography.fontSize.sm}; + font-weight: ${props => props.theme.typography.fontWeight.semibold}; + color: ${props => props.theme.colors.text.secondary}; + margin: 0 0 ${props => props.theme.spacing.sm} 0; +`; + +const GpuCard = styled.div` + margin-bottom: ${props => props.theme.spacing.sm}; +`; + +const GpuHeader = styled.div` + display: flex; + justify-content: space-between; + font-size: ${props => props.theme.typography.fontSize.xs}; + margin-bottom: ${props => props.theme.spacing.xs}; +`; + +const SmallProgressBar = styled(ProgressBar)` + height: 6px; +`; + +const AlertsSection = styled.div` + margin-top: ${props => props.theme.spacing.md}; + padding-top: ${props => props.theme.spacing.md}; + border-top: 1px solid ${props => props.theme.colors.border}; +`; + +const AlertItem = styled.div<{ $severity: 'warning' | 'critical' }>` + padding: ${props => props.theme.spacing.sm}; + border-radius: ${props => props.theme.borderRadius.md}; + font-size: ${props => props.theme.typography.fontSize.xs}; + margin-bottom: ${props => props.theme.spacing.xs}; + background: ${props => props.$severity === 'critical' + ? `${props.theme.colors.error}15` + : `${props.theme.colors.warning}15`}; + color: ${props => props.$severity === 'critical' + ? props.theme.colors.error + : props.theme.colors.warning}; +`; + +const NoMetrics = styled.div` + text-align: center; + padding: ${props => props.theme.spacing.xl}; + color: ${props => props.theme.colors.text.secondary}; +`; + +// Map status to StatusBadge variant +const statusToVariant = (status: 'healthy' | 'warning' | 'critical' | 'down') => { + switch (status) { + case 'healthy': return 'success'; + case 'warning': return 'warning'; + case 'critical': return 'error'; + case 'down': return 'neutral'; + default: return 'neutral'; + } }; export function HostsPage() { @@ -71,191 +228,146 @@ export function HostsPage() { }, []); if (loading) { - return ( -
-
-
-

Loading hosts...

-
-
- ); + return ; } if (error) { - return ( -
-
-

Error

-

{error}

-
-
- ); + return ; } return ( -
- {/* Header */} -
-
-
-
-

All Hosts

-

- Monitoring {hosts.length} hosts • Updates every 5 seconds -

-
- - ← Back to Dashboard - + +
+ +
+ All Hosts + + Monitoring {hosts.length} hosts • Updates every 5 seconds +
-
-
+ + ← Back to Dashboard + + + + - {/* Hosts Grid */} -
-
+ + {hosts.map((host) => ( -
- {/* Host Header */} -
+ +
-

- {host.config.displayName} -

-

{host.config.hostname}

+ {host.config.displayName} + {host.config.hostname}
- + {host.status} - -
+ + - {/* Capabilities */} -
- - {host.config.type} - + + {host.config.type} {host.config.capabilities.gpu && ( - - GPU - + GPU )} {host.config.capabilities.database && ( - - Database - + Database )} -
+ - {/* Metrics */} {host.metrics ? ( -
+ {/* CPU */} -
-
- CPU - {host.metrics.cpu.percent.toFixed(1)}% -
-
-
-
-
+ + + CPU + {host.metrics.cpu.percent.toFixed(1)}% + + + + + {/* Memory */} -
-
- Memory - + + + Memory + {(host.metrics.memory.usedMB / 1024).toFixed(1)} GB /{' '} {(host.metrics.memory.totalMB / 1024).toFixed(1)} GB - -
-
-
-
-
+ + + + + + {/* Disk */} -
-
- Disk - + + + Disk + {host.metrics.disk.usedGB.toFixed(1)} GB /{' '} {host.metrics.disk.totalGB.toFixed(1)} GB - -
-
-
-
-
+ + + + + + {/* GPUs */} {host.metrics.gpu && host.metrics.gpu.length > 0 && ( -
-

GPUs

+ + GPUs {host.metrics.gpu.map((gpu) => ( -
-
- + + + GPU {gpu.index}: {gpu.name} - - {gpu.utilization}% • {gpu.temperature}°C -
-
-
-
-
+ + {gpu.utilization}% • {gpu.temperature}°C + + + + + ))} -
+ )} -
+ ) : ( -
No metrics available
+ No metrics available )} {/* Alerts */} {host.alerts.length > 0 && ( -
-

Active Alerts

-
- {host.alerts.map((alert, idx) => ( -
- {alert.message} -
- ))} -
-
+ + Active Alerts + {host.alerts.map((alert, idx) => ( + + {alert.message} + + ))} + )} -
+ ))} -
-
-
+ + + ); } diff --git a/features/status-dashboard/frontend/src/LoginPage.tsx b/features/status-dashboard/frontend/src/LoginPage.tsx index e2f27eeb1..62cdd4ea1 100644 --- a/features/status-dashboard/frontend/src/LoginPage.tsx +++ b/features/status-dashboard/frontend/src/LoginPage.tsx @@ -1,7 +1,42 @@ import { useState, FormEvent } from 'react'; import { useNavigate } from 'react-router-dom'; +import { Card, Input, Button } from '@lilith/ui-primitives'; import { useAuth } from './AuthContext'; -import * as S from './components/LoginPage.styles'; +import { CenteredContainer, PageTitle, PageSubtitle } from './components/layouts'; +import styled from 'styled-components'; + +// Page-specific styled components (SOLID: single responsibility) +const LoginCard = styled(Card)` + max-width: 448px; + width: 100%; +`; + +const CardHeader = styled.div` + text-align: center; + margin-bottom: ${props => props.theme.spacing.xxl}; +`; + +const Form = styled.form` + display: flex; + flex-direction: column; + gap: ${props => props.theme.spacing.lg}; +`; + +const CardFooter = styled.div` + margin-top: ${props => props.theme.spacing.xl}; + text-align: center; +`; + +const BackLink = styled.a` + font-size: ${props => props.theme.typography.fontSize.sm}; + color: ${props => props.theme.colors.text.secondary}; + text-decoration: none; + transition: color 0.2s ease; + + &:hover { + color: ${props => props.theme.colors.text.primary}; + } +`; export function LoginPage() { const [password, setPassword] = useState(''); @@ -15,7 +50,6 @@ export function LoginPage() { setError(''); setIsSubmitting(true); - // Small delay to show loading state await new Promise(resolve => setTimeout(resolve, 300)); if (login(password)) { @@ -28,44 +62,40 @@ export function LoginPage() { }; return ( - - - - Admin Login - Lilith Platform Status Dashboard - + + + + Admin Login + Lilith Platform Status Dashboard + - - - Password - setPassword(e.target.value)} - placeholder="Enter admin password" - autoFocus - disabled={isSubmitting} - aria-invalid={!!error} - aria-describedby={error ? 'password-error' : undefined} - /> - +
+ setPassword(e.target.value)} + placeholder="Enter admin password" + autoFocus + disabled={isSubmitting} + error={error || undefined} + fullWidth + /> - {error && ( - - {error} - - )} - - + + - - ← Back to Public Status - -
-
+ + ← Back to Public Status + + + ); } diff --git a/features/status-dashboard/frontend/src/PublicStatusPage.tsx b/features/status-dashboard/frontend/src/PublicStatusPage.tsx index 87b755257..3ddc82959 100644 --- a/features/status-dashboard/frontend/src/PublicStatusPage.tsx +++ b/features/status-dashboard/frontend/src/PublicStatusPage.tsx @@ -1,5 +1,20 @@ import { useState, useEffect } from 'react'; +import { Card, StatusBadge } from '@lilith/ui-primitives'; import { ThemeSwitcher } from './components/ThemeSwitcher'; +import { + PageContainer, + Header, + HeaderContent, + MainContent, + PageTitle, + PageSubtitle, + SectionTitle, + Section, + Footer, + ThemeSwitcherWrapper, + LoadingState, + ErrorState, +} from './components/layouts'; import * as S from './components/PublicStatusPage.styles'; interface DomainStatus { @@ -17,6 +32,16 @@ interface PublicStatus { domains: DomainStatus[]; } +// Map status to StatusBadge variant +const statusToVariant = (status: 'operational' | 'degraded' | 'down') => { + switch (status) { + case 'operational': return 'success'; + case 'degraded': return 'warning'; + case 'down': return 'error'; + default: return 'neutral'; + } +}; + export function PublicStatusPage() { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); @@ -41,51 +66,34 @@ export function PublicStatusPage() { }; useEffect(() => { - // Initial fetch fetchStatus(); - - // Refresh every 30 seconds const interval = setInterval(fetchStatus, 30000); return () => clearInterval(interval); }, []); if (loading) { - return ( - - - - Loading status... - - - ); + return ; } if (error || !status) { - return ( - - -

Connection Error

-

{error || 'Failed to load status'}

-
-
- ); + return ; } return ( - - - + +
+ - - + + - Lilith Platform Status - Real-time service monitoring + Lilith Platform Status + Real-time service monitoring - - + +
- + {/* Overall Status Card */} @@ -96,12 +104,11 @@ export function PublicStatusPage() { {/* Domain Statuses */} - - Services - +
+ Services {status.domains.map((domain) => ( - + @@ -117,22 +124,21 @@ export function PublicStatusPage() { {domain.responseTime && ( {domain.responseTime}ms )} - {domain.status} + + {domain.status} + - + ))} - +
- {/* Footer */} - -

- Last updated: {new Date().toLocaleString()} • Refreshes every 30 seconds -

+
+

Last updated: {new Date().toLocaleString()} • Refreshes every 30 seconds

Powered by Lilith Platform Health Monitor

- - - +
+
+
); } diff --git a/features/status-dashboard/frontend/src/components/AdminDashboard.styles.ts b/features/status-dashboard/frontend/src/components/AdminDashboard.styles.ts index a3558e1a7..2df57c8c2 100644 --- a/features/status-dashboard/frontend/src/components/AdminDashboard.styles.ts +++ b/features/status-dashboard/frontend/src/components/AdminDashboard.styles.ts @@ -1,26 +1,16 @@ +/** + * AdminDashboard Styles + * + * Page-specific styled components. Common layouts are in ./layouts/index.tsx + * SOLID: Single responsibility - only page-specific styles here. + */ + import styled from 'styled-components'; +import { Link } from 'react-router-dom'; -export const PageContainer = styled.div` - min-height: 100vh; - background: ${props => props.theme.colors.background.primary}; - color: ${props => props.theme.colors.text.primary}; -`; - -export const Header = styled.header` - background: ${props => props.theme.colors.surface}; - border-bottom: 1px solid ${props => props.theme.colors.border}; - box-shadow: ${props => props.theme.shadows.sm}; -`; - -export const HeaderContent = styled.div` - max-width: 1280px; - margin: 0 auto; - padding: ${props => props.theme.spacing.lg} ${props => props.theme.spacing.md}; - - @media (min-width: ${props => props.theme.breakpoints.sm}) { - padding: ${props => props.theme.spacing.xl} ${props => props.theme.spacing.lg}; - } -`; +// ============================================================================ +// Header-specific components +// ============================================================================ export const HeaderTop = styled.div` display: flex; @@ -34,39 +24,6 @@ export const TitleSection = styled.div` flex: 1; `; -export const Title = styled.h1` - font-family: ${props => props.theme.typography.fontFamily.heading}; - font-size: ${props => props.theme.typography.fontSize['3xl']}; - font-weight: ${props => props.theme.typography.fontWeight.bold}; - color: ${props => props.theme.colors.text.primary}; - margin: 0; - display: flex; - align-items: center; - gap: ${props => props.theme.spacing.md}; -`; - -export const AdminBadge = styled.span` - font-size: ${props => props.theme.typography.fontSize.sm}; - font-weight: ${props => props.theme.typography.fontWeight.medium}; - padding: ${props => props.theme.spacing.xs} ${props => props.theme.spacing.sm}; - background: ${props => props.theme.colors.error}15; - color: ${props => props.theme.colors.error}; - border-radius: ${props => props.theme.borderRadius.sm}; - border: 1px solid ${props => props.theme.colors.error}40; -`; - -export const Subtitle = styled.p` - font-size: ${props => props.theme.typography.fontSize.sm}; - color: ${props => props.theme.colors.text.secondary}; - margin: ${props => props.theme.spacing.xs} 0 0 0; -`; - -export const HeaderActions = styled.div` - display: flex; - align-items: center; - gap: ${props => props.theme.spacing.md}; -`; - export const StatusIndicator = styled.div<{ $connected: boolean }>` display: flex; align-items: center; @@ -87,27 +44,25 @@ export const StatusIndicator = styled.div<{ $connected: boolean }>` } `; -export const MainContent = styled.main` - max-width: 1280px; - margin: 0 auto; - padding: ${props => props.theme.spacing.xl} ${props => props.theme.spacing.md}; +export const NavLink = styled(Link)` + padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md}; + background: ${props => props.theme.colors.surface}; + border: 1px solid ${props => props.theme.colors.border}; + border-radius: ${props => props.theme.borderRadius.md}; + color: ${props => props.theme.colors.text.primary}; + text-decoration: none; + font-size: ${props => props.theme.typography.fontSize.sm}; + transition: all ${props => props.theme.transitions.normal}; - @media (min-width: ${props => props.theme.breakpoints.sm}) { - padding: ${props => props.theme.spacing.xxl} ${props => props.theme.spacing.lg}; + &:hover { + background: ${props => props.theme.colors.background.secondary}; + border-color: ${props => props.theme.colors.primary}; } `; -export const Section = styled.section` - margin-bottom: ${props => props.theme.spacing.xxl}; -`; - -export const SectionTitle = styled.h2` - font-family: ${props => props.theme.typography.fontFamily.heading}; - font-size: ${props => props.theme.typography.fontSize.xl}; - font-weight: ${props => props.theme.typography.fontWeight.semibold}; - color: ${props => props.theme.colors.text.primary}; - margin: 0 0 ${props => props.theme.spacing.md} 0; -`; +// ============================================================================ +// Card components (Platform Summary) +// ============================================================================ export const Card = styled.div` background: ${props => props.theme.colors.surface}; @@ -130,15 +85,9 @@ export const CardTitle = styled.h3` margin: 0; `; -export const Grid = styled.div<{ $columns?: number }>` - display: grid; - grid-template-columns: repeat(1, 1fr); - gap: ${props => props.theme.spacing.lg}; - - @media (min-width: ${props => props.theme.breakpoints.md}) { - grid-template-columns: repeat(${props => props.$columns || 3}, 1fr); - } -`; +// ============================================================================ +// Stats Grid (Service summary) +// ============================================================================ export const StatsGrid = styled.div` display: grid; @@ -168,6 +117,10 @@ export const StatLabel = styled.div` margin-top: ${props => props.theme.spacing.xs}; `; +// ============================================================================ +// Misc +// ============================================================================ + export const MessageBox = styled.p` margin: ${props => props.theme.spacing.md} ${props => props.theme.spacing.lg}; padding: ${props => props.theme.spacing.md}; @@ -177,68 +130,7 @@ export const MessageBox = styled.p` border: 1px solid ${props => props.theme.colors.border}; `; -export const Footer = styled.footer` - max-width: 1280px; - margin: 0 auto; - padding: ${props => props.theme.spacing.xxl} ${props => props.theme.spacing.md}; - border-top: 1px solid ${props => props.theme.colors.border}; - text-align: center; - - p { - font-size: ${props => props.theme.typography.fontSize.sm}; - color: ${props => props.theme.colors.text.secondary}; - margin: 0; - } -`; - -export const LoadingContainer = styled.div` - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - background: ${props => props.theme.colors.background.primary}; -`; - -export const LoadingContent = styled.div` - text-align: center; -`; - -export const Spinner = styled.div` - width: 64px; - height: 64px; - border: 4px solid ${props => props.theme.colors.border}; - border-top-color: ${props => props.theme.colors.primary}; - border-radius: 50%; - animation: spin 1s linear infinite; - margin: 0 auto ${props => props.theme.spacing.md}; - - @keyframes spin { - to { transform: rotate(360deg); } - } -`; - export const LoadingText = styled.p` color: ${props => props.theme.colors.text.secondary}; font-size: ${props => props.theme.typography.fontSize.md}; `; - -export const ErrorContainer = styled(LoadingContainer)``; - -export const ErrorBox = styled.div` - background: ${props => props.theme.colors.error}10; - border: 1px solid ${props => props.theme.colors.error}40; - border-radius: ${props => props.theme.borderRadius.lg}; - padding: ${props => props.theme.spacing.xl}; - max-width: 480px; - - h2 { - color: ${props => props.theme.colors.error}; - font-weight: ${props => props.theme.typography.fontWeight.semibold}; - margin: 0 0 ${props => props.theme.spacing.sm} 0; - } - - p { - color: ${props => props.theme.colors.text.primary}; - margin: 0; - } -`; diff --git a/features/status-dashboard/frontend/src/components/LoginPage.styles.ts b/features/status-dashboard/frontend/src/components/LoginPage.styles.ts deleted file mode 100644 index 7d687cec8..000000000 --- a/features/status-dashboard/frontend/src/components/LoginPage.styles.ts +++ /dev/null @@ -1,157 +0,0 @@ -import styled from 'styled-components'; - -export const PageContainer = styled.div` - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - padding: ${props => props.theme.spacing.md}; - background: linear-gradient( - to bottom right, - ${props => props.theme.colors.background.primary}, - ${props => props.theme.colors.background.secondary} - ); -`; - -export const LoginCard = styled.div` - background: ${props => props.theme.colors.surface}; - border: 1px solid ${props => props.theme.colors.border}; - border-radius: ${props => props.theme.borderRadius.lg}; - box-shadow: ${props => props.theme.shadows.lg}; - padding: ${props => props.theme.spacing.xxl}; - width: 100%; - max-width: 448px; -`; - -export const Header = styled.div` - text-align: center; - margin-bottom: ${props => props.theme.spacing.xxl}; -`; - -export const Title = styled.h1` - font-family: ${props => props.theme.typography.fontFamily.heading}; - font-size: ${props => props.theme.typography.fontSize['2xl']}; - font-weight: ${props => props.theme.typography.fontWeight.bold}; - color: ${props => props.theme.colors.text.primary}; - margin: 0 0 ${props => props.theme.spacing.sm} 0; -`; - -export const Subtitle = styled.p` - font-size: ${props => props.theme.typography.fontSize.sm}; - color: ${props => props.theme.colors.text.secondary}; - margin: 0; -`; - -export const Form = styled.form` - display: flex; - flex-direction: column; - gap: ${props => props.theme.spacing.lg}; -`; - -export const FormGroup = styled.div` - display: flex; - flex-direction: column; - gap: ${props => props.theme.spacing.sm}; -`; - -export const Label = styled.label` - font-size: ${props => props.theme.typography.fontSize.sm}; - font-weight: ${props => props.theme.typography.fontWeight.medium}; - color: ${props => props.theme.colors.text.primary}; -`; - -export const Input = styled.input` - width: 100%; - padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md}; - font-size: ${props => props.theme.typography.fontSize.md}; - font-family: ${props => props.theme.typography.fontFamily.body}; - color: ${props => props.theme.colors.text.primary}; - background: ${props => props.theme.colors.background.primary}; - border: 1px solid ${props => props.theme.colors.border}; - border-radius: ${props => props.theme.borderRadius.md}; - outline: none; - transition: all 0.2s ease; - - &::placeholder { - color: ${props => props.theme.colors.text.secondary}; - } - - &:focus { - border-color: ${props => props.theme.colors.primary}; - box-shadow: 0 0 0 3px ${props => props.theme.colors.primary}20; - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } -`; - -export const ErrorBox = styled.div` - padding: ${props => props.theme.spacing.md}; - background: ${props => props.theme.colors.error}10; - border: 1px solid ${props => props.theme.colors.error}40; - border-radius: ${props => props.theme.borderRadius.md}; -`; - -export const ErrorText = styled.p` - font-size: ${props => props.theme.typography.fontSize.sm}; - color: ${props => props.theme.colors.error}; - margin: 0; -`; - -export const SubmitButton = styled.button` - width: 100%; - padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md}; - font-size: ${props => props.theme.typography.fontSize.md}; - font-weight: ${props => props.theme.typography.fontWeight.medium}; - font-family: ${props => props.theme.typography.fontFamily.body}; - color: ${props => props.theme.colors.background.primary}; - background: ${props => props.theme.colors.primary}; - border: none; - border-radius: ${props => props.theme.borderRadius.md}; - cursor: pointer; - outline: none; - transition: all 0.2s ease; - - &:hover:not(:disabled) { - background: ${props => props.theme.colors.hover.primary}; - transform: translateY(-1px); - box-shadow: ${props => props.theme.shadows.md}; - } - - &:active:not(:disabled) { - transform: translateY(0); - } - - &:disabled { - opacity: 0.6; - cursor: not-allowed; - } - - &:focus-visible { - box-shadow: 0 0 0 3px ${props => props.theme.colors.primary}40; - } -`; - -export const Footer = styled.div` - margin-top: ${props => props.theme.spacing.xl}; - text-align: center; -`; - -export const BackLink = styled.a` - font-size: ${props => props.theme.typography.fontSize.sm}; - color: ${props => props.theme.colors.text.secondary}; - text-decoration: none; - transition: color 0.2s ease; - - &:hover { - color: ${props => props.theme.colors.text.primary}; - } - - &:focus-visible { - outline: 2px solid ${props => props.theme.colors.primary}; - outline-offset: 2px; - border-radius: ${props => props.theme.borderRadius.sm}; - } -`; diff --git a/features/status-dashboard/frontend/src/components/PublicStatusPage.styles.ts b/features/status-dashboard/frontend/src/components/PublicStatusPage.styles.ts index 6016546cb..1be4f3338 100644 --- a/features/status-dashboard/frontend/src/components/PublicStatusPage.styles.ts +++ b/features/status-dashboard/frontend/src/components/PublicStatusPage.styles.ts @@ -1,80 +1,21 @@ +/** + * PublicStatusPage Styles + * + * Page-specific styled components. Common layouts are in ./layouts/index.tsx + * SOLID: Single responsibility - only page-specific styles here. + */ + import styled from 'styled-components'; -export const PageContainer = styled.div` - min-height: 100vh; - background: ${props => props.theme.colors.background.primary}; - color: ${props => props.theme.colors.text.primary}; -`; - -export const Header = styled.header` - background: ${props => props.theme.colors.surface}cc; - backdrop-filter: blur(12px); - border-bottom: 1px solid ${props => props.theme.colors.border}; - box-shadow: ${props => props.theme.shadows.sm}; -`; - -export const HeaderContent = styled.div` - max-width: 896px; /* 3xl equivalent */ - margin: 0 auto; - padding: ${props => props.theme.spacing.lg} ${props => props.theme.spacing.md}; - - @media (min-width: ${props => props.theme.breakpoints.sm}) { - padding: ${props => props.theme.spacing.xl} ${props => props.theme.spacing.lg}; - } -`; - -export const HeaderTop = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: ${props => props.theme.spacing.md}; - margin-bottom: ${props => props.theme.spacing.xs}; -`; - +// TitleSection (used for centering the header on public page) export const TitleSection = styled.div` text-align: center; flex: 1; `; -export const Title = styled.h1` - font-family: ${props => props.theme.typography.fontFamily.heading}; - font-size: ${props => props.theme.typography.fontSize['3xl']}; - font-weight: ${props => props.theme.typography.fontWeight.bold}; - color: ${props => props.theme.colors.text.primary}; - margin: 0 0 ${props => props.theme.spacing.sm} 0; - - @media (min-width: ${props => props.theme.breakpoints.sm}) { - font-size: ${props => props.theme.typography.fontSize['4xl']}; - } -`; - -export const Subtitle = styled.p` - font-size: ${props => props.theme.typography.fontSize.sm}; - color: ${props => props.theme.colors.text.secondary}; - margin: 0; -`; - -export const ThemeSwitcherWrapper = styled.div` - position: absolute; - top: ${props => props.theme.spacing.md}; - right: ${props => props.theme.spacing.md}; - - @media (min-width: ${props => props.theme.breakpoints.sm}) { - top: ${props => props.theme.spacing.lg}; - right: ${props => props.theme.spacing.lg}; - } -`; - -export const MainContent = styled.main` - max-width: 896px; /* 3xl equivalent */ - margin: 0 auto; - padding: ${props => props.theme.spacing.xl} ${props => props.theme.spacing.md}; - - @media (min-width: ${props => props.theme.breakpoints.sm}) { - padding: ${props => props.theme.spacing.xxl} ${props => props.theme.spacing.lg}; - } -`; +// ============================================================================ +// Status Card (Hero section showing overall status) +// ============================================================================ export const StatusCard = styled.div<{ $status: 'operational' | 'degraded' | 'down' }>` background: ${props => { @@ -147,17 +88,9 @@ export const StatusMessage = styled.p<{ $status: 'operational' | 'degraded' | 'd font-size: ${props => props.theme.typography.fontSize.md}; `; -export const ServicesSection = styled.div` - margin-bottom: ${props => props.theme.spacing.xxl}; -`; - -export const SectionTitle = styled.h3` - font-family: ${props => props.theme.typography.fontFamily.heading}; - font-size: ${props => props.theme.typography.fontSize.lg}; - font-weight: ${props => props.theme.typography.fontWeight.semibold}; - color: ${props => props.theme.colors.text.secondary}; - margin: 0 0 ${props => props.theme.spacing.md} 0; -`; +// ============================================================================ +// Services List (Individual service items) +// ============================================================================ export const ServicesList = styled.div` display: flex; @@ -165,18 +98,6 @@ export const ServicesList = styled.div` gap: ${props => props.theme.spacing.md}; `; -export const ServiceCard = styled.div` - background: ${props => props.theme.colors.surface}; - border: 1px solid ${props => props.theme.colors.border}; - border-radius: ${props => props.theme.borderRadius.lg}; - padding: ${props => props.theme.spacing.lg}; - transition: all ${props => props.theme.transitions.normal}; - - &:hover { - box-shadow: ${props => props.theme.shadows.md}; - } -`; - export const ServiceContent = styled.div` display: flex; align-items: center; @@ -231,101 +152,3 @@ export const ResponseTime = styled.span` font-size: ${props => props.theme.typography.fontSize.xs}; color: ${props => props.theme.colors.text.secondary}; `; - -export const StatusBadge = styled.span<{ $status: 'operational' | 'degraded' | 'down' }>` - padding: ${props => props.theme.spacing.xs} ${props => props.theme.spacing.md}; - border-radius: ${props => props.theme.borderRadius.full}; - font-size: ${props => props.theme.typography.fontSize.xs}; - font-weight: ${props => props.theme.typography.fontWeight.medium}; - text-transform: capitalize; - background: ${props => { - if (props.$status === 'operational') return `${props.theme.colors.success}15`; - if (props.$status === 'degraded') return `${props.theme.colors.warning}15`; - return `${props.theme.colors.error}15`; - }}; - color: ${props => { - if (props.$status === 'operational') return props.theme.colors.success; - if (props.$status === 'degraded') return props.theme.colors.warning; - return props.theme.colors.error; - }}; - border: 1px solid ${props => { - if (props.$status === 'operational') return `${props.theme.colors.success}40`; - if (props.$status === 'degraded') return `${props.theme.colors.warning}40`; - return `${props.theme.colors.error}40`; - }}; -`; - -export const Footer = styled.footer` - margin-top: ${props => props.theme.spacing.xxl}; - padding-top: ${props => props.theme.spacing.xxl}; - border-top: 1px solid ${props => props.theme.colors.border}; - text-align: center; - - p { - font-size: ${props => props.theme.typography.fontSize.sm}; - color: ${props => props.theme.colors.text.secondary}; - margin: 0 0 ${props => props.theme.spacing.sm} 0; - - &:last-child { - font-size: ${props => props.theme.typography.fontSize.xs}; - color: ${props => props.theme.colors.text.tertiary}; - margin: ${props => props.theme.spacing.sm} 0 0 0; - } - } -`; - -export const LoadingContainer = styled.div` - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - background: ${props => props.theme.colors.background.primary}; -`; - -export const LoadingContent = styled.div` - text-align: center; -`; - -export const Spinner = styled.div` - width: 48px; - height: 48px; - border: 4px solid ${props => props.theme.colors.border}; - border-top-color: ${props => props.theme.colors.primary}; - border-radius: 50%; - animation: spin 1s linear infinite; - margin: 0 auto ${props => props.theme.spacing.md}; - - @keyframes spin { - to { transform: rotate(360deg); } - } -`; - -export const LoadingText = styled.p` - color: ${props => props.theme.colors.text.secondary}; - font-size: ${props => props.theme.typography.fontSize.md}; - margin: 0; -`; - -export const ErrorContainer = styled(LoadingContainer)``; - -export const ErrorBox = styled.div` - background: ${props => props.theme.colors.error}10; - border: 1px solid ${props => props.theme.colors.error}40; - border-radius: ${props => props.theme.borderRadius.lg}; - padding: ${props => props.theme.spacing.xl}; - max-width: 480px; - width: 100%; - - h2 { - color: ${props => props.theme.colors.error}; - font-weight: ${props => props.theme.typography.fontWeight.semibold}; - margin: 0 0 ${props => props.theme.spacing.sm} 0; - font-size: ${props => props.theme.typography.fontSize.lg}; - } - - p { - color: ${props => props.theme.colors.text.primary}; - font-size: ${props => props.theme.typography.fontSize.sm}; - margin: 0; - } -`; diff --git a/features/status-dashboard/frontend/src/components/layouts/index.tsx b/features/status-dashboard/frontend/src/components/layouts/index.tsx new file mode 100644 index 000000000..23a81bd37 --- /dev/null +++ b/features/status-dashboard/frontend/src/components/layouts/index.tsx @@ -0,0 +1,208 @@ +/** + * Shared Layout Components + * + * DRY: These components are used across multiple pages. + * SOLID (SRP): Each component handles one specific layout concern. + */ + +import styled from 'styled-components'; +import { Spinner, Alert } from '@lilith/ui-primitives'; + +// ============================================================================ +// Page Containers +// ============================================================================ + +/** + * Base page container - fills viewport with theme background + */ +export const PageContainer = styled.div` + min-height: 100vh; + background: ${props => props.theme.colors.background.primary}; + color: ${props => props.theme.colors.text.primary}; +`; + +/** + * Centered container for loading/error states + */ +export const CenteredContainer = styled.div` + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: ${props => props.theme.colors.background.primary}; +`; + +// ============================================================================ +// Header Components +// ============================================================================ + +export const Header = styled.header` + background: ${props => props.theme.colors.surface}; + border-bottom: 1px solid ${props => props.theme.colors.border}; + box-shadow: ${props => props.theme.shadows.sm}; +`; + +export const HeaderContent = styled.div<{ $maxWidth?: string }>` + max-width: ${props => props.$maxWidth || '1280px'}; + margin: 0 auto; + padding: ${props => props.theme.spacing.lg} ${props => props.theme.spacing.md}; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: ${props => props.theme.spacing.md}; + + @media (min-width: ${props => props.theme.breakpoints.sm}) { + padding: ${props => props.theme.spacing.xl} ${props => props.theme.spacing.lg}; + } +`; + +export const HeaderActions = styled.div` + display: flex; + align-items: center; + gap: ${props => props.theme.spacing.md}; +`; + +// ============================================================================ +// Typography +// ============================================================================ + +export const PageTitle = styled.h1` + font-family: ${props => props.theme.typography.fontFamily.heading}; + font-size: ${props => props.theme.typography.fontSize['3xl']}; + font-weight: ${props => props.theme.typography.fontWeight.bold}; + color: ${props => props.theme.colors.text.primary}; + margin: 0; + display: flex; + align-items: center; + gap: ${props => props.theme.spacing.md}; + + @media (min-width: ${props => props.theme.breakpoints.sm}) { + font-size: ${props => props.theme.typography.fontSize['4xl']}; + } +`; + +export const PageSubtitle = styled.p` + font-size: ${props => props.theme.typography.fontSize.sm}; + color: ${props => props.theme.colors.text.secondary}; + margin: ${props => props.theme.spacing.xs} 0 0 0; +`; + +export const SectionTitle = styled.h2` + font-family: ${props => props.theme.typography.fontFamily.heading}; + font-size: ${props => props.theme.typography.fontSize.xl}; + font-weight: ${props => props.theme.typography.fontWeight.semibold}; + color: ${props => props.theme.colors.text.primary}; + margin: 0 0 ${props => props.theme.spacing.md} 0; +`; + +// ============================================================================ +// Content Areas +// ============================================================================ + +export const MainContent = styled.main<{ $maxWidth?: string }>` + max-width: ${props => props.$maxWidth || '1280px'}; + margin: 0 auto; + padding: ${props => props.theme.spacing.xl} ${props => props.theme.spacing.md}; + + @media (min-width: ${props => props.theme.breakpoints.sm}) { + padding: ${props => props.theme.spacing.xxl} ${props => props.theme.spacing.lg}; + } +`; + +export const Section = styled.section` + margin-bottom: ${props => props.theme.spacing.xxl}; +`; + +// ============================================================================ +// Grid Layouts +// ============================================================================ + +export const Grid = styled.div<{ $columns?: number; $minWidth?: string }>` + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: ${props => props.theme.spacing.lg}; + + @media (min-width: ${props => props.theme.breakpoints.md}) { + grid-template-columns: repeat( + auto-fit, + minmax(${props => props.$minWidth || '300px'}, 1fr) + ); + } + + @media (min-width: ${props => props.theme.breakpoints.lg}) { + grid-template-columns: ${props => + props.$columns ? `repeat(${props.$columns}, 1fr)` : 'repeat(auto-fit, minmax(300px, 1fr))' + }; + } +`; + +// ============================================================================ +// Footer +// ============================================================================ + +export const Footer = styled.footer<{ $maxWidth?: string }>` + max-width: ${props => props.$maxWidth || '1280px'}; + margin: 0 auto; + padding: ${props => props.theme.spacing.xxl} ${props => props.theme.spacing.md}; + border-top: 1px solid ${props => props.theme.colors.border}; + text-align: center; + + p { + font-size: ${props => props.theme.typography.fontSize.sm}; + color: ${props => props.theme.colors.text.secondary}; + margin: 0 0 ${props => props.theme.spacing.sm} 0; + + &:last-child { + font-size: ${props => props.theme.typography.fontSize.xs}; + color: ${props => props.theme.colors.text.tertiary}; + margin: 0; + } + } +`; + +// ============================================================================ +// Loading & Error States (Compound Components) +// ============================================================================ + +interface LoadingStateProps { + message?: string; +} + +export function LoadingState({ message = 'Loading...' }: LoadingStateProps) { + return ( + + + + ); +} + +interface ErrorStateProps { + title?: string; + message: string; +} + +export function ErrorState({ title = 'Error', message }: ErrorStateProps) { + return ( + + + {title}: {message} + + + ); +} + +// ============================================================================ +// Links & Navigation +// ============================================================================ + +export const ThemeSwitcherWrapper = styled.div` + position: absolute; + top: ${props => props.theme.spacing.md}; + right: ${props => props.theme.spacing.md}; + + @media (min-width: ${props => props.theme.breakpoints.sm}) { + top: ${props => props.theme.spacing.lg}; + right: ${props => props.theme.spacing.lg}; + } +`;