Files
cv-site/prompts/004-system-aware-theme-switcher.md
T
juanatsap 1f7757c848 good
2025-11-15 15:59:54 +00:00

24 KiB
Raw Blame History

Implement System-Aware Theme Switcher with Animated Expanding Button

Implement a comprehensive light/dark/auto theme system that respects the user's system preferences and allows manual override. The feature will include an animated expanding button in the top-right corner that reveals three theme options (Light, Dark, Auto) when interacted with.

Key Goals:

  1. Support three theme modes: Light (force light), Dark (force dark), Auto (follow system)
  2. Detect and respect system theme preference via prefers-color-scheme media query
  3. Create an elegant animated button that expands to reveal three options
  4. Persist user preference in localStorage across sessions
  5. Apply theme instantly without page reload using CSS classes

Why This Matters:

  • Modern UX standard: Users expect dark mode support (65% of users prefer it at night)
  • Accessibility: Dark mode reduces eye strain in low-light environments
  • System integration: Respecting OS preferences shows attention to detail
  • User control: Some users want to override system settings (e.g., dark mode during day)
**Current State:** - The CV application currently has a "theme toggle" that switches between default and "clean" views - Theme is applied via `.theme-clean` class on `.cv-container` - Existing toggle uses localStorage for persistence: `localStorage['cv-theme']` - No current dark/light mode support - only layout theme variations

Desired Implementation:

  1. Three Theme Options:

    • Light: Force light color scheme regardless of system preference
    • Dark: Force dark color scheme regardless of system preference
    • Auto: Follow system preference (uses prefers-color-scheme media query)
  2. Animated Button Behavior:

    • Default state: Single circular button showing current theme icon
    • On hover (desktop): Button expands horizontally left-to-right revealing 3 options
    • On click/tap (mobile): Button expands to show 3 options
    • Each option shows an icon (sun, moon, auto) and optional label
    • Smooth animation: width expansion + fade-in of additional buttons
    • Click any option: Collapse button + apply theme + save preference
  3. Positioning:

    • Fixed position at top-right of viewport
    • Always visible (doesn't scroll with page)
    • Positioned above other content (high z-index)
    • Works on mobile and desktop viewports
  4. Persistence:

    • Save user preference to localStorage: localStorage['theme-mode']
    • Values: 'light', 'dark', 'auto'
    • On page load: Read localStorage and apply saved preference
    • If no saved preference: Default to 'auto' (follow system)

Reference Files: @templates/partials/navigation/view-controls.html - Existing toggle pattern with localStorage @static/css/main.css - Existing theme classes and styles @static/hyperscript/functions._hs - Existing hyperscript functions

Tech Stack:

  • CSS custom properties (CSS variables) for theme colors
  • CSS prefers-color-scheme media query for system detection
  • Hyperscript for button animation and theme switching logic
  • localStorage for persistence
  • Iconify icons for theme indicators (already in use)

1. Theme System Architecture

CSS Variables Structure: Create a comprehensive set of CSS custom properties for theming:

:root {
  /* Light theme (default) */
  --bg-primary: #ffffff;
  --bg-secondary: #f5f5f5;
  --text-primary: #333333;
  --text-secondary: #666666;
  --border-color: #e0e0e0;
  --shadow: rgba(0, 0, 0, 0.1);
}

/* Dark theme - applied via [data-theme="dark"] */
[data-theme="dark"] {
  --bg-primary: #1a1a1a;
  --bg-secondary: #2a2a2a;
  --text-primary: #e0e0e0;
  --text-secondary: #b0b0b0;
  --border-color: #404040;
  --shadow: rgba(0, 0, 0, 0.3);
}

/* Auto theme - uses media query */
@media (prefers-color-scheme: dark) {
  [data-theme="auto"] {
    /* Same as dark theme variables */
  }
}

Why CSS Variables:

  • Single source of truth for colors
  • Easy to maintain and extend
  • Automatically cascade to all components
  • Better performance than class-based theme switching for large DOMs

Theme Application:

  • Apply theme via data-theme attribute on <html> or <body>
  • Values: data-theme="light", data-theme="dark", data-theme="auto"
  • JavaScript/Hyperscript sets this attribute based on user selection

2. Animated Button Component

Button States:

  1. Collapsed (default):

    • Single circular button (~48px diameter)
    • Shows icon representing current theme (sun/moon/auto)
    • Subtle shadow and hover effect
  2. Expanded (on hover/click):

    • Expands horizontally to ~160px width (or 3 × 48px = 144px)
    • Reveals 3 circular buttons side-by-side
    • Each button: icon + optional tooltip label
    • Smooth width transition (300ms ease-out)
    • Icons fade in with staggered delay for polish

Animation Specifications:

  • Expansion: width: 48px → 160px over 300ms with ease-out easing
  • Icon fade: Opacity 0 → 1 over 200ms with 50ms stagger
  • Collapse: Reverse animation when mouse leaves (desktop) or after selection
  • Use transform and opacity for GPU acceleration (avoid width if possible, use scale + clip)

HTML Structure Example:

<div id="theme-switcher" class="theme-switcher"
     _="on mouseenter add .expanded to me
        on mouseleave remove .expanded from me">

  <!-- Current theme indicator (always visible) -->
  <button class="theme-btn active" data-theme-mode="auto">
    <iconify-icon icon="mdi:theme-light-dark" width="20"></iconify-icon>
  </button>

  <!-- Additional options (visible when expanded) -->
  <button class="theme-btn" data-theme-mode="light"
          _="on click call setTheme('light')">
    <iconify-icon icon="mdi:white-balance-sunny" width="20"></iconify-icon>
    <span class="tooltip">Light</span>
  </button>

  <button class="theme-btn" data-theme-mode="dark"
          _="on click call setTheme('dark')">
    <iconify-icon icon="mdi:moon-waning-crescent" width="20"></iconify-icon>
    <span class="tooltip">Dark</span>
  </button>

  <button class="theme-btn" data-theme-mode="auto"
          _="on click call setTheme('auto')">
    <iconify-icon icon="mdi:theme-light-dark" width="20"></iconify-icon>
    <span class="tooltip">Auto</span>
  </button>
</div>

Responsive Behavior:

  • Desktop (>768px): Expand on hover, collapse on mouse leave
  • Mobile/Tablet (≤768px): Expand on tap, collapse on background tap or selection
  • Use media query + hyperscript to detect and apply appropriate behavior

3. Theme Switching Logic

Hyperscript Function: Create a global setTheme(mode) function in static/hyperscript/functions._hs:

def setTheme(mode)
  -- Save preference to localStorage
  set localStorage['theme-mode'] to mode

  -- Apply theme to document
  if mode is 'light'
    set document.documentElement's @data-theme to 'light'
  else if mode is 'dark'
    set document.documentElement's @data-theme to 'dark'
  else if mode is 'auto'
    set document.documentElement's @data-theme to 'auto'
  end

  -- Update button active states
  set buttons to .theme-btn in #theme-switcher
  for btn in buttons
    if btn's @data-theme-mode is mode
      add .active to btn
    else
      remove .active from btn
    end
  end

  -- Collapse button on mobile
  if window.innerWidth <= 768
    remove .expanded from #theme-switcher
  end
end

Page Load Theme Detection: Create an initialization script that runs immediately on page load:

def initTheme()
  -- Get saved preference or default to 'auto'
  set savedTheme to localStorage['theme-mode'] or 'auto'
  call setTheme(savedTheme)
end

-- Run on page load
on load call initTheme()

System Preference Detection: Listen for system theme changes when in 'auto' mode:

// Optional: Listen for system theme changes
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeQuery.addEventListener('change', (e) => {
  const currentMode = localStorage.getItem('theme-mode');
  if (currentMode === 'auto' || !currentMode) {
    // Theme will automatically update via CSS media query
    // No action needed, but could trigger a visual indicator
  }
});

4. Positioning and Layout

Fixed Positioning:

.theme-switcher {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 1000; /* Above all content */
  display: flex;
  gap: 4px;
  background: var(--bg-primary);
  border: 1px solid var(--border-color);
  border-radius: 24px;
  padding: 4px;
  box-shadow: 0 2px 8px var(--shadow);
  width: 56px; /* Single button + padding */
  transition: width 300ms ease-out;
  overflow: hidden;
}

.theme-switcher.expanded {
  width: 176px; /* 3 buttons + gaps + padding */
}

.theme-btn {
  min-width: 48px;
  height: 48px;
  border-radius: 50%;
  border: none;
  background: transparent;
  cursor: pointer;
  transition: background 200ms ease;
  opacity: 1;
}

.theme-btn:not(.active) {
  opacity: 0.5;
}

.theme-switcher:not(.expanded) .theme-btn:not(.active) {
  display: none; /* Hide non-active buttons when collapsed */
}

.theme-btn:hover {
  background: var(--bg-secondary);
}

.theme-btn.active {
  background: var(--bg-secondary);
}

Mobile Considerations:

  • Increase touch target size to minimum 44×44px (iOS HIG)
  • Ensure button doesn't overlap with hamburger menu or other controls
  • Consider adding backdrop overlay when expanded on mobile
  • Position may need adjustment on smaller screens (e.g., top: 16px; right: 16px)

5. Color Scheme Design

Light Theme Colors:

  • Background: White to light gray (#ffffff, #f5f5f5)
  • Text: Dark gray to black (#333333, #1a1a1a)
  • Accent: Keep existing brand colors
  • Borders: Light gray (#e0e0e0)

Dark Theme Colors:

  • Background: Very dark gray to black (#1a1a1a, #0a0a0a)
  • Text: Light gray to white (#e0e0e0, #ffffff)
  • Accent: Slightly brighter versions of brand colors
  • Borders: Medium dark gray (#404040)

Design Principles:

  • Maintain sufficient contrast (WCAG AA: 4.5:1 for text, AAA: 7:1 preferred)
  • Don't use pure black (#000000) for dark backgrounds (too harsh)
  • Don't use pure white text on dark backgrounds (causes halation)
  • Test with both themes for readability and accessibility

6. Persistence and State Management

localStorage Schema:

// Store theme preference
localStorage.setItem('theme-mode', 'light'); // or 'dark', 'auto'

// Read on page load
const savedTheme = localStorage.getItem('theme-mode') || 'auto';

State Synchronization:

  • Button active state must always reflect current theme
  • System theme changes should update UI if in 'auto' mode
  • Manual theme selection should override system preference
  • Theme should apply before page render to avoid flash (FOUC)

Initial Load Optimization: To prevent flash of wrong theme, add inline script in <head>:

<script>
(function() {
  const theme = localStorage.getItem('theme-mode') || 'auto';
  document.documentElement.setAttribute('data-theme', theme);
})();
</script>

This runs before page render, applying theme instantly.

7. Integration with Existing Theme System

Current System:

  • .theme-clean class toggles between default and clean layouts
  • This is a layout theme, not a color theme

New System:

  • data-theme attribute controls color theme (light/dark/auto)
  • .theme-clean class still controls layout theme

Both Can Coexist:

<body class="theme-clean" data-theme="dark">
  <!-- Clean layout + Dark colors -->
</body>

CSS Organization:

  • Keep existing .theme-clean styles unchanged
  • Add new [data-theme="dark"] styles for colors
  • Ensure both systems work independently and together

Step-by-Step Implementation Plan

Phase 1: Create CSS Theme Variables

  1. Add CSS Variables to static/css/main.css:

    • Define :root (light theme) color variables
    • Define [data-theme="dark"] color variables
    • Define @media (prefers-color-scheme: dark) with [data-theme="auto"]
    • Update existing components to use CSS variables instead of hardcoded colors
  2. Color Mapping Strategy:

    • Audit existing colors in main.css
    • Create a mapping from hardcoded colors to CSS variable names
    • Replace colors incrementally (high-impact areas first: backgrounds, text, borders)

Phase 2: Create Theme Switcher Button Component

  1. Create HTML Template:

    • New file: templates/partials/theme-switcher.html
    • Structure: Container with 3 buttons (light, dark, auto)
    • Include iconify icons for each theme
    • Add hyperscript for expand/collapse behavior
  2. Create CSS Styles:

    • Add to static/css/main.css or new static/css/theme-switcher.css
    • Fixed positioning at top-right
    • Collapsed and expanded states
    • Button hover and active states
    • Smooth transitions and animations
    • Responsive behavior (desktop vs mobile)
  3. Include in Main Layout:

    • Add {{template "theme-switcher" .}} to main layout template
    • Position below action bar but above content (z-index management)

Phase 3: Implement Theme Switching Logic

  1. Add Hyperscript Functions to static/hyperscript/functions._hs:

    • setTheme(mode) - Apply theme and save to localStorage
    • initTheme() - Load saved theme on page load
    • Button click handlers for each theme option
  2. Add Inline Script for FOUC Prevention:

    • In <head> of main template, add inline script
    • Reads localStorage and sets data-theme before render
    • Prevents flash of wrong theme
  3. System Theme Detection (Optional Enhancement):

    • Add media query listener for system theme changes
    • Update UI when system preference changes (if in 'auto' mode)

Phase 4: Update Existing Components with Theme Variables

Priority Order:

  1. Main backgrounds and text (highest visual impact)
  2. CV paper and content areas
  3. Navigation and controls
  4. Borders and shadows
  5. Accent colors and highlights

Example Refactor:

/* Before */
.cv-container {
  background: #ffffff;
  color: #333333;
}

/* After */
.cv-container {
  background: var(--bg-primary);
  color: var(--text-primary);
}

Phase 5: Testing and Refinement

  1. Visual Testing:

    • Test all three theme modes (light, dark, auto)
    • Verify color contrast meets WCAG AA standards
    • Check all components render correctly in both themes
  2. Interaction Testing:

    • Button expands smoothly on hover (desktop)
    • Button expands/collapses on tap (mobile)
    • Theme applies instantly when selected
    • Active state updates correctly
  3. Persistence Testing:

    • Save theme preference and reload page
    • Verify saved theme is applied before render (no FOUC)
    • Clear localStorage and verify default to 'auto'
  4. System Integration Testing:

    • Change system theme preference (OS settings)
    • Verify 'auto' mode respects system preference
    • Verify 'light' and 'dark' modes override system

What to Prioritize:

  1. Core theme switching functionality
  2. FOUC prevention (inline script)
  3. Button animation and UX
  4. High-impact component theming (backgrounds, text)
  5. Fine-tuning colors and contrast

What to Avoid:

  • Don't try to theme every single pixel in first pass - prioritize high-impact areas
  • Don't use JavaScript for theme application if CSS can handle it (performance)
  • Don't forget mobile UX - touch targets, tap behavior, responsive design
  • Don't hardcode colors - always use CSS variables
  • Don't sacrifice accessibility for aesthetics (contrast ratios are critical)

Why These Constraints Matter:

  • CSS Variables: Maintainable, performant, scalable theming system
  • FOUC Prevention: Critical for professional UX - theme must apply before render
  • Mobile-First: Touch devices are primary interaction method for many users
  • Accessibility: WCAG compliance is non-negotiable for professional applications
  • Progressive Enhancement: Light theme works even if JavaScript fails
Create/modify the following files:
  1. ./templates/partials/theme-switcher.html (NEW)

    • Animated theme switcher button component
    • Three buttons for light, dark, auto modes
    • Hyperscript for expand/collapse and theme selection
    • Iconify icons for each theme option
  2. ./static/css/theme-variables.css (NEW) or add to ./static/css/main.css

    • CSS custom properties for light theme (:root)
    • CSS custom properties for dark theme ([data-theme="dark"])
    • Media query for auto mode (@media (prefers-color-scheme: dark))
    • Theme switcher button styles
  3. ./static/hyperscript/functions._hs

    • Add setTheme(mode) function
    • Add initTheme() function
    • Add page load initialization
  4. ./templates/index.html (or main layout template)

    • Include theme switcher component
    • Add inline FOUC prevention script in <head>
  5. ./static/css/main.css

    • Refactor existing hardcoded colors to use CSS variables
    • Update backgrounds, text, borders, shadows
    • Ensure compatibility with both theme systems (layout + color)

Optional (recommended): 6. ./static/js/theme-system-listener.js (NEW)

  • Listen for system theme changes
  • Update UI when system preference changes in 'auto' mode
  • Only needed for dynamic system theme updates
Before declaring complete, perform these comprehensive tests:

1. Visual Verification:

  • Light mode: All text readable, good contrast, professional appearance
  • Dark mode: All text readable, good contrast, not too harsh
  • Auto mode: Follows system preference correctly
  • Button expands smoothly showing 3 options
  • Button collapses smoothly after selection
  • Active button is visually distinct

2. Interaction Testing:

  • Desktop: Button expands on hover, collapses on mouse leave
  • Mobile: Button expands on tap, collapses on selection or backdrop tap
  • Click light button: Theme switches to light instantly
  • Click dark button: Theme switches to dark instantly
  • Click auto button: Theme follows system preference
  • No delay or lag in theme application

3. Persistence Testing:

  • Select light theme, reload page → Still light
  • Select dark theme, reload page → Still dark
  • Select auto theme, reload page → Still auto (follows system)
  • Clear localStorage → Defaults to auto mode
  • No flash of unstyled content (FOUC) on page load

4. System Integration:

  • Set system to light mode, set app to auto → Light theme applied
  • Change system to dark mode with app in auto → Dark theme applied
  • Set app to light mode, change system to dark → Stays light (override works)
  • Media query correctly detects system preference

5. Accessibility Testing:

  • Light mode: WCAG AA contrast ratio for all text (4.5:1 minimum)
  • Dark mode: WCAG AA contrast ratio for all text
  • Button tooltips/labels are readable
  • Keyboard navigation works (tab through theme options)
  • Focus states are visible
  • Screen reader announces theme changes

6. Component Coverage:

  • CV container background themed correctly
  • Text colors themed correctly (headings, body, secondary)
  • Navigation elements themed correctly
  • Borders and dividers visible in both themes
  • Shadows appropriate for both themes
  • Icons and images work in both themes
  • Toggle controls remain functional and readable

7. Layout Compatibility:

  • Theme works with default layout (not .theme-clean)
  • Theme works with .theme-clean layout
  • Both theme systems can be used simultaneously
  • No conflicts between layout theme and color theme

8. Animation Performance:

  • Button expansion is smooth (60fps, no jank)
  • Theme switching is instant (no visible delay)
  • GPU-accelerated properties used (opacity, transform)
  • No layout thrashing during animations

9. Mobile Specific:

  • Button positioned correctly on mobile (no overlap with other controls)
  • Touch targets are adequate size (44×44px minimum)
  • Tap behavior works correctly (expand/collapse)
  • Responsive design adapts to small screens

10. Edge Cases:

  • Very fast clicking doesn't break state
  • System theme change during 'auto' mode updates correctly
  • Works in private/incognito mode (localStorage available)
  • Graceful degradation if JavaScript disabled (defaults to light)

Success Indicators: Three theme modes work correctly (light, dark, auto) System preference detected and respected in auto mode Theme persists across page reloads (localStorage) No FOUC - theme applies before page render Animated button expands/collapses smoothly Responsive behavior (hover on desktop, tap on mobile) All text meets WCAG AA contrast standards Theme switching is instant and smooth Works alongside existing .theme-clean layout system Professional, polished UX

<success_criteria>

  1. Three distinct theme modes implemented: Light, Dark, Auto (system)
  2. System preference correctly detected via prefers-color-scheme media query
  3. User preference persists in localStorage across sessions
  4. Animated expanding button reveals theme options smoothly (300ms transition)
  5. Responsive behavior: hover on desktop (>768px), tap on mobile (≤768px)
  6. Fixed positioning at top-right, always visible, doesn't interfere with content
  7. No flash of unstyled content (FOUC) - theme applies before render
  8. All text meets WCAG AA contrast ratio (4.5:1 for normal text)
  9. Theme applies instantly on selection (<50ms perceived delay)
  10. Compatible with existing .theme-clean layout system
  11. Accessible keyboard navigation and screen reader support
  12. Smooth animations using GPU-accelerated properties
  13. Code follows existing project patterns and conventions
  14. Implementation is maintainable and well-documented </success_criteria>
**CSS Color Schemes:** - MDN prefers-color-scheme: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme - Material Design dark theme: https://material.io/design/color/dark-theme.html - CSS custom properties: https://developer.mozilla.org/en-US/docs/Web/CSS/--*

Best Practices:

  • Avoid pure black in dark mode (use #1a1a1a or similar)
  • Maintain consistent contrast ratios
  • Use elevation (shadows) to create depth in dark mode
  • Test with real system theme preferences
  • Prevent FOUC with inline script in head

Icon Suggestions (iconify):

  • Light mode: mdi:white-balance-sunny or mdi:brightness-7
  • Dark mode: mdi:moon-waning-crescent or mdi:weather-night
  • Auto mode: mdi:theme-light-dark or mdi:brightness-auto

Animation Patterns:

  • Expand: ease-out (starts fast, ends slow)
  • Collapse: ease-in (starts slow, ends fast)
  • Theme switch: instant (no transition on color change)
  • Icons: staggered fade-in for polish

Accessibility:

  • WCAG AA: 4.5:1 for normal text, 3:1 for large text
  • WCAG AAA: 7:1 for normal text, 4.5:1 for large text
  • Test with actual screen readers (VoiceOver, NVDA)
  • Ensure focus indicators are visible in both themes
**Files to Examine:** @static/css/main.css - Current color usage and theme system @templates/partials/navigation/view-controls.html - Existing toggle pattern @static/hyperscript/functions._hs - Existing hyperscript functions @templates/index.html - Main layout structure

Questions to Answer:

  1. What colors are currently hardcoded and need to become variables?
  2. How is the existing .theme-clean system implemented?
  3. Where should the theme switcher button be positioned to not conflict with existing UI?
  4. What iconify icons are already in use (maintain consistency)?
  5. What localStorage keys are already in use (avoid conflicts)?

<additional_notes> Implementation Philosophy:

  • Progressive enhancement: Works without JavaScript (defaults to light)
  • Mobile-first: Touch interactions are primary, hover is enhancement
  • Performance-first: CSS handles theme, JavaScript only manages state
  • Accessibility-first: WCAG compliance is non-negotiable
  • Maintainability: CSS variables make future updates trivial

Design Considerations:

  • Button should feel premium and polished (subtle shadows, smooth animations)
  • Don't over-animate - smooth and subtle is better than flashy
  • Dark mode should be comfortable for extended reading, not just "looks cool"
  • Auto mode should be the intelligent default (respects user's OS preference)

Future Enhancements:

  • Add transition animation when theme changes (optional fade)
  • Support custom theme colors (user-selectable accent colors)
  • Add sunrise/sunset auto-scheduling (auto-switch based on time)
  • Sync theme preference across devices (server-side storage)
  • Add "high contrast" mode for accessibility </additional_notes>