diff --git a/frontend/src/components/ExecutiveStatsBar.tsx b/frontend/src/components/ExecutiveStatsBar.tsx index c27622ce..0cc2d219 100644 --- a/frontend/src/components/ExecutiveStatsBar.tsx +++ b/frontend/src/components/ExecutiveStatsBar.tsx @@ -43,7 +43,7 @@ export const ExecutiveStatsBar: React.FC = ({ > {/* 1. Risk Profile */}
- Status Profile + Status Profile
{loading ? ( <> @@ -61,7 +61,7 @@ export const ExecutiveStatsBar: React.FC = ({ > {riskLabel || 'Moderate'} -

+

{riskNote}

@@ -71,7 +71,7 @@ export const ExecutiveStatsBar: React.FC = ({ {/* 2. Critical Vulns */}
- Critical Vulns + Critical Vulns
{loading ? ( <> @@ -81,7 +81,7 @@ export const ExecutiveStatsBar: React.FC = ({ ) : ( <> {criticalCount} @@ -96,7 +96,7 @@ export const ExecutiveStatsBar: React.FC = ({ {/* 3. Total Findings */}
- Total Findings + Total Findings
{loading ? ( <> @@ -105,7 +105,7 @@ export const ExecutiveStatsBar: React.FC = ({ ) : ( <> - + {findingCount.toLocaleString()} @@ -118,7 +118,7 @@ export const ExecutiveStatsBar: React.FC = ({ {/* 4. Scan Activity */}
- Scan Cycles + Scan Cycles
{loading ? ( <> @@ -127,7 +127,7 @@ export const ExecutiveStatsBar: React.FC = ({ ) : ( <> - + {scanCount.toLocaleString()} diff --git a/frontend/src/index.css b/frontend/src/index.css index 31549e3b..6537e551 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -14,6 +14,8 @@ Design Tokens ============================================ */ :root { + color-scheme: dark; + /* Core Palette - Premium SOC */ --bg-primary: #0a0b0d; /* Deep Onyx */ @@ -63,12 +65,33 @@ --sidebar-expanded: 280px; } +/* Light mode keeps the same semantic palette while swapping its surfaces and + text values. Tailwind's custom charcoal/silver utilities resolve to these + variables, so one root class updates the entire application. */ +:root.theme-light { + color-scheme: light; + + --bg-primary: #f4f6f8; + --bg-secondary: #ffffff; + --bg-tertiary: #e8edf2; + --bg-elevated: #dce3ea; + + --accent-silver: #64748b; + --accent-silver-dim: #64748b; + --accent-silver-bright: #111827; + + --text-primary: #111827; + --text-secondary: #475569; + --text-muted: #64748b; +} + /* Base Overrides */ body { background-color: var(--bg-primary); color: var(--accent-silver); font-family: var(--font-sans); text-rendering: optimizeLegibility; + transition: background-color 200ms ease, color 200ms ease; } @layer components { diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 3b803a0d..42ffcadb 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,5 +1,6 @@ /** @type {import('tailwindcss').Config} */ export default { + darkMode: 'class', content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", @@ -7,16 +8,16 @@ export default { theme: { extend: { colors: { - 'charcoal-dark': '#0a0a0c', + 'charcoal-dark': 'var(--bg-primary)', charcoal: { - light: '#1d1d21', - DEFAULT: '#121214', - dark: '#0a0a0c', /* mapped for backward compatibility */ + light: 'var(--bg-tertiary)', + DEFAULT: 'var(--bg-secondary)', + dark: 'var(--bg-primary)', /* mapped for backward compatibility */ }, silver: { - bright: '#f4f4f5', - DEFAULT: '#a1a1aa', - dark: '#475569', + bright: 'var(--text-primary)', + DEFAULT: 'var(--text-secondary)', + dark: 'var(--text-muted)', }, rag: { red: '#ef4444', @@ -27,7 +28,7 @@ export default { 'blue-bright': '#3b82f6', }, accent: { - silver: '#3f3f46' + silver: 'var(--accent-silver)' } }, fontFamily: { diff --git a/frontend/testing/unit/components/ThemeToggle.test.tsx b/frontend/testing/unit/components/ThemeToggle.test.tsx index e678b92d..6358cc36 100644 --- a/frontend/testing/unit/components/ThemeToggle.test.tsx +++ b/frontend/testing/unit/components/ThemeToggle.test.tsx @@ -39,6 +39,8 @@ describe('ThemeToggle', () => { expect(localStorage.getItem(STORAGE_KEY)).toBe('light') expect(button).toHaveAttribute('aria-pressed', 'false') + expect(document.documentElement).toHaveClass('theme-light') + expect(document.documentElement).not.toHaveClass('dark') }) it('toggles from light to dark on click and persists to localStorage', async () => { @@ -53,6 +55,18 @@ describe('ThemeToggle', () => { expect(localStorage.getItem(STORAGE_KEY)).toBe('dark') expect(button).toHaveAttribute('aria-pressed', 'true') + expect(document.documentElement).toHaveClass('dark') + expect(document.documentElement).not.toHaveClass('theme-light') + }) + + it('restores a saved light theme when the provider mounts', () => { + localStorage.setItem(STORAGE_KEY, 'light') + + renderWithTheme() + + expect(document.documentElement).toHaveClass('theme-light') + expect(document.documentElement).not.toHaveClass('dark') + expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'false') }) it('aria-label reflects the target theme, not the current one', () => {