357 lines
9.7 KiB
TypeScript
357 lines
9.7 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
|
|
export type Theme = 'light' | 'dark' | 'auto';
|
|
export type Language = 'es' | 'en' | 'eu';
|
|
export type ViewMode = 'list' | 'grid' | 'card';
|
|
export type SidebarState = 'expanded' | 'collapsed' | 'hidden';
|
|
|
|
// Toast interface kept for backward compatibility but toast functionality
|
|
// has been moved to src/utils/toast.ts using react-hot-toast
|
|
// This interface is deprecated and will be removed in a future version
|
|
export interface Toast {
|
|
id: string;
|
|
type: 'success' | 'error' | 'warning' | 'info';
|
|
title: string;
|
|
message?: string;
|
|
duration?: number;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
};
|
|
persistent?: boolean;
|
|
}
|
|
|
|
export interface Modal {
|
|
id: string;
|
|
type: 'dialog' | 'drawer' | 'fullscreen';
|
|
title: string;
|
|
content: React.ReactNode;
|
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
closeable?: boolean;
|
|
onClose?: () => void;
|
|
}
|
|
|
|
export interface UIState {
|
|
// Theme & Appearance
|
|
theme: Theme;
|
|
language: Language;
|
|
sidebarState: SidebarState;
|
|
compactMode: boolean;
|
|
reducedMotion: boolean;
|
|
|
|
// Layout & Navigation
|
|
currentPage: string;
|
|
breadcrumbs: Array<{ label: string; path: string }>;
|
|
viewMode: ViewMode;
|
|
|
|
// Loading States
|
|
globalLoading: boolean;
|
|
loadingStates: Record<string, boolean>;
|
|
|
|
// Modals & Dialogs
|
|
modals: Modal[];
|
|
|
|
// User Preferences
|
|
preferences: {
|
|
showTips: boolean;
|
|
autoSave: boolean;
|
|
confirmActions: boolean;
|
|
defaultPageSize: number;
|
|
dateFormat: string;
|
|
numberFormat: string;
|
|
timezone: string;
|
|
};
|
|
|
|
// Actions
|
|
setTheme: (theme: Theme) => void;
|
|
setLanguage: (language: Language) => void;
|
|
setSidebarState: (state: SidebarState) => void;
|
|
setCompactMode: (compact: boolean) => void;
|
|
setReducedMotion: (reduced: boolean) => void;
|
|
|
|
setCurrentPage: (page: string) => void;
|
|
setBreadcrumbs: (breadcrumbs: Array<{ label: string; path: string }>) => void;
|
|
setViewMode: (mode: ViewMode) => void;
|
|
|
|
setGlobalLoading: (loading: boolean) => void;
|
|
setLoading: (key: string, loading: boolean) => void;
|
|
isLoading: (key: string) => boolean;
|
|
|
|
showModal: (modal: Omit<Modal, 'id'>) => string;
|
|
hideModal: (id: string) => void;
|
|
clearModals: () => void;
|
|
|
|
updatePreference: <K extends keyof UIState['preferences']>(
|
|
key: K,
|
|
value: UIState['preferences'][K]
|
|
) => void;
|
|
resetPreferences: () => void;
|
|
}
|
|
|
|
const defaultPreferences = {
|
|
showTips: true,
|
|
autoSave: true,
|
|
confirmActions: true,
|
|
defaultPageSize: 25,
|
|
dateFormat: 'DD/MM/YYYY',
|
|
numberFormat: 'european', // european, american
|
|
timezone: 'Europe/Madrid',
|
|
};
|
|
|
|
export const useUIStore = create<UIState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
// Initial state
|
|
theme: 'dark',
|
|
language: 'es',
|
|
sidebarState: 'expanded',
|
|
compactMode: false,
|
|
reducedMotion: false,
|
|
|
|
currentPage: '',
|
|
breadcrumbs: [],
|
|
viewMode: 'list',
|
|
|
|
globalLoading: false,
|
|
loadingStates: {},
|
|
|
|
modals: [],
|
|
|
|
preferences: defaultPreferences,
|
|
|
|
// Theme & Appearance actions
|
|
setTheme: (theme: Theme) => {
|
|
set({ theme });
|
|
|
|
// Apply theme to document
|
|
const root = document.documentElement;
|
|
if (theme === 'dark') {
|
|
root.classList.add('dark');
|
|
root.classList.remove('light');
|
|
} else if (theme === 'light') {
|
|
root.classList.add('light');
|
|
root.classList.remove('dark');
|
|
} else {
|
|
// Auto theme - check system preference
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
if (prefersDark) {
|
|
root.classList.add('dark');
|
|
root.classList.remove('light');
|
|
} else {
|
|
root.classList.add('light');
|
|
root.classList.remove('dark');
|
|
}
|
|
}
|
|
},
|
|
|
|
setLanguage: (language: Language) => {
|
|
set({ language });
|
|
// Trigger i18n language change only if different
|
|
import('../i18n').then(({ default: i18n }) => {
|
|
if (i18n.language !== language) {
|
|
i18n.changeLanguage(language);
|
|
}
|
|
});
|
|
},
|
|
|
|
setSidebarState: (sidebarState: SidebarState) => {
|
|
set({ sidebarState });
|
|
},
|
|
|
|
setCompactMode: (compactMode: boolean) => {
|
|
set({ compactMode });
|
|
},
|
|
|
|
setReducedMotion: (reducedMotion: boolean) => {
|
|
set({ reducedMotion });
|
|
|
|
// Apply reduced motion preference
|
|
const root = document.documentElement;
|
|
if (reducedMotion) {
|
|
root.classList.add('reduce-motion');
|
|
} else {
|
|
root.classList.remove('reduce-motion');
|
|
}
|
|
},
|
|
|
|
// Navigation actions
|
|
setCurrentPage: (currentPage: string) => {
|
|
set({ currentPage });
|
|
},
|
|
|
|
setBreadcrumbs: (breadcrumbs: Array<{ label: string; path: string }>) => {
|
|
set({ breadcrumbs });
|
|
},
|
|
|
|
setViewMode: (viewMode: ViewMode) => {
|
|
set({ viewMode });
|
|
},
|
|
|
|
// Loading actions
|
|
setGlobalLoading: (globalLoading: boolean) => {
|
|
set({ globalLoading });
|
|
},
|
|
|
|
setLoading: (key: string, loading: boolean) => {
|
|
set((state) => ({
|
|
loadingStates: {
|
|
...state.loadingStates,
|
|
[key]: loading,
|
|
},
|
|
}));
|
|
},
|
|
|
|
isLoading: (key: string): boolean => {
|
|
return get().loadingStates[key] ?? false;
|
|
},
|
|
|
|
// Modal actions
|
|
showModal: (modal: Omit<Modal, 'id'>): string => {
|
|
const id = `modal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
const newModal: Modal = {
|
|
...modal,
|
|
id,
|
|
size: modal.size ?? 'md',
|
|
closeable: modal.closeable ?? true,
|
|
};
|
|
|
|
set((state) => ({
|
|
modals: [...state.modals, newModal],
|
|
}));
|
|
|
|
return id;
|
|
},
|
|
|
|
hideModal: (id: string) => {
|
|
const { modals } = get();
|
|
const modal = modals.find(m => m.id === id);
|
|
|
|
// Call onClose callback if provided
|
|
if (modal?.onClose) {
|
|
modal.onClose();
|
|
}
|
|
|
|
set((state) => ({
|
|
modals: state.modals.filter(modal => modal.id !== id),
|
|
}));
|
|
},
|
|
|
|
clearModals: () => {
|
|
// Call onClose for all modals
|
|
const { modals } = get();
|
|
modals.forEach(modal => {
|
|
if (modal.onClose) {
|
|
modal.onClose();
|
|
}
|
|
});
|
|
|
|
set({ modals: [] });
|
|
},
|
|
|
|
// Preferences actions
|
|
updatePreference: <K extends keyof UIState['preferences']>(
|
|
key: K,
|
|
value: UIState['preferences'][K]
|
|
) => {
|
|
set((state) => ({
|
|
preferences: {
|
|
...state.preferences,
|
|
[key]: value,
|
|
},
|
|
}));
|
|
},
|
|
|
|
resetPreferences: () => {
|
|
set({ preferences: defaultPreferences });
|
|
},
|
|
}),
|
|
{
|
|
name: 'ui-storage',
|
|
storage: createJSONStorage(() => localStorage),
|
|
partialize: (state) => ({
|
|
theme: state.theme,
|
|
language: state.language,
|
|
sidebarState: state.sidebarState,
|
|
compactMode: state.compactMode,
|
|
reducedMotion: state.reducedMotion,
|
|
viewMode: state.viewMode,
|
|
preferences: state.preferences,
|
|
}),
|
|
}
|
|
)
|
|
);
|
|
|
|
// Selectors for common use cases
|
|
export const useLanguage = () => useUIStore((state) => state.language);
|
|
export const useSidebar = () => useUIStore((state) => ({
|
|
state: state.sidebarState,
|
|
setState: state.setSidebarState,
|
|
}));
|
|
export const useCompactMode = () => useUIStore((state) => state.compactMode);
|
|
export const useViewMode = () => useUIStore((state) => state.viewMode);
|
|
|
|
export const useLoading = (key?: string) => {
|
|
if (key) {
|
|
return useUIStore((state) => state.isLoading(key));
|
|
}
|
|
return useUIStore((state) => state.globalLoading);
|
|
};
|
|
|
|
export const useModals = () => useUIStore((state) => state.modals);
|
|
|
|
export const useBreadcrumbs = () => useUIStore((state) => ({
|
|
breadcrumbs: state.breadcrumbs,
|
|
setBreadcrumbs: state.setBreadcrumbs,
|
|
}));
|
|
|
|
export const usePreferences = () => useUIStore((state) => state.preferences);
|
|
|
|
// Hook for UI actions
|
|
export const useUIActions = () => useUIStore((state) => ({
|
|
setTheme: state.setTheme,
|
|
setLanguage: state.setLanguage,
|
|
setSidebarState: state.setSidebarState,
|
|
setCompactMode: state.setCompactMode,
|
|
setReducedMotion: state.setReducedMotion,
|
|
setCurrentPage: state.setCurrentPage,
|
|
setBreadcrumbs: state.setBreadcrumbs,
|
|
setViewMode: state.setViewMode,
|
|
setGlobalLoading: state.setGlobalLoading,
|
|
setLoading: state.setLoading,
|
|
showModal: state.showModal,
|
|
hideModal: state.hideModal,
|
|
clearModals: state.clearModals,
|
|
updatePreference: state.updatePreference,
|
|
resetPreferences: state.resetPreferences,
|
|
}));
|
|
|
|
// Initialize theme on store creation
|
|
if (typeof window !== 'undefined') {
|
|
// Set initial theme based on stored preference or system preference
|
|
const storedState = localStorage.getItem('ui-storage');
|
|
if (storedState) {
|
|
try {
|
|
const { state } = JSON.parse(storedState);
|
|
useUIStore.getState().setTheme(state.theme || 'dark');
|
|
} catch (error) {
|
|
console.warn('Failed to parse stored UI state:', error);
|
|
useUIStore.getState().setTheme('dark');
|
|
}
|
|
} else {
|
|
useUIStore.getState().setTheme('dark');
|
|
}
|
|
|
|
// Listen for system theme changes
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
const { theme } = useUIStore.getState();
|
|
if (theme === 'auto') {
|
|
useUIStore.getState().setTheme('auto');
|
|
}
|
|
});
|
|
|
|
// Listen for reduced motion preference
|
|
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', (e) => {
|
|
useUIStore.getState().setReducedMotion(e.matches);
|
|
});
|
|
} |