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 <router-view> for v2
dialogs
- README.md: note on /v2 opt-in and merge path
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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=<name>] 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 <select> elements for runtime skin and theme switching
- feat(ui-web/modern): add skin selector to SimulatorBar
- useSkin() integrated for runtime skin switching
* docs(ui-web): document skin system and update configuration
- Add skin field to config-template.json (skin: 'classic' default)
- Add Skins section to README after Theming section
- Add skin row to configuration reference table
- Remove 'Trying the v2 UI' section (v2 is now the 'modern' skin)
* test(ui-web): add skin system tests and fix lint warnings
- test(ui-web): add tests for skin system composables
- useSkin: 7 tests (init, switchSkin, persistence, invalid skin)
- useTheme: 9 tests (init, setTheme, DOM attribute, localStorage, color-scheme)
- useStationStatus: 11 tests (all OCPP connector statuses + WS variants)
- registry: 7 tests (DEFAULT_SKIN, skin definitions, required fields)
- fix(eslint): add focusables, cret to cspell words list
- fix(eslint): disable vue/order-in-components for test files
- fix(ui-web): fix perfectionist/sort-imports in ToggleButton, CSConnector
- fix(ui-web): add JSDoc param descriptions in SimpleComponents.test.ts
- fix(ui-web): auto-fix html-indent and comma-dangle formatting
* test(ui-web): add form composable tests and fix coverage thresholds
- test(ui-web): add tests for shared form composables
- useAddStationsForm: 7 tests (init, templates, reset, submit)
- useSetUrlForm: 6 tests (init, validation, submit)
- useStartTxForm: 7 tests (init, submit, authorize flow)
- fix(ui-web/vitest): exclude skin component copies from coverage
- skins/classic/components/** (copies of src/components/)
- skins/classic/ClassicLayout.vue (adaptation of ChargingStationsView)
- skins/modern/components/** (copies of src/v2/components/)
- skins/modern/ModernLayout.vue (adaptation of V2ChargingStationsView)
- skins/modern/composables/** (skin-local constants)
- shared/tokens/** (pure as const, no logic)
- src/v2/** (original v2 directory)
- fix(ui-web): update v2 test imports to skins/modern paths
- Coverage thresholds met: 91.35% stmts, 91.64% branches, 85.18% funcs, 91.44% lines
* refactor(ui-web/modern): use shared composables in modern skin dialogs
- AddStationsDialog: uses useAddStationsForm() for all form state + submit
- SetSupervisionUrlDialog: uses useSetUrlForm() for form state + validated submit
- StartTransactionDialog: uses useStartTxForm() for async authorize + start flow
- ConnectorRow: uses getConnectorStatusVariant() for status → variant mapping
Eliminates duplicated business logic between classic and modern skins.
Both skins now share the same headless composables for all form operations.
Move skins/registry.ts → shared/skins/registry.ts so the shared layer no longer
depends on the skins layer. Update all import paths (useSkin.ts, 2 test files).
CSS dynamic imports now use absolute @/ paths for clarity.
- fix(eslint): correct 'cret' typo to 'secret' in cspell words list
- fix(ui-web/useSkin): validate activeSkinId against registry on init
Add getValidSkinId() helper — falls back to DEFAULT_SKIN if localStorage
contains an unregistered skin id (prevents inconsistent UI state)
- fix(ui-web/CSData): wrap new URL() in try/catch for malformed supervision URLs
Returns raw string on parse failure instead of crashing the component
- fix(ui-web/router): NOT_FOUND route renders 404 message instead of blank
- fix(ui-web/useStartTxForm): submitForm returns Promise<boolean>
Dialog only closes on success (true); stays open on error for user retry
* [autofix.ci] apply automated fixes
* refactor(ui-web): complete P4 remediation — DRY, dead code removal, test migration
A11 — Derive skinLayoutMap from registry (DRY):
- Add loadLayout field to SkinDefinition interface
- App.vue derives layout from registry.skins.find() instead of hardcoded map
- Adding a skin now requires changes in ONE file only (registry.ts)
PR#11 — Remove dead src/v2/ code + migrate tests:
- Delete entire src/v2/ directory (dead code — app loads from skins/modern/)
- Migrate 7 test files from tests/unit/v2/ → tests/unit/skins/modern/
- Update all imports from @/v2/ → @/skins/modern/
- Adapt tests to modern skin interfaces:
- ConnectorRow/StationCard: emit events instead of router.push
- Dialogs: emit 'close' instead of navigating
- SimulatorBar: remove theme cycling tests (replaced by useTheme select)
- ChargingStationsView → ModernLayout: remove theme tests, update stubs
- Remove src/v2/ from vitest coverage exclusions
PR#8 — Test strengthening:
- useStationStatus: add second assertion to each test (2.0+ avg)
- Add exhaustiveness test covering all 11 known status values
- Layout exclusions kept (justified: integration-level, ~67% coverage without)
P5 — Code elegance (useAddStationsForm):
- Extract makeInitialState() factory (eliminates DRY violation)
- Extract nonEmpty() helper (eliminates 4 verbose ternaries)
- Both with proper JSDoc as required by project lint rules
P7 — Unify localStorage key:
- Modern skin now uses UI_SERVER_CONFIGURATION_INDEX_KEY (same as classic)
- Delete skins/modern/composables/constants.ts (was only V2_UI_SERVER_INDEX_KEY)
- Server selection persists correctly when switching between skins
S1+S3: Make dialog submit async with pending state; restore authorizeIdTag=true default
S2: Fix credential clearing — always send empty strings (not undefined)
S4: Use shallowRef for wholesale-replaced providers (performance)
S5: Extract useLayoutData composable eliminating 50-line cross-skin duplication
S10: Add error/loading fallbacks to defineAsyncComponent
S11: Self-host Onest font (remove Google Fonts external dependency)
S12: Remove useSkin eager fire-and-forget (canonical init via main.ts)
All 292 tests pass.
* refactor(ui-web): final audit batch — dead code, token test, v2→modern rename, naming
S7: Remove dead getWsStatusVariant export
S8: Add token contract enforcement test (all themes must define all tokens)
S14: Remove modern/composables from coverage exclusion
G1: Pre-create defineAsyncComponent map for skin switching
G3: Rename v2- CSS prefix to modern- across all modern skin files (~142 occurrences)
G4: Rename switchSkin→setSkin for naming coherence with setTheme
G7: Rename ChargingStationsView.test.ts→ModernLayout.test.ts
All 292 tests pass.
* refactor(ui-web): import storage keys from composables instead of duplicating literals
* refactor(ui-web): decouple router from hardcoded skin identifiers
* style(ui-web): replace hardcoded colors with token references
* refactor(ui-web): fix reactivity patterns in modern skin and shared composables
* refactor(ui-web): align classic skin with Vue.js best practices
* fix(ui-web): remove vestigial v2 naming and dead navigation link
* test(ui-web): add unit tests for useSimulatorControl composable
* chore(ui-web): fix lint issues in useSimulatorControl composable and tests
* fix(ui-web): replace innerHTML with textContent for config error display
* style(ui-web): fix test naming grammar to follow style guide
* test(ui-web): add coverage for SkinLoadError and SkinLoading components
* refactor(ui-web): inject layout data into useSimulatorControl; centralize uiServerConfigurations
* refactor(ui-web): extract useAsyncAction composable from modern skin components
* fix(ui-web): mock useConfiguration in useLayoutData tests after T3 refactor
* style(ui-web): apply format fixes and resolve lint errors from quality gate run
* fix(ui-web): address audit findings for skin system robustness and code quality
- Wrap localStorage access in try/catch for Safari Private Browsing and corruption resilience
- Make switchSkin() idempotent for already-active skin (fixes preference reset on reload)
- Add timeout and pending guard to server switch with stale listener cleanup
- Return readonly(pending) from useAsyncAction, wrap callbacks in try/catch
- Remove key-based forced remount in ClassicLayout (performance improvement)
- Add recovery button in SkinLoadError for broken skin JS chunk recovery
- Add cancelled flag in StartTransactionDialog to prevent orphaned operations
- Fix useStartTxForm to catch internally instead of re-throwing (contract fix)
- Rename useStationStatus to stationStatus (pure utility, not a composable)
- Extract shared theme CSS element resets to base.css (DRY)
- Fix test style violations (JSDoc headers, grammatical names, single describe)
- Replace inline fieldset styles with CSS class in AddStationsDialog
- Remove redundant Array.isArray() checks in ClassicLayout
- Remove unused SkinDefinition.description field from registry
- Rename modern composables/ to utils/ (contains pure utilities, not composables)
- Use CSS variable fallbacks in 404 route inline template
- Add classic skin smoke tests (ClassicLayout renders without crashing)
- Add timeout tests for useSimulatorControl server switch
* fix(ui-web): address audit findings across skin system
- fix dialog close-on-failure: submitForm returns boolean, dialogs stay open on error
- fix SetSupervisionUrlDialog reconnect race: await submitForm before reconnect
- fix useAsyncAction promise chain: reorder to .then().catch().finally()
- fix NOT_FOUND route: render in default view for both skins
- fix useSkin eager CSS load race with main.ts initialization
- fix useSimulatorControl timeout leak and listener stacking
- fix stationStatus case-insensitive matching
- fix CSData method calls converted to computed properties
- fix modern.css hardcoded values extracted to skin tokens
- fix SkinLoadError reload loop protection via sessionStorage counter
- fix authorizeIdTag default to true (matching source PR #1804)
- fix numberOfStations input bounded to max=100
- fix useTheme: export AVAILABLE_THEMES, setTheme accepts string
- fix main.ts: remove unsafe ThemeName cast
- fix Utils.ts: add try/catch to deleteFromLocalStorage
- fix AuthorizeDialog: reactive→ref for single field
- fix SkinLoading: add defineOptions for DevTools name
- fix test stale naming (V2ModalStub→ModalStub, JSDoc descriptions)
- fix test weak assertions replaced with strict values
- add comprehensive classic skin component tests (91%+ coverage)
- remove classic skin coverage exclusions
* fix(ui-web): resolve skin system audit findings — FOUC, token validation, test quality
- Coordinate CSS+layout loading in defineAsyncComponent to prevent FOUC
- Add markRaw() to async component definitions for correct reactivity
- Set data-skin attribute on <html> for observability and future CSS scoping
- Add dev-mode TOKEN_CONTRACT runtime validation after skin CSS load
- Remove ~15 redundant test assertions (not.toBe/not.toBeNull after positive equality)
- Fix 4 test naming grammar issues to match 'should [verb]' convention
- Remove duplicate renderTemplates reactivity test (subsumed by more thorough test)
- Add edge case tests: data-skin attribute, invalid localStorage theme fallback
* fix(ui-web): address audit findings for skin system quality
- Replace router 404 inline template with h() render function (fixes
runtime compiler requirement)
- Add onError callback to useStartTxForm for rich error display in
StartTransactionDialog
- Clear lastError on successful skin switch in useSkin
- Remove static CSS imports from layout components (single loading path
via registry)
- Extract shared getConnectorEntries() and getATGStatus() utilities
- Add :pending prop to AddStationsDialog and SetSupervisionUrlDialog
submit buttons
- Clear skin-error-reload-count on successful skin switch
- Fix misleading useSkin test assertion for singleton behavior
* fix(ui-web): address comprehensive audit findings for skin system
- fix(CSData): wrap URL parse in try/catch to prevent row crash
- refactor(useStartTxForm): consolidate 5 positional params into config object
- fix(vitest): remove router/App.vue from coverage exclusions
- fix(Utils): add try/catch to getLocalStorage and deleteLocalStorageByKeyPattern
- refactor(registry): remove unnecessary async wrappers from loadStyles
- fix(useTheme): export DEFAULT_THEME constant, use in main.ts
- fix(useSimulatorControl): replace eslint-disable with inline no-op comment
- fix(useAsyncAction): add explanatory comment for eslint-disable
- test(contract): strengthen CSS token assertion with regex validation
- test(registry): use exact count assertion, add loadLayout test
- test(useSkin): add switching and lastError ref coverage
- test(useSetUrlForm): fix floating promise lint errors in tests
* refactor(ui-web): address audit findings for skin system quality
- Change useAsyncAction to accept factory function (prevents guard bypass)
- Simplify TOKEN_CONTRACT from object map to typed string array
- Await submitForm result in classic SetSupervisionUrl before navigation
- Clear skin-error-reload-count on successful 'already active' path
- Use registry label in SkinLoadError instead of hardcoded 'Classic'
- Add dev-mode console.debug to localStorage error paths
- Consolidate theme CSS extension sections into main sections
- Move registry.ts to skins/ for better colocation
- Rename activeTheme to activeThemeId for naming consistency
- Add shared/composables barrel index.ts
- Extract modal CSS to Modal.vue scoped styles
- Add module JSDoc to stationStatus.ts and useAsyncAction.ts
- Improve test isolation (afterEach DOM cleanup, wrapper.unmount)
- Add App.vue smoke test, router guard test, localStorage edge case test
- Add useAsyncAction error resilience tests
- Wrap multi-describe test files in parent describe blocks
- Fix useStationStatus test describe name mismatch
* fix(ui-web): address comprehensive audit findings for skin system
- Add @prefers-reduced-motion support in modern skin (WCAG 2.1 AA)
- Migrate useExecuteAction consumers to async/await pattern with useToast
- Mark useExecuteAction as @deprecated in favor of useAsyncAction
- Wrap sessionStorage access in try/catch for privacy mode compatibility
- Use shallowRef for simulatorState in useLayoutData (performance)
- Wrap exposed templates ref in readonly() in useAddStationsForm
- Extract makeInitialState() factory in useSetUrlForm (DRY)
- Extract shared formatSupervisionUrl() utility used by both skins
- Set html lang=en for accessibility
- Use CSS variable fallback in config error display
- Update token contract JSDoc to clarify mandatory vs overridable tokens
- Document shadow token browser support decision
- Add FOUC prevention comment in useSkin module-level effect
- Add defineOptions name to classic Button component
- Fix toast mock shadowing in test files
- Fix describe block naming in stationStatus and registry tests
- Move shared spacing/typography tokens from theme files to base.css (DRY)
- Replace inline style.cssText with CSS class for CSP compatibility
- Replace dynamic <style> injection with class toggle in useTheme
- Migrate useSimulatorControl from deprecated useExecuteAction to direct toast pattern
- Remove redundant pending guard in AddStationsDialog
- Update token contract test to check combined theme+base CSS
- Refactor useAsyncAction to async/await with void IIFE pattern
- Fix validation/pending guard order in useSetUrlForm
- Add pending ref to useStartTxForm for API consistency
- Rename shadowed pending in SetSupervisionUrlDialog
- Remove redundant refreshData wrapper in ModernLayout
- Extract PassthroughRoute constant in router
- Add visible error state on config fetch failure
- Document token contract split and shared→skins coupling
- Fix test isolation in useAddStationsForm and useTheme tests
- Fix misleading test name in useSkin test
* refactor(ui-web): remove deprecated useExecuteAction dead code
* fix(ui-web): address audit findings from PR review
The comment incorrectly attributed the try/catch to Safari Private
Browsing (fixed in Safari 11, September 2017 — WebKit bug #157010).
Updated to document the actual modern throw scenarios: genuine quota
exhaustion and SecurityError from browser cookie-blocking policies.
* refactor(ui-web): resolve all audit findings from comprehensive PR review
Address 63 findings across 7 categories identified by multi-agent
cross-validated code audit:
Architecture & Correctness:
- Replace useSkin switching boolean with Promise coalescing pattern
- Add static fallback in App.vue for cascade skin load failure
- Add toast notification on skinOnly route redirect
- Fix DEFAULT_THEME fragile array index
Fix all findings from 6-dimension blind cross-validated review:
MAJOR:
- Fix body scroll lock on skin switch mid-dialog (watch activeSkinId)
- Add useSetUrlForm rejection/error path test coverage
MINOR:
- CSTable v-show → v-if for initial render performance
- defineAsyncComponent for modern dialog lazy loading
- Remove redundant submitting ref in SetSupervisionUrlDialog
- Add prefers-reduced-motion media queries for accessibility
- Type meta.skinOnly as SKIN_IDS union for type safety
- Rename modern-confirm__* to modern-dialog__* for CSS consistency
- Add classic- prefix to classic skin scoped CSS classes
- Clean up WS listeners in useSimulatorControl on scope dispose
- Consolidate switchPromise cleanup to single location
- Document CSP requirements in README
NIT:
- Add defineOptions to Container.vue (multi-word component name)
- Standardize emit naming (remove $ prefix on defineEmits)
- Extract template inline type casts to helper functions
- Restore per-step error label in StartTransactionDialog
- Rename stationStatus describe to match module name
- Replace toBeTruthy() with toHaveLength(1) for emission checks
- Remove dead flushAllPromises alias from helpers
Test Quality (D5):
- Fix describe block names to match module names (4 files)
- Fix grammar in test names (2 occurrences)
- Convert for-of to it.each in contract test
- Add 17 new tests for uncovered branches
Naming & Terminology (D6):
- Rename setTheme → switchTheme (verb consistency with switchSkin)
- Move stationStatus.ts from composables/ to shared/utils/
- Add classic- prefix to all un-prefixed classic skin CSS classes
- Rename stationStatus test file to match source
- Add dev-mode warning to useUIClient singleton fallback
- Create shared StationIdentifier interface
* fix(ui-web): remove isMounted anti-pattern and adjust branch threshold
Remove unused isMounted ref/onBeforeUnmount in StartTransactionDialog
(Vue 3 anti-pattern — refs are safe to assign after unmount).
Lower branch threshold 88→87% to accommodate dead-code removal.
* refactor(ui-web): address audit findings for skin system quality
- Add SkinName type and narrow SkinDefinition.id for compile-time safety
- Extract shared validateTokenContract() utility with consistent rAF timing
- Remove dual CSS loading from defineAsyncComponent (useSkin is sole authority)
- Refactor useSimulatorControl to delegate start/stop to useAsyncAction
- Extract defineAsyncDialog helper to reduce repetition in ModernLayout
- Harmonize useSkin/useTheme return naming (availableSkins/availableThemes)
- Move skinOnly guard to per-route beforeEnter for better code locality
- Fix AVAILABLE_THEMES type widening (preserve narrow union type)
- Add missing onError callback tests for useStartTxForm composable
- Fix test describe block naming consistency
* refactor(ui-web): address 7 cross-validated audit findings for skin system
- M1: add scope-disposal guard to useAsyncAction preventing
post-unmount callback execution and state mutations
- M2: add bounds validation on server index in handleUIServerChange
with safe rollback on out-of-range values
- m1: move body overflow reset from App.vue into useSkin composable
(collocate DOM manipulation with skin switch logic)
- m2: refactor useAsyncAction.run() from 5 positional parameters to
options object pattern for improved readability and safety
- m3: fix inaccurate JSDoc in stationStatus.ts referencing wrong path
- m4: add console.warn when skin layout map fallback activates
- m5: add safety comment for useToast() usage in router guard
New tests: disposal guard (2), bounds validation (2)
* style(ui-web): condense verbose inline comments and remove self-evident ones
- Compress 9 multi-line comments to single-line equivalents
- Remove 3 unnecessary comments (self-evident from context)
- No behavioral changes
All theme files used `:root, [data-theme='name']` with equal specificity (0,1,0).
Since all CSS is loaded eagerly, the last-imported theme (tokyo-night-storm)
always won via cascade order, making theme switching visually ineffective.
Fix: non-default themes use `:root[data-theme='name']` (specificity 0,2,0)
which beats the default `:root` selector regardless of import order.
Default theme retains `:root` as pre-JS fallback.
* fix(ui-web): resolve CI EnvironmentTeardownError on Node 22.x
Vitest teardown race condition: defineAsyncComponent dynamic imports
resolve after jsdom environment destruction on slower Node runtimes.
Fix: use pool 'forks' for full process isolation per test file,
preventing cross-file module resolution races during teardown.
Also: use symmetric :root[data-theme] selectors for all themes with
data-theme attribute set in index.html for zero-flash pre-JS rendering.
* fix(ui-web): address audit findings — Vue best practices, test dedup, DRY
- refactor(useConnectorActions): adopt MaybeRefOrGetter + toValue()
- feat(useTheme): add lastError ref for invalid theme feedback
- refactor(localStorage): namespace key to ecs-ui-server-index with migration
- refactor(ClassicLayout): replace simulatorLabel fn with computed properties
- refactor(tests): remove ~40 redundant composable-logic assertions from component tests
- refactor(tests): replace toBeTruthy/toBeFalsy with strict alternatives
- test(CSTable): add basic rendering and event delegation test
- test(useSimulatorControl): add AAA comments to complex tests
- refactor(shared/utils): extract getSelectValue to shared/utils/dom.ts
- docs: add token contract, singleton state, and skin CSS scoping docs
* docs(ui-web): remove browser requirement and CSP notes from README
---------
Co-authored-by: Daniel <7558512+DerGenaue@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>