dc78d8fea3
Componentes React reutilizables: card, dialog, tabs, select, alert, badge, button, input, label, skeleton, tooltip, progress_bar, page_header, form_field, settings_page, crud_page, analytics_page, dashboard_layout. Charts: area, bar, line, sparkline, kpi_card, chart_container. Hooks Wails: use_wails_query, use_wails_mutation, use_wails_stream, use_wails_event, use_animated_canvas. Funciones core: cn, format_compact, chart_colors, get_series_color, wails_cache, theme_config_to_colors. Tipos: chart_series, wails_ipc, theme_config, component_variants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
102 lines
3.8 KiB
TypeScript
102 lines
3.8 KiB
TypeScript
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react'
|
|
|
|
interface ThemeColors {
|
|
[key: string]: string
|
|
}
|
|
|
|
interface Theme {
|
|
name: string
|
|
label: string
|
|
colors: ThemeColors
|
|
}
|
|
|
|
interface ThemeContextValue {
|
|
theme: Theme
|
|
themeName: string
|
|
setTheme: (name: string) => void
|
|
themes: Record<string, Theme>
|
|
}
|
|
|
|
const STORAGE_KEY = 'frontend-library-theme'
|
|
const ThemeContext = createContext<ThemeContextValue | null>(null)
|
|
|
|
function applyThemeColors(theme: Theme) {
|
|
const root = document.documentElement
|
|
const cssVarMap: Record<string, string> = {
|
|
background: '--background', foreground: '--foreground',
|
|
card: '--card', cardForeground: '--card-foreground',
|
|
popover: '--popover', popoverForeground: '--popover-foreground',
|
|
primary: '--primary', primaryForeground: '--primary-foreground',
|
|
secondary: '--secondary', secondaryForeground: '--secondary-foreground',
|
|
muted: '--muted', mutedForeground: '--muted-foreground',
|
|
accent: '--accent', accentForeground: '--accent-foreground',
|
|
destructive: '--destructive', destructiveForeground: '--destructive-foreground',
|
|
success: '--success', successForeground: '--success-foreground',
|
|
warning: '--warning', warningForeground: '--warning-foreground',
|
|
info: '--info', infoForeground: '--info-foreground',
|
|
surface: '--surface', surfaceHover: '--surface-hover', overlay: '--overlay',
|
|
border: '--border', input: '--input', ring: '--ring',
|
|
chart1: '--chart-1', chart2: '--chart-2', chart3: '--chart-3', chart4: '--chart-4', chart5: '--chart-5',
|
|
sidebar: '--sidebar', sidebarForeground: '--sidebar-foreground',
|
|
sidebarPrimary: '--sidebar-primary', sidebarPrimaryForeground: '--sidebar-primary-foreground',
|
|
sidebarAccent: '--sidebar-accent', sidebarAccentForeground: '--sidebar-accent-foreground',
|
|
sidebarBorder: '--sidebar-border', sidebarRing: '--sidebar-ring',
|
|
}
|
|
Object.entries(cssVarMap).forEach(([key, cssVar]) => {
|
|
if (theme.colors[key]) root.style.setProperty(cssVar, theme.colors[key])
|
|
})
|
|
if (theme.name === 'dark' || theme.name === 'midnight' || theme.name === 'sunset') {
|
|
root.classList.add('dark')
|
|
} else {
|
|
root.classList.remove('dark')
|
|
}
|
|
}
|
|
|
|
interface ThemeProviderProps {
|
|
children: ReactNode
|
|
themes: Record<string, Theme>
|
|
defaultTheme?: string
|
|
}
|
|
|
|
export function ThemeProvider({ children, themes, defaultTheme: initialTheme }: ThemeProviderProps) {
|
|
const [themeName, setThemeName] = useState<string>(() => {
|
|
if (initialTheme) return initialTheme
|
|
if (typeof window === 'undefined') return 'default'
|
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
if (stored && themes[stored]) return stored
|
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark'
|
|
return 'default'
|
|
})
|
|
|
|
const theme = themes[themeName] ?? themes['default'] ?? Object.values(themes)[0]
|
|
|
|
const setTheme = useCallback((name: string) => {
|
|
if (themes[name]) {
|
|
setThemeName(name)
|
|
localStorage.setItem(STORAGE_KEY, name)
|
|
}
|
|
}, [themes])
|
|
|
|
useEffect(() => { applyThemeColors(theme) }, [theme])
|
|
|
|
useEffect(() => {
|
|
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
const handle = (e: MediaQueryListEvent) => {
|
|
if (!localStorage.getItem(STORAGE_KEY)) setThemeName(e.matches ? 'dark' : 'default')
|
|
}
|
|
mq.addEventListener('change', handle)
|
|
return () => mq.removeEventListener('change', handle)
|
|
}, [])
|
|
|
|
return <ThemeContext.Provider value={{ theme, themeName, setTheme, themes }}>{children}</ThemeContext.Provider>
|
|
}
|
|
|
|
export function useTheme() {
|
|
const ctx = useContext(ThemeContext)
|
|
if (!ctx) throw new Error('useTheme must be used within a ThemeProvider')
|
|
return ctx
|
|
}
|
|
|
|
export { ThemeContext }
|
|
export type { ThemeContextValue, ThemeProviderProps, Theme, ThemeColors }
|