feat: Implement module-specific accent colors and enhance theme integration

This commit introduces module-specific accent colors and further integrates
the primary and secondary theme colors throughout the application's UI components.

Key features:
- Four new customizable accent colors, one for each main module:
    - Empleados
    - Tareas
    - Planillas
    - Asistencias
- These accent colors are configurable in the Settings view and persist in local storage.
- Broader application of global primary and secondary colors to common UI elements like navigation bars.
- Module-specific components now use their designated accent color for key visual elements (e.g., primary buttons, titles, icons, borders), providing better visual differentiation between modules.

Changes include:
- Extended `ui/src/stores/useUi.js` to manage state for the four new module accent colors, including actions and local storage persistence.
- Added a "Module Accent Colors" section with color pickers to `ui/src/views/SettingsView.vue`.
- Updated `ui/src/App.vue` to expose these module accent colors as CSS variables (e.g., `--accent-color-empleados`).
- Systematically updated styles in general UI components (`ui/src/components/ui/`) and all module-specific components (`ui/src/components/{module}/` and `ui/src/views/{module}/`) to utilize the new global and module-specific theme colors.
- Updated unit tests for `useUi.js` and component tests for `SettingsView.vue` to cover the new accent color functionality. A bug related to color input event handling in `SettingsView.vue` was identified and fixed during this process.

This enhancement provides a more visually distinct and customizable experience across different sections of the application.
This commit is contained in:
google-labs-jules[bot]
2025-05-31 00:09:55 +00:00
parent 4f1ec58a99
commit b5c8d88113
27 changed files with 3908 additions and 154 deletions

View File

@@ -0,0 +1,256 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useUi } from '../useUi' // Adjust path as necessary
// Mock localStorage
const localStorageMock = (() => {
let store = {}
return {
getItem: vi.fn((key) => store[key] || null),
setItem: vi.fn((key, value) => {
store[key] = value.toString()
}),
clear: vi.fn(() => {
store = {}
}),
removeItem: vi.fn((key) => {
delete store[key]
}),
}
})()
// Define the storage key, matching the one in useUi.js
const APPEARANCE_STORAGE_KEY = 'appearanceSettings';
// Apply the mock to window.localStorage BEFORE store import or usage
vi.stubGlobal('localStorage', localStorageMock)
describe('useUi Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
localStorageMock.clear()
localStorageMock.setItem.mockClear()
localStorageMock.getItem.mockClear()
// Ensure that when the store is initialized, it re-reads from the (mocked) localStorage
// This is important because the store's state definition runs only once when imported.
// For tests, we need to control this. Re-importing or using a factory for useUi might be needed
// if the store is not re-evaluating its state function that calls loadSettingsFromLocalStorage().
// However, Pinia's setup with setActivePinia(createPinia()) should handle store isolation.
})
it('initializes with default appearance settings if no local storage data exists', () => {
const store = useUi()
expect(store.primaryColor).toBe('#1976D2')
expect(store.theme).toBe('light')
expect(store.fontSize).toBe(16)
// Check new accent color defaults
expect(store.accentColorEmpleados).toBe('#2196F3')
expect(store.accentColorTareas).toBe('#4CAF50')
expect(store.accentColorPlanillas).toBe('#FF9800')
expect(store.accentColorAsistencias).toBe('#E91E63')
expect(localStorageMock.getItem).toHaveBeenCalledWith(APPEARANCE_STORAGE_KEY)
})
it('loads settings from localStorage including new accent colors if present', () => {
const storedSettings = {
primaryColor: '#FF0000',
theme: 'dark',
fontSize: 20,
animationsEnabled: false,
accentColorEmpleados: '#0000FF',
accentColorTareas: '#00FF00',
accentColorPlanillas: '#FFFF00',
accentColorAsistencias: '#FF00FF',
// other settings...
}
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(storedSettings))
const store = useUi()
expect(localStorageMock.getItem).toHaveBeenCalledWith(APPEARANCE_STORAGE_KEY)
expect(store.primaryColor).toBe('#FF0000')
expect(store.theme).toBe('dark')
expect(store.fontSize).toBe(20)
expect(store.animationsEnabled).toBe(false)
expect(store.accentColorEmpleados).toBe('#0000FF')
expect(store.accentColorTareas).toBe('#00FF00')
expect(store.accentColorPlanillas).toBe('#FFFF00')
expect(store.accentColorAsistencias).toBe('#FF00FF')
})
it('loads older settings from localStorage and uses defaults for new accent colors if not present', () => {
const olderStoredSettings = {
primaryColor: '#ABCDEF',
theme: 'dark',
fontSize: 18,
} // Does not include new accent colors
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(olderStoredSettings))
const store = useUi()
expect(store.primaryColor).toBe('#ABCDEF')
expect(store.theme).toBe('dark')
expect(store.fontSize).toBe(18)
// New accent colors should fall back to defaults
expect(store.accentColorEmpleados).toBe('#2196F3')
expect(store.accentColorTareas).toBe('#4CAF50')
expect(store.accentColorPlanillas).toBe('#FF9800')
expect(store.accentColorAsistencias).toBe('#E91E63')
})
it('falls back to default settings if localStorage data is invalid JSON', () => {
localStorageMock.getItem.mockReturnValueOnce('invalid json')
const store = useUi()
expect(store.primaryColor).toBe('#1976D2') // Default
})
it('falls back to default settings if localStorage is not available (simulated by load error)', () => {
// Simulate localStorage.getItem throwing an error by making the mock throw
localStorageMock.getItem.mockImplementationOnce(() => {
throw new Error("Storage unavailable");
});
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); // Suppress console.error for this test
const store = useUi();
expect(store.primaryColor).toBe('#1976D2'); // Should use default
expect(store.theme).toBe('light');
expect(console.error).toHaveBeenCalledWith('Error loading appearance settings from local storage:', expect.any(Error));
consoleErrorSpy.mockRestore(); // Restore console.error
})
describe('Actions', () => {
// Update this list to match the store's appearanceSettingKeys
const appearanceSettingKeysInTest = [
'primaryColor', 'secondaryColor', 'warningColor', 'fontFamily',
'fontSize', 'animationsEnabled', 'backgroundColor', 'theme',
'accentColorEmpleados', 'accentColorTareas', 'accentColorPlanillas', 'accentColorAsistencias',
]
it('setPrimaryColor updates state and saves to localStorage', () => {
const store = useUi()
store.setPrimaryColor('#00FF00')
expect(store.primaryColor).toBe('#00FF00')
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"primaryColor":"#00FF00"')
)
})
it('setFontSize updates state and saves to localStorage', () => {
const store = useUi()
store.setFontSize(24)
expect(store.fontSize).toBe(24)
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"fontSize":24')
)
})
it('setAnimationsEnabled updates state and saves to localStorage', () => {
const store = useUi()
store.setAnimationsEnabled(false)
expect(store.animationsEnabled).toBe(false)
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"animationsEnabled":false')
)
store.setAnimationsEnabled(true)
expect(store.animationsEnabled).toBe(true)
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"animationsEnabled":true')
)
})
it('setTheme updates state and saves to localStorage', () => {
const store = useUi()
store.setTheme('dark')
expect(store.theme).toBe('dark')
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"theme":"dark"')
)
})
it('toggleTheme switches theme and saves to localStorage', () => {
const store = useUi() // default is 'light'
store.toggleTheme()
expect(store.theme).toBe('dark')
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"theme":"dark"')
)
store.toggleTheme()
expect(store.theme).toBe('light')
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"theme":"light"')
)
})
it('saves only appearance settings to localStorage', () => {
const store = useUi()
// Clear any previous calls from initialization if store was already used in this describe block
localStorageMock.setItem.mockClear();
store.setPrimaryColor('#ABCDEF') // This will trigger a save
// Check if setItem was called
expect(localStorageMock.setItem).toHaveBeenCalledTimes(1);
expect(localStorageMock.setItem).toHaveBeenCalledWith(APPEARANCE_STORAGE_KEY, expect.any(String));
// Now parse the actual saved data
const savedDataString = localStorageMock.setItem.mock.calls[0][1];
const savedData = JSON.parse(savedDataString);
expect(Object.keys(savedData).length).toBe(appearanceSettingKeysInTest.length);
expect(savedData.sidebarOpen).toBeUndefined() // Ensure non-appearance data is not saved
appearanceSettingKeysInTest.forEach(key => {
expect(savedData.hasOwnProperty(key)).toBe(true)
})
})
// Tests for new accent color actions
it('setAccentColorEmpleados updates state and saves to localStorage', () => {
const store = useUi()
store.setAccentColorEmpleados('#FF1122')
expect(store.accentColorEmpleados).toBe('#FF1122')
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"accentColorEmpleados":"#FF1122"')
)
})
it('setAccentColorTareas updates state and saves to localStorage', () => {
const store = useUi()
store.setAccentColorTareas('#33FF44')
expect(store.accentColorTareas).toBe('#33FF44')
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"accentColorTareas":"#33FF44"')
)
})
it('setAccentColorPlanillas updates state and saves to localStorage', () => {
const store = useUi()
store.setAccentColorPlanillas('#5566FF')
expect(store.accentColorPlanillas).toBe('#5566FF')
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"accentColorPlanillas":"#5566FF"')
)
})
it('setAccentColorAsistencias updates state and saves to localStorage', () => {
const store = useUi()
store.setAccentColorAsistencias('#FF7788')
expect(store.accentColorAsistencias).toBe('#FF7788')
expect(localStorageMock.setItem).toHaveBeenCalledWith(
APPEARANCE_STORAGE_KEY,
expect.stringContaining('"accentColorAsistencias":"#FF7788"')
)
})
})
})