feat: funciones frontend React/TS — componentes UI, hooks Wails, charts y tipos
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>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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 }
|
||||
Reference in New Issue
Block a user