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 */}
-
+
-
+
+
);
}
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 (
-
- );
+ return ;
}
if (error) {
- return (
-
- );
+ return ;
}
return (
-
- {/* Header */}
-
-
+
{/* 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
-
+
-
-
+
+
+
);
}
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};
+ }
+`;