feat(themes): add theme picker with swatch previews and per-theme fonts

PART 1: Add missing CSS variables to all ui objects
- Add --bg-inset, --bg-muted for component backgrounds
- Add --success, --warning as aliases for --status-success/warning
- Add --warning-muted for warning background states
- Each theme now has 32 CSS variables

PART 2: Per-theme font support
- Move --font-sans and --font-mono from :root to ui objects
- Add fontImports array to theme config (for future custom fonts)
- applyThemeUI() now manages <link> tags for font imports
- Existing themes use empty fontImports (system fonts already loaded)

PART 3: Swatch preview colors
- Add swatch array (3 hex colors) to each theme for visual preview
- light: warm tan, sage green, khaki
- dark: dark brown, sage green, tan
- clean: light gray, Google blue, Google green
- themeList() now returns swatch in result shape

PART 4: Theme picker UI
- New ThemePicker component replaces icon toggle in header
- Palette icon trigger opens popover below
- Shows all themes as circular swatches (conic gradient)
- Active theme has accent ring indicator
- Click swatch to apply theme, closes popover
- Click outside or Escape closes popover
- Styled with current theme CSS variables

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-05-01 17:42:51 +00:00
commit a7fd4e4e8c
5 changed files with 263 additions and 53 deletions

View file

@ -1,6 +1,6 @@
import { useRef, useCallback, useEffect, useState } from 'react'
import { Sun, Moon, Sparkles, LogIn, LogOut } from 'lucide-react'
import { themeList } from '../themes/registry'
import { LogIn, LogOut } from 'lucide-react'
import ThemePicker from './ThemePicker'
import { useStore, usePanelState } from '../store'
import { hasFeature } from '../config'
import SearchBar from './SearchBar'
@ -55,32 +55,6 @@ export default function Panel({ onManeuverClick }) {
return () => window.removeEventListener('resize', check)
}, [])
// Theme toggle - cycles through all available themes
const toggleTheme = () => {
const themes = themeList()
const currentIdx = themes.findIndex(t => t.id === theme)
const nextIdx = (currentIdx + 1) % themes.length
setThemeOverride(themes[nextIdx].id)
}
// Get theme icon based on current theme
const getThemeIcon = () => {
switch (theme) {
case 'dark': return <Moon size={16} />
case 'light': return <Sun size={16} />
case 'clean': return <Sparkles size={16} />
default: return <Sun size={16} />
}
}
// Get next theme name for tooltip
const getNextThemeName = () => {
const themes = themeList()
const currentIdx = themes.findIndex(t => t.id === theme)
const nextIdx = (currentIdx + 1) % themes.length
return themes[nextIdx].name
}
// Auth handlers
const handleLogin = () => { window.location.href = '/outpost.goauthentik.io/start?rd=%2F' }
const handleLogout = () => { window.location.href = 'https://auth.echo6.co/if/flow/default-invalidation-flow/?next=https://navi.echo6.co/' }
@ -274,15 +248,7 @@ export default function Panel({ onManeuverClick }) {
</button>
)
)}
<button
onClick={toggleTheme}
className="p-1.5 rounded"
style={{ color: 'var(--text-secondary)' }}
aria-label={`Switch to ${getNextThemeName()} theme`}
title={`Switch to ${getNextThemeName()} theme`}
>
{getThemeIcon()}
</button>
<ThemePicker />
</div>
</div>
)