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

@@ -21,17 +21,39 @@ function decodeJwtPayload(token: string): { id: string; companyId: string; role:
return JSON.parse(atob(payload))
}
export const useAuthStore = create<AuthState>((set) => ({
token: null,
user: null,
companyId: null,
function loadSession(): { token: string; user: User; companyId: string } | null {
try {
const raw = sessionStorage.getItem('forte-auth')
if (!raw) return null
return JSON.parse(raw)
} catch {
return null
}
}
function saveSession(token: string, user: User, companyId: string) {
sessionStorage.setItem('forte-auth', JSON.stringify({ token, user, companyId }))
}
function clearSession() {
sessionStorage.removeItem('forte-auth')
}
export const useAuthStore = create<AuthState>((set) => {
const initial = typeof window !== 'undefined' ? loadSession() : null
return {
token: initial?.token ?? null,
user: initial?.user ?? null,
companyId: initial?.companyId ?? null,
setAuth: (token, user) => {
const payload = decodeJwtPayload(token)
saveSession(token, user, payload.companyId)
set({ token, user, companyId: payload.companyId })
},
logout: () => {
clearSession()
set({ token: null, user: null, companyId: null })
},
}))
}})

View File

@@ -0,0 +1,50 @@
import { create } from 'zustand'
import { applyThemeColors, getThemeByName } from '@/lib/themes'
type Mode = 'light' | 'dark' | 'system'
interface ThemeState {
mode: Mode
colorTheme: string
setMode: (mode: Mode) => void
setColorTheme: (name: string) => void
}
function isDarkMode(mode: Mode): boolean {
return mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
}
function apply(mode: Mode, colorTheme: string) {
const dark = isDarkMode(mode)
document.documentElement.classList.toggle('dark', dark)
const theme = getThemeByName(colorTheme)
applyThemeColors(dark ? theme.dark : theme.light)
}
export const useThemeStore = create<ThemeState>((set) => {
const initialMode = (typeof window !== 'undefined' ? localStorage.getItem('forte-mode') as Mode : null) ?? 'system'
const initialColor = (typeof window !== 'undefined' ? localStorage.getItem('forte-color-theme') : null) ?? 'slate'
if (typeof window !== 'undefined') {
apply(initialMode, initialColor)
}
return {
mode: initialMode,
colorTheme: initialColor,
setMode: (mode) => {
localStorage.setItem('forte-mode', mode)
const colorTheme = useThemeStore.getState().colorTheme
apply(mode, colorTheme)
set({ mode })
},
setColorTheme: (name) => {
localStorage.setItem('forte-color-theme', name)
const mode = useThemeStore.getState().mode
apply(mode, name)
set({ colorTheme: name })
},
}
})