Add theme system with color presets, fix login page styling, persist auth session

Theme system with 5 color presets (Slate, Emerald, Violet, Amber, Rose)
and light/dark/system mode. User menu in sidebar with theme picker and
sign out. Login page uses standalone dark branded styling with autofill
override. Auth persists in sessionStorage across refreshes.
This commit is contained in:
Ryan Moon
2026-03-28 08:30:24 -05:00
parent 9abdf6c050
commit 7c64a928e1
10 changed files with 691 additions and 145 deletions

View File

@@ -0,0 +1,379 @@
export interface ThemeColors {
background: string
foreground: string
card: string
cardForeground: string
popover: string
popoverForeground: string
primary: string
primaryForeground: string
secondary: string
secondaryForeground: string
muted: string
mutedForeground: string
accent: string
accentForeground: string
destructive: string
destructiveForeground: string
border: string
input: string
ring: string
sidebar: string
sidebarForeground: string
sidebarPrimary: string
sidebarPrimaryForeground: string
sidebarAccent: string
sidebarAccentForeground: string
sidebarBorder: string
}
export interface ThemePreset {
name: string
label: string
light: ThemeColors
dark: ThemeColors
}
export const themes: ThemePreset[] = [
{
name: 'slate',
label: 'Slate',
light: {
background: '210 20% 97%',
foreground: '222 47% 11%',
card: '0 0% 100%',
cardForeground: '222 47% 11%',
popover: '0 0% 100%',
popoverForeground: '222 47% 11%',
primary: '215 25% 27%',
primaryForeground: '210 40% 98%',
secondary: '210 40% 94%',
secondaryForeground: '222 47% 11%',
muted: '210 40% 94%',
mutedForeground: '215 16% 47%',
accent: '210 40% 94%',
accentForeground: '222 47% 11%',
destructive: '0 72% 51%',
destructiveForeground: '210 40% 98%',
border: '214 32% 89%',
input: '214 32% 89%',
ring: '215 25% 27%',
sidebar: '210 25% 95%',
sidebarForeground: '215 16% 47%',
sidebarPrimary: '215 25% 27%',
sidebarPrimaryForeground: '210 40% 98%',
sidebarAccent: '210 40% 92%',
sidebarAccentForeground: '222 47% 11%',
sidebarBorder: '214 32% 89%',
},
dark: {
background: '222 30% 8%',
foreground: '210 20% 86%',
card: '222 30% 10%',
cardForeground: '210 20% 86%',
popover: '222 30% 10%',
popoverForeground: '210 20% 86%',
primary: '215 20% 72%',
primaryForeground: '222 30% 8%',
secondary: '217 20% 15%',
secondaryForeground: '210 20% 86%',
muted: '217 20% 15%',
mutedForeground: '215 15% 55%',
accent: '217 20% 15%',
accentForeground: '210 20% 86%',
destructive: '0 55% 48%',
destructiveForeground: '210 20% 92%',
border: '217 20% 17%',
input: '217 20% 17%',
ring: '215 20% 55%',
sidebar: '222 30% 7%',
sidebarForeground: '215 15% 65%',
sidebarPrimary: '215 20% 72%',
sidebarPrimaryForeground: '0 0% 100%',
sidebarAccent: '217 20% 13%',
sidebarAccentForeground: '210 20% 86%',
sidebarBorder: '217 20% 15%',
},
},
{
name: 'emerald',
label: 'Emerald',
light: {
background: '150 20% 97%',
foreground: '150 40% 10%',
card: '0 0% 100%',
cardForeground: '150 40% 10%',
popover: '0 0% 100%',
popoverForeground: '150 40% 10%',
primary: '160 84% 29%',
primaryForeground: '150 20% 98%',
secondary: '150 20% 93%',
secondaryForeground: '150 40% 10%',
muted: '150 20% 93%',
mutedForeground: '150 10% 45%',
accent: '150 20% 93%',
accentForeground: '150 40% 10%',
destructive: '0 72% 51%',
destructiveForeground: '150 20% 98%',
border: '150 15% 88%',
input: '150 15% 88%',
ring: '160 84% 29%',
sidebar: '150 20% 95%',
sidebarForeground: '150 10% 45%',
sidebarPrimary: '160 84% 29%',
sidebarPrimaryForeground: '150 20% 98%',
sidebarAccent: '150 20% 91%',
sidebarAccentForeground: '150 40% 10%',
sidebarBorder: '150 15% 88%',
},
dark: {
background: '155 30% 7%',
foreground: '150 15% 85%',
card: '155 30% 9%',
cardForeground: '150 15% 85%',
popover: '155 30% 9%',
popoverForeground: '150 15% 85%',
primary: '160 72% 42%',
primaryForeground: '155 30% 7%',
secondary: '155 20% 14%',
secondaryForeground: '150 15% 85%',
muted: '155 20% 14%',
mutedForeground: '150 10% 52%',
accent: '155 20% 14%',
accentForeground: '150 15% 85%',
destructive: '0 55% 48%',
destructiveForeground: '150 15% 92%',
border: '155 20% 16%',
input: '155 20% 16%',
ring: '160 72% 42%',
sidebar: '155 30% 6%',
sidebarForeground: '150 10% 62%',
sidebarPrimary: '160 72% 42%',
sidebarPrimaryForeground: '0 0% 100%',
sidebarAccent: '155 20% 12%',
sidebarAccentForeground: '150 15% 85%',
sidebarBorder: '155 20% 14%',
},
},
{
name: 'violet',
label: 'Violet',
light: {
background: '260 15% 97%',
foreground: '260 40% 11%',
card: '0 0% 100%',
cardForeground: '260 40% 11%',
popover: '0 0% 100%',
popoverForeground: '260 40% 11%',
primary: '262 83% 58%',
primaryForeground: '260 15% 98%',
secondary: '260 20% 94%',
secondaryForeground: '260 40% 11%',
muted: '260 20% 94%',
mutedForeground: '260 10% 47%',
accent: '260 20% 94%',
accentForeground: '260 40% 11%',
destructive: '0 72% 51%',
destructiveForeground: '260 15% 98%',
border: '260 15% 89%',
input: '260 15% 89%',
ring: '262 83% 58%',
sidebar: '260 18% 95%',
sidebarForeground: '260 10% 47%',
sidebarPrimary: '262 83% 58%',
sidebarPrimaryForeground: '260 15% 98%',
sidebarAccent: '260 20% 92%',
sidebarAccentForeground: '260 40% 11%',
sidebarBorder: '260 15% 89%',
},
dark: {
background: '260 30% 8%',
foreground: '260 15% 86%',
card: '260 30% 10%',
cardForeground: '260 15% 86%',
popover: '260 30% 10%',
popoverForeground: '260 15% 86%',
primary: '262 83% 65%',
primaryForeground: '260 30% 8%',
secondary: '260 20% 15%',
secondaryForeground: '260 15% 86%',
muted: '260 20% 15%',
mutedForeground: '260 10% 54%',
accent: '260 20% 15%',
accentForeground: '260 15% 86%',
destructive: '0 55% 48%',
destructiveForeground: '260 15% 92%',
border: '260 20% 17%',
input: '260 20% 17%',
ring: '262 83% 65%',
sidebar: '260 30% 7%',
sidebarForeground: '260 10% 64%',
sidebarPrimary: '262 83% 65%',
sidebarPrimaryForeground: '0 0% 100%',
sidebarAccent: '260 20% 13%',
sidebarAccentForeground: '260 15% 86%',
sidebarBorder: '260 20% 15%',
},
},
{
name: 'amber',
label: 'Amber',
light: {
background: '36 20% 97%',
foreground: '28 40% 10%',
card: '0 0% 100%',
cardForeground: '28 40% 10%',
popover: '0 0% 100%',
popoverForeground: '28 40% 10%',
primary: '25 95% 40%',
primaryForeground: '36 20% 98%',
secondary: '36 25% 93%',
secondaryForeground: '28 40% 10%',
muted: '36 25% 93%',
mutedForeground: '28 10% 47%',
accent: '36 25% 93%',
accentForeground: '28 40% 10%',
destructive: '0 72% 51%',
destructiveForeground: '36 20% 98%',
border: '36 20% 88%',
input: '36 20% 88%',
ring: '25 95% 40%',
sidebar: '36 22% 95%',
sidebarForeground: '28 10% 47%',
sidebarPrimary: '25 95% 40%',
sidebarPrimaryForeground: '36 20% 98%',
sidebarAccent: '36 25% 91%',
sidebarAccentForeground: '28 40% 10%',
sidebarBorder: '36 20% 88%',
},
dark: {
background: '28 25% 7%',
foreground: '36 15% 85%',
card: '28 25% 9%',
cardForeground: '36 15% 85%',
popover: '28 25% 9%',
popoverForeground: '36 15% 85%',
primary: '30 90% 52%',
primaryForeground: '28 25% 7%',
secondary: '28 18% 14%',
secondaryForeground: '36 15% 85%',
muted: '28 18% 14%',
mutedForeground: '28 10% 52%',
accent: '28 18% 14%',
accentForeground: '36 15% 85%',
destructive: '0 55% 48%',
destructiveForeground: '36 15% 92%',
border: '28 18% 16%',
input: '28 18% 16%',
ring: '30 90% 52%',
sidebar: '28 25% 6%',
sidebarForeground: '28 10% 62%',
sidebarPrimary: '30 90% 52%',
sidebarPrimaryForeground: '0 0% 100%',
sidebarAccent: '28 18% 12%',
sidebarAccentForeground: '36 15% 85%',
sidebarBorder: '28 18% 14%',
},
},
{
name: 'rose',
label: 'Rose',
light: {
background: '350 15% 97%',
foreground: '350 40% 10%',
card: '0 0% 100%',
cardForeground: '350 40% 10%',
popover: '0 0% 100%',
popoverForeground: '350 40% 10%',
primary: '347 77% 50%',
primaryForeground: '350 15% 98%',
secondary: '350 20% 94%',
secondaryForeground: '350 40% 10%',
muted: '350 20% 94%',
mutedForeground: '350 10% 47%',
accent: '350 20% 94%',
accentForeground: '350 40% 10%',
destructive: '0 72% 51%',
destructiveForeground: '350 15% 98%',
border: '350 15% 89%',
input: '350 15% 89%',
ring: '347 77% 50%',
sidebar: '350 18% 95%',
sidebarForeground: '350 10% 47%',
sidebarPrimary: '347 77% 50%',
sidebarPrimaryForeground: '350 15% 98%',
sidebarAccent: '350 20% 92%',
sidebarAccentForeground: '350 40% 10%',
sidebarBorder: '350 15% 89%',
},
dark: {
background: '350 25% 7%',
foreground: '350 12% 86%',
card: '350 25% 9%',
cardForeground: '350 12% 86%',
popover: '350 25% 9%',
popoverForeground: '350 12% 86%',
primary: '347 77% 58%',
primaryForeground: '350 25% 7%',
secondary: '350 18% 14%',
secondaryForeground: '350 12% 86%',
muted: '350 18% 14%',
mutedForeground: '350 10% 54%',
accent: '350 18% 14%',
accentForeground: '350 12% 86%',
destructive: '0 55% 48%',
destructiveForeground: '350 12% 92%',
border: '350 18% 16%',
input: '350 18% 16%',
ring: '347 77% 58%',
sidebar: '350 25% 6%',
sidebarForeground: '350 10% 62%',
sidebarPrimary: '347 77% 58%',
sidebarPrimaryForeground: '0 0% 100%',
sidebarAccent: '350 18% 12%',
sidebarAccentForeground: '350 12% 86%',
sidebarBorder: '350 18% 14%',
},
},
]
const CSS_VAR_MAP: Record<keyof ThemeColors, string> = {
background: '--color-background',
foreground: '--color-foreground',
card: '--color-card',
cardForeground: '--color-card-foreground',
popover: '--color-popover',
popoverForeground: '--color-popover-foreground',
primary: '--color-primary',
primaryForeground: '--color-primary-foreground',
secondary: '--color-secondary',
secondaryForeground: '--color-secondary-foreground',
muted: '--color-muted',
mutedForeground: '--color-muted-foreground',
accent: '--color-accent',
accentForeground: '--color-accent-foreground',
destructive: '--color-destructive',
destructiveForeground: '--color-destructive-foreground',
border: '--color-border',
input: '--color-input',
ring: '--color-ring',
sidebar: '--sidebar',
sidebarForeground: '--sidebar-foreground',
sidebarPrimary: '--sidebar-primary',
sidebarPrimaryForeground: '--sidebar-primary-foreground',
sidebarAccent: '--sidebar-accent',
sidebarAccentForeground: '--sidebar-accent-foreground',
sidebarBorder: '--sidebar-border',
}
export function applyThemeColors(colors: ThemeColors) {
const root = document.documentElement
for (const [key, cssVar] of Object.entries(CSS_VAR_MAP)) {
const value = colors[key as keyof ThemeColors]
root.style.setProperty(cssVar, `hsl(${value})`)
}
}
export function getThemeByName(name: string): ThemePreset {
return themes.find((t) => t.name === name) ?? themes[0]
}