From d4256eb040f8fdfb9fec509bd7c566666b19bb1a Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Fri, 15 May 2026 11:54:53 -0400 Subject: [PATCH 1/4] Add website dark mode --- app/src/WebsiteApp.tsx | 19 ++- app/src/app.css | 106 +++++++++++++ app/src/components/ThemeToggle.tsx | 45 ++++++ app/src/components/blog/blogStyles.ts | 42 ++--- .../home/DowningStreetCredibility.tsx | 4 +- app/src/components/home/HeroSection.tsx | 4 +- app/src/components/home/HomeBlogPreview.tsx | 4 +- .../components/home/HomeTrackerPreview.tsx | 8 +- app/src/components/home/PrimaryCard.tsx | 6 +- app/src/components/home/SecondaryCard.tsx | 6 +- app/src/components/home/TypewriterPrompt.tsx | 2 +- .../components/homeHeader/CountrySelector.tsx | 10 +- .../homeHeader/HeaderActionButtons.tsx | 9 +- app/src/components/homeHeader/MobileMenu.tsx | 7 +- app/src/components/homeHeader/NavItem.tsx | 10 +- .../components/shared/static/ActionButton.tsx | 6 +- .../components/shared/static/CTASection.tsx | 4 +- .../shared/static/ContentSection.tsx | 4 +- .../components/shared/static/HeroSection.tsx | 6 +- .../shared/static/LegalPageLayout.tsx | 2 +- .../shared/static/SupportedProject.tsx | 2 +- .../shared/static/TeamMemberCard.tsx | 2 +- .../components/shared/static/TeamSection.tsx | 4 +- app/src/contexts/ThemeContext.tsx | 144 +++++++++++++++++ app/src/designTokens/colors.ts | 38 +++-- .../tests/unit/contexts/ThemeContext.test.tsx | 71 +++++++++ app/test-utils/render.tsx | 21 +-- app/test-utils/renderWithCountry.tsx | 31 ++-- app/website.html | 21 +++ .../src/__tests__/components/theme.test.tsx | 98 ++++++++++++ website/src/app/globals.css | 121 +++++++++++++- website/src/app/layout.tsx | 33 +++- website/src/components/Header.tsx | 94 +++++++---- website/src/components/ThemeToggle.tsx | 47 ++++++ website/src/components/blog/blogStyles.ts | 40 +++-- website/src/components/home/HeroSection.tsx | 10 +- .../src/components/home/HomeBlogPreview.tsx | 38 ++--- .../components/home/HomeTrackerPreview.tsx | 14 +- .../src/components/home/TypewriterPrompt.tsx | 8 +- .../src/components/static/ActionButton.tsx | 12 +- website/src/components/static/CTASection.tsx | 14 +- .../src/components/static/ContentSection.tsx | 10 +- website/src/components/static/HeroSection.tsx | 6 +- .../src/components/static/LegalPageLayout.tsx | 27 ++-- .../components/static/SupportedProject.tsx | 20 +-- .../src/components/static/TeamMemberCard.tsx | 28 ++-- website/src/components/static/TeamSection.tsx | 10 +- website/src/contexts/ThemeContext.tsx | 149 ++++++++++++++++++ 48 files changed, 1122 insertions(+), 295 deletions(-) create mode 100644 app/src/components/ThemeToggle.tsx create mode 100644 app/src/contexts/ThemeContext.tsx create mode 100644 app/src/tests/unit/contexts/ThemeContext.test.tsx create mode 100644 website/src/__tests__/components/theme.test.tsx create mode 100644 website/src/components/ThemeToggle.tsx create mode 100644 website/src/contexts/ThemeContext.tsx diff --git a/app/src/WebsiteApp.tsx b/app/src/WebsiteApp.tsx index 090624a77..5b6af9d1a 100644 --- a/app/src/WebsiteApp.tsx +++ b/app/src/WebsiteApp.tsx @@ -9,6 +9,7 @@ import { Provider } from 'react-redux'; import DevTools from '@/components/DevTools'; import { TooltipProvider } from '@/components/ui/tooltip'; import { AppProvider } from './contexts/AppContext'; +import { ThemeProvider } from './contexts/ThemeContext'; import { store } from './store'; import { cacheMonitor } from './utils/cacheMonitor'; import { WebsiteRouter } from './WebsiteRouter'; @@ -26,14 +27,16 @@ cacheMonitor.init(queryClient); export default function WebsiteApp() { return ( - - - - - - - - + + + + + + + + + + ); } diff --git a/app/src/app.css b/app/src/app.css index a658806fe..05819e209 100644 --- a/app/src/app.css +++ b/app/src/app.css @@ -161,6 +161,112 @@ --spacing-container: 976px; } +:root, +:root[data-pe-theme='light'] { + color-scheme: light; + --pe-color-surface-accent: #e6fffa; + --pe-color-surface-overlay: rgb(255 255 255 / 0.9); + --pe-color-surface-hover: #f9fafb; + --pe-shadow-light: rgb(16 24 40 / 0.05); + --pe-shadow-medium: rgb(16 24 40 / 0.1); + --pe-shadow-dark: rgb(16 24 40 / 0.2); + + /* Compatibility variables for existing arbitrary Tailwind values. */ + --color-primary-400: var(--tw-color-primary-400); + --color-primary-500: var(--tw-color-primary-500); + --color-primary-600: var(--tw-color-primary-600); +} + +:root[data-pe-theme='dark'] { + color-scheme: dark; + + --tw-color-primary-50: #0f302f; + --tw-color-primary-100: #15504e; + --tw-color-primary-200: #1f6f6c; + --tw-color-primary-300: #2b918d; + --tw-color-primary-400: #38b2ac; + --tw-color-primary-500: #4fd1c5; + --tw-color-primary-600: #63d9cf; + --tw-color-primary-700: #81e6d9; + --tw-color-primary-800: #b2f5ea; + --tw-color-primary-900: #e6fffa; + + --tw-color-secondary-50: #101a1c; + --tw-color-secondary-100: #142225; + --tw-color-secondary-200: #1e3337; + --tw-color-secondary-300: #2b484d; + --tw-color-secondary-400: #49666a; + --tw-color-secondary-500: #73898b; + --tw-color-secondary-600: #9fb0b0; + --tw-color-secondary-700: #c5d0cf; + --tw-color-secondary-800: #e1e8e7; + --tw-color-secondary-900: #f5f8f8; + + --tw-color-gray-50: #0d1719; + --tw-color-gray-100: #132124; + --tw-color-gray-200: #1d3034; + --tw-color-gray-300: #2c454a; + --tw-color-gray-400: #587276; + --tw-color-gray-500: #8ca0a2; + --tw-color-gray-600: #b5c4c5; + --tw-color-gray-700: #d3dddc; + --tw-color-gray-800: #ebf0ef; + --tw-color-gray-900: #f7fafa; + + --tw-color-bg-primary: #071112; + --tw-color-bg-secondary: #0d1c1e; + --tw-color-bg-tertiary: #13272a; + --tw-color-text-primary: #f7fafa; + --tw-color-text-secondary: #bdcaca; + --tw-color-text-tertiary: #899fa1; + --tw-color-text-inverse: #061314; + --tw-color-text-link: #63d9cf; + --tw-color-text-link-hover: #81e6d9; + --tw-color-border-light: #20383c; + --tw-color-border-medium: #315257; + --tw-color-border-dark: #537276; + + --tw-color-background: #071112; + --tw-color-foreground: #f7fafa; + --tw-color-primary: #4fd1c5; + --tw-color-primary-foreground: #061314; + --tw-color-secondary: #132124; + --tw-color-secondary-foreground: #f7fafa; + --tw-color-muted: #132124; + --tw-color-muted-foreground: #bdcaca; + --tw-color-accent: #173235; + --tw-color-accent-foreground: #f7fafa; + --tw-color-popover: #0d1c1e; + --tw-color-popover-foreground: #f7fafa; + --tw-color-card: #0d1c1e; + --tw-color-card-foreground: #f7fafa; + --tw-color-border: #20383c; + --tw-color-input: #20383c; + --tw-color-ring: #4fd1c5; + + --pe-color-surface-accent: #102d2c; + --pe-color-surface-overlay: rgb(13 28 30 / 0.9); + --pe-color-surface-hover: #173235; + --pe-shadow-light: rgb(0 0 0 / 0.28); + --pe-shadow-medium: rgb(0 0 0 / 0.36); + --pe-shadow-dark: rgb(0 0 0 / 0.48); +} + +:root[data-pe-theme='dark'] .tw\:bg-white { + background-color: var(--tw-color-card); +} + +:root[data-pe-theme='dark'] .tw\:text-gray-900, +:root[data-pe-theme='dark'] .tw\:text-gray-800, +:root[data-pe-theme='dark'] .tw\:text-gray-700 { + color: var(--tw-color-foreground); +} + +:root[data-pe-theme='dark'] .tw\:bg-gray-50, +:root[data-pe-theme='dark'] .tw\:bg-gray-100 { + background-color: var(--tw-color-bg-secondary); +} + /* * ========================================================= * Base styles diff --git a/app/src/components/ThemeToggle.tsx b/app/src/components/ThemeToggle.tsx new file mode 100644 index 000000000..d76f7d5fd --- /dev/null +++ b/app/src/components/ThemeToggle.tsx @@ -0,0 +1,45 @@ +import { IconMoon, IconSun } from '@tabler/icons-react'; +import { useTheme } from '@/contexts/ThemeContext'; +import { colors, spacing } from '@/designTokens'; + +const buttonSize = 32; + +export default function ThemeToggle() { + const { resolvedTheme, toggleTheme } = useTheme(); + const isDark = resolvedTheme === 'dark'; + const label = isDark ? 'Switch to light mode' : 'Switch to dark mode'; + const Icon = isDark ? IconSun : IconMoon; + + return ( + + ); +} diff --git a/app/src/components/blog/blogStyles.ts b/app/src/components/blog/blogStyles.ts index 0a27b8da8..8bba05dfa 100644 --- a/app/src/components/blog/blogStyles.ts +++ b/app/src/components/blog/blogStyles.ts @@ -14,48 +14,48 @@ import { colors, spacing, typography } from '@/designTokens'; export const blogColors = { // Primary brand colors primary: colors.primary[600], - primaryLight: colors.primary[100], + primaryLight: colors.background.accent, primaryHover: colors.primary[700], // Text colors (semantic naming) - textPrimary: colors.gray[800], // Main body text - textSecondary: colors.gray[600], // Muted text, quotes - textTertiary: colors.gray[500], // Very muted (code labels) - textHeading: colors.gray[900], // Headings (darkest) - textHeading2: colors.gray[800], // H2 headings - textHeading3: colors.gray[700], // H3 headings - textHeading4: colors.gray[600], // H4 headings + textPrimary: colors.text.primary, // Main body text + textSecondary: colors.text.secondary, // Muted text, quotes + textTertiary: colors.text.tertiary, // Very muted (code labels) + textHeading: colors.text.title, // Headings (darkest) + textHeading2: colors.text.title, // H2 headings + textHeading3: colors.text.primary, // H3 headings + textHeading4: colors.text.secondary, // H4 headings // Background colors (semantic naming) - backgroundPrimary: colors.white, // Main content background - backgroundSecondary: colors.gray[50], // Light panels (quotes, footnotes) - backgroundCode: colors.gray[100], // Code blocks - backgroundCodeLabel: colors.gray[100], // Code language label - backgroundTable: colors.gray[100], // Alternate table rows + backgroundPrimary: colors.background.primary, // Main content background + backgroundSecondary: colors.background.secondary, // Light panels (quotes, footnotes) + backgroundCode: colors.background.tertiary, // Code blocks + backgroundCodeLabel: colors.background.tertiary, // Code language label + backgroundTable: colors.background.secondary, // Alternate table rows // Border colors - borderLight: colors.gray[100], // Very light borders (h2) - borderMedium: colors.gray[200], // Medium borders (code blocks) - borderDark: colors.gray[300], // Darker borders (footnotes, labels) + borderLight: colors.border.light, // Very light borders (h2) + borderMedium: colors.border.medium, // Medium borders (code blocks) + borderDark: colors.border.dark, // Darker borders (footnotes, labels) // Link colors link: colors.primary[600], linkHover: colors.primary[700], // Anchor link colors (heading permalinks) - anchorLink: colors.gray[400], // Subtle anchor links + anchorLink: colors.text.tertiary, // Subtle anchor links // Legacy color mappings (from old app - DEPRECATED, use semantic names above) /** @deprecated Use textHeading instead */ blue: colors.primary[600], /** @deprecated Use textPrimary instead */ - darkGray: colors.gray[800], + darkGray: colors.text.primary, /** @deprecated Use textSecondary instead */ - gray: colors.gray[600], + gray: colors.text.secondary, /** @deprecated Use backgroundCode or backgroundSecondary instead */ - lightGray: colors.gray[100], + lightGray: colors.background.tertiary, /** @deprecated Use anchorLink instead */ - mediumLightGray: colors.gray[400], + mediumLightGray: colors.text.tertiary, } as const; /** diff --git a/app/src/components/home/DowningStreetCredibility.tsx b/app/src/components/home/DowningStreetCredibility.tsx index 0363dbb4a..1624d6f63 100644 --- a/app/src/components/home/DowningStreetCredibility.tsx +++ b/app/src/components/home/DowningStreetCredibility.tsx @@ -13,7 +13,7 @@ export default function DowningStreetCredibility() {