@voice-ai-workforce/react
Version:
React components with 3-tier interface modes for voice-controlled workforce applications
209 lines (208 loc) • 7.6 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
// packages/react/src/hooks/useVoiceTheme.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { DEFAULT_VOICE_AI_THEME } from '../types/theme';
import { mergeThemes, generateCSSCustomProperties } from '../utils/theme';
// Create theme context
const VoiceThemeContext = createContext(null);
// Theme provider component
export const VoiceThemeProvider = ({ children, initialTheme, enableDarkMode = true, persistTheme = true, storageKey = 'voice-ai-theme', }) => {
const [customTheme, setCustomTheme] = useState(null);
const [isDark, setIsDark] = useState(false);
// Load theme from localStorage on mount
useEffect(() => {
if (persistTheme && typeof window !== 'undefined') {
try {
const savedTheme = localStorage.getItem(storageKey);
const savedDarkMode = localStorage.getItem(`${storageKey}-dark`);
if (savedTheme) {
setCustomTheme(JSON.parse(savedTheme));
}
if (savedDarkMode) {
setIsDark(JSON.parse(savedDarkMode));
}
else if (enableDarkMode) {
// Auto-detect system dark mode preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
setIsDark(prefersDark);
}
}
catch (error) {
console.warn('Failed to load theme from localStorage:', error);
}
}
else if (initialTheme) {
setCustomTheme(initialTheme);
}
}, [persistTheme, storageKey, initialTheme, enableDarkMode]);
// Create merged theme
const theme = React.useMemo(() => {
let baseTheme = DEFAULT_VOICE_AI_THEME;
// Apply dark mode adjustments
if (isDark) {
baseTheme = {
...baseTheme,
colors: {
...baseTheme.colors,
background: '#1F2937',
surface: '#374151',
border: '#4B5563',
text: {
primary: '#F9FAFB',
secondary: '#D1D5DB',
muted: '#9CA3AF',
inverse: '#111827',
},
},
};
}
// Merge with custom theme
if (customTheme) {
baseTheme = mergeThemes(baseTheme, customTheme);
}
return baseTheme;
}, [customTheme, isDark]);
// Update theme
const setTheme = React.useCallback((newTheme) => {
// @ts-ignore
setCustomTheme(prev => mergeThemes(prev || {}, newTheme));
if (persistTheme && typeof window !== 'undefined') {
try {
localStorage.setItem(storageKey, JSON.stringify(newTheme));
}
catch (error) {
console.warn('Failed to save theme to localStorage:', error);
}
}
}, [persistTheme, storageKey]);
// Reset theme to default
const resetTheme = React.useCallback(() => {
setCustomTheme(null);
if (persistTheme && typeof window !== 'undefined') {
try {
localStorage.removeItem(storageKey);
}
catch (error) {
console.warn('Failed to remove theme from localStorage:', error);
}
}
}, [persistTheme, storageKey]);
// Toggle dark mode
const toggleDarkMode = React.useCallback(() => {
const newDarkMode = !isDark;
setIsDark(newDarkMode);
if (persistTheme && typeof window !== 'undefined') {
try {
localStorage.setItem(`${storageKey}-dark`, JSON.stringify(newDarkMode));
}
catch (error) {
console.warn('Failed to save dark mode preference:', error);
}
}
}, [isDark, persistTheme, storageKey]);
// Apply CSS custom properties
useEffect(() => {
if (typeof window !== 'undefined') {
const cssProps = generateCSSCustomProperties(theme);
const root = document.documentElement;
Object.entries(cssProps).forEach(([property, value]) => {
root.style.setProperty(property, value);
});
}
}, [theme]);
const contextValue = {
theme,
setTheme,
resetTheme,
isDark,
toggleDarkMode,
};
return (_jsx(VoiceThemeContext.Provider, { value: contextValue, children: children }));
};
// Hook to use theme context
export const useVoiceTheme = () => {
const context = useContext(VoiceThemeContext);
if (!context) {
// Return default theme if no provider
console.warn('useVoiceTheme must be used within a VoiceThemeProvider. Using default theme.');
return {
theme: DEFAULT_VOICE_AI_THEME,
setTheme: () => { },
resetTheme: () => { },
isDark: false,
toggleDarkMode: () => { },
};
}
return context;
};
// Hook for component-level theme customization
export const useComponentTheme = (customTheme) => {
const { theme: globalTheme } = useVoiceTheme();
const componentTheme = React.useMemo(() => {
if (!customTheme)
return globalTheme;
return mergeThemes(globalTheme, customTheme);
}, [globalTheme, customTheme]);
return componentTheme;
};
// Hook for getting theme-aware styles
export const useThemeStyles = () => {
const { theme } = useVoiceTheme();
return React.useMemo(() => ({
// Button styles
primaryButton: {
backgroundColor: theme.colors.primary,
color: theme.colors.text.inverse,
borderColor: theme.colors.primary,
},
secondaryButton: {
backgroundColor: theme.colors.secondary,
color: theme.colors.text.inverse,
borderColor: theme.colors.secondary,
},
ghostButton: {
backgroundColor: 'transparent',
color: theme.colors.text.primary,
borderColor: theme.colors.border,
},
// Panel styles
panel: {
backgroundColor: theme.colors.surface,
borderColor: theme.colors.border,
color: theme.colors.text.primary,
boxShadow: theme.shadows.lg,
},
// Status styles
statusOnline: {
backgroundColor: theme.colors.status.online,
},
statusOffline: {
backgroundColor: theme.colors.status.offline,
},
statusProcessing: {
backgroundColor: theme.colors.status.processing,
},
statusListening: {
backgroundColor: theme.colors.status.listening,
},
}), [theme]);
};
// Hook for responsive theme values
export const useResponsiveTheme = () => {
const { theme } = useVoiceTheme();
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
return {
theme,
isMobile,
spacing: isMobile ? theme.spacing.sm : theme.spacing.md,
fontSize: isMobile ? theme.typography.fontSize.sm : theme.typography.fontSize.md,
};
};