From: Jérôme Benoit Date: Wed, 29 Apr 2026 22:25:44 +0000 (+0200) Subject: feat(ui-web): implement runtime skin system with classic and modern skins (#1815) X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=72aba1edf1957107024a043cbbd122fc0a4ee552;p=e-mobility-charging-stations-simulator.git feat(ui-web): implement runtime skin system with classic and modern skins (#1815) * feat(ui-web): add opt-in v2 UI with Material-inspired design Parallel Vue 3 UI at /v2 that mirrors the existing feature set with a flat Material-inspired palette, card-based station layout, modal dialogs (Teleport + focus trap), grouped action buttons with clear hierarchy, and a theme toggle (auto/light/dark, persisted per-user). The v2 tree is fully self-contained under src/v2/; only three files outside that folder change: - router/index.ts: five /v2/* routes mirroring the existing ones - App.vue: v1/v2 toggle link + top-level named for v2 dialogs - README.md: note on /v2 opt-in and merge path Co-Authored-By: Claude Opus 4.7 (1M context) * test(ui-web): add unit tests for the v2 UI subtree Covers every v2 file (ActionButton, StatePill, Modal, ConfirmDialog, SimulatorBar, StationCard, ConnectorRow, V2NotFoundView, the four dialogs, v2Errors, and V2ChargingStationsView) under a dedicated tests/unit/v2/ folder so it can be dropped as a unit when v1 is removed. Takes the global coverage back above the existing 91/89/83/91 thresholds (lines 93.2%, branches 92.2%, functions 86.1%, statements 92.3%) after the v2 subtree added 643 previously-untested lines. Mocks the Teleport-based Modal with an inline stub in dialog tests so wrapper.find() reaches the form inputs. * feat(ui-web): add skin+theme schema, token contract, and skin registry - feat(ui-common): add skin and theme fields to configuration schema - ConfigurationData: add skin?: string alongside existing theme?: string - configurationSchema: add skin and theme as optional string fields - Fixes pre-existing gap where theme was typed but not Zod-validated - feat(ui-web): extend theme CSS files with unified token contract - All 3 theme files: add [data-theme=] selector for runtime switching - Add 9 new semantic tokens: bg-raised, bg-sunken, state-ok/warn/err/idle, spacing-xl, font-size-base/xs; color-scheme: dark|light - All existing token names and values unchanged (backward compatible) - feat(ui-web): add TOKEN_CONTRACT TypeScript interface in shared/tokens - 33-entry as const map of semantic name to CSS custom property name - Exports TokenName and CssCustomProperty derived types - feat(ui-web): add skin registry with classic and modern skin definitions - SkinDefinition interface with lazy CSS loader for code splitting - DEFAULT_SKIN = 'classic' - CSS module shim added to shims-vue.d.ts for TypeScript resolution * feat(ui-web): add useSkin and useTheme composables - useSkin: module-level singleton for runtime skin switching - activeSkinId ref initialized from localStorage (fallback: 'classic') - switchSkin() validates, lazy-loads CSS, persists preference - Eager load of initial skin styles at module init - useTheme: module-level singleton for runtime theme switching - activeTheme ref initialized from localStorage (fallback: 'tokyo-night-storm') - setTheme() sets data-theme attribute + color-scheme on document root - ThemeName union type exported for component type safety - Themes: catppuccin-latte | sap-horizon | tokyo-night-storm (alphabetical) - eslint.config.js: add 'catppuccin' to cspell words list * feat(ui-web): add shared composables and classic skin structure - feat(ui-web): add shared headless composables - useStationStatus: pure OCPP status → 'ok'|'warn'|'err'|'idle' mapping - useAddStationsForm: all 12 form fields + submit/reset for adding stations - useSetUrlForm: supervision URL form state + validated submit - useStartTxForm: start transaction with optional authorize flow - feat(ui-web): create skins/classic/ directory structure - ClassicLayout.vue: table-based root layout (adapted from ChargingStationsView) - classic.css: structural tokens for classic skin - Full component tree: Container, Button, StateButton, ToggleButton, CSTable, CSData, CSConnector, and 3 action components - Internal imports use relative paths; @/composables remain shared - fix(eslint): add skins/classic/ paths to multi-word component name exceptions * feat(ui-web): create skins/modern/ structure with migrated CSS tokens - feat(ui-web): modern skin directory structure - ModernLayout.vue: card-based root layout (from V2ChargingStationsView.vue) - CSS import updated to './modern.css' - Component imports updated to './components/...' - Theme cycle logic removed (replaced by unified useTheme() in T18) - 7 base components + 4 dialog components copied from v2 - composables/constants.ts: skin-local route/key constants - composables/errors.ts: error handling utilities - feat(ui-web): migrate modern.css CSS tokens to unified contract - 0 --v2-primary refs remain (replaced by --color-primary) - 0 --v2-state-* refs remain (replaced by --color-state-*) - 0 [data-v2-theme] blocks remain (removed — theme via unified system) - 242 var(--v2-*) references migrated to --skin-* or --color-* tokens - Structural tokens renamed: --v2-space/radius/elev/font/* → --skin-space/radius/shadow/font/* - File reduced from 1311 to 1218 lines (93 lines of redundant light overrides removed) * feat(ui-web): integrate skin system into App.vue, main.ts, and router - feat(ui-web/App.vue): replace dual-view shell with skin switching shell - defineAsyncComponent layoutMap for ClassicLayout + ModernLayout - :key={activeSkinId} forces full remount on skin switch - Fade transition (opacity 0.2s) between skins - App.vue reduced to 46 lines (pure orchestration, no skin logic) - feat(ui-web/main.ts): replace dynamic theme loading with static imports - All 3 theme CSS files loaded eagerly at boot - Theme applied via useTheme().setTheme() composable - Skin preference initialized from config.skin → useSkin().switchSkin() - Config.skin and config.theme both respected with localStorage override - feat(ui-web/router): remove v2 routes, keep v1 skin-agnostic routes - Remove all /v2/* routes and V2 component imports - Keep named 'action' view routes for classic skin sidebar panels - Action panel components loaded lazily via async imports - No skin-specific paths in router (both skins share same URLs) - fix(ui-web/ClassicLayout.vue): add router-view name='action' for sidebar * refactor(ui-web): integrate shared composables into both skins and add skin/theme switchers - refactor(ui-web/classic): action components use shared headless composables - AddChargingStations: uses useAddStationsForm() for all form state + submit - SetSupervisionUrl: uses useSetUrlForm() for form state + validated submit - StartTransaction: uses useStartTxForm() for form state + async submit - feat(ui-web/classic): add skin/theme selectors to ClassicLayout top bar - useSkin() provides activeSkinId, skins, switchSkin - useTheme() provides activeTheme, availableThemes, setTheme - Two ` element's change event. + * @param event - The DOM change event from a `