]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ui-web): fix audit findings — dead code, layer violations, composable extraction
authorJérôme Benoit <jerome.benoit@sap.com>
Thu, 30 Apr 2026 08:46:15 +0000 (10:46 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Thu, 30 Apr 2026 08:46:15 +0000 (10:46 +0200)
24 files changed:
ui/web/src/assets/themes/catppuccin-latte.css
ui/web/src/assets/themes/sap-horizon.css
ui/web/src/assets/themes/teal-dark.css
ui/web/src/assets/themes/teal-light.css
ui/web/src/assets/themes/tokyo-night-storm.css
ui/web/src/core/Constants.ts
ui/web/src/core/UIClient.ts
ui/web/src/core/index.ts
ui/web/src/main.ts
ui/web/src/router/index.ts
ui/web/src/shared/components/SkinLoadError.vue
ui/web/src/shared/components/SkinLoading.vue
ui/web/src/shared/composables/useServerSwitch.ts [new file with mode: 0644]
ui/web/src/shared/composables/useSimulatorControl.ts
ui/web/src/shared/composables/useSkin.ts
ui/web/src/shared/types.ts [deleted file]
ui/web/src/shared/utils/index.ts
ui/web/src/skins/modern/modern.css
ui/web/src/skins/registry.ts
ui/web/tests/unit/App.test.ts
ui/web/tests/unit/UIClient.test.ts
ui/web/tests/unit/Utils.test.ts
ui/web/tests/unit/shared/composables/useSkin.test.ts
ui/web/tests/unit/skins/registry.test.ts

index ae5cba6bbb22854e34ad4e0cfcd6994d638b98d9..cc00f058ccb78417a7e42158b26f5aa35b692969 100644 (file)
@@ -12,6 +12,9 @@
   --ctp-overlay1: #8c8fa1;
   --ctp-blue: #1e66f5;
   --ctp-lavender: #7287fd;
+  --ctp-green: #2e7d32;
+  --ctp-orange: #ef6c00;
+  --ctp-red: #c62828;
 
   /* Semantic */
   --color-bg: var(--ctp-base);
@@ -38,9 +41,9 @@
   --color-bg-sunken: var(--ctp-crust);
 
   /* State colors (Material 700 for light mode readability) */
-  --color-state-ok: #2e7d32;
-  --color-state-warn: #ef6c00;
-  --color-state-err: #c62828;
+  --color-state-ok: var(--ctp-green);
+  --color-state-warn: var(--ctp-orange);
+  --color-state-err: var(--ctp-red);
   --color-state-idle: var(--ctp-overlay1);
 
   color-scheme: light;
index c74975d394b8f98a6f2cfce7ee35b1f0b301364a..07950e6dc649f97c062e9777a964e628f81243ef 100644 (file)
@@ -17,6 +17,9 @@
   --sap-button-border: #bcc3ca;
   --sap-button-text: #0064d9;
   --sap-button-emphasized-bg: #0070f2;
+  --sap-positive: #256f3a;
+  --sap-critical: #e76500;
+  --sap-negative: #aa0808;
   --sap-white: #fff;
 
   /* Semantic */
@@ -44,9 +47,9 @@
   --color-bg-sunken: var(--sap-hover);
 
   /* State colors (SAP Horizon palette) */
-  --color-state-ok: #256f3a;
-  --color-state-warn: #e76500;
-  --color-state-err: #aa0808;
+  --color-state-ok: var(--sap-positive);
+  --color-state-warn: var(--sap-critical);
+  --color-state-err: var(--sap-negative);
   --color-state-idle: var(--sap-label);
 
   color-scheme: light;
index 898ad7d2e3d4351881ba30a0b7b2627c4bd6fd00..a0c756021b9d8d7cfcb502875ba62291aca40be6 100644 (file)
@@ -12,6 +12,9 @@
   --td-fg-muted: #78909c;
   --td-teal: #26a69a;
   --td-teal-dim: #00897b;
+  --td-green: #66bb6a;
+  --td-amber: #ffb300;
+  --td-red: #ef5350;
   --td-white: #ffffff;
 
   /* Semantic */
@@ -39,9 +42,9 @@
   --color-bg-sunken: var(--td-bg-dark);
 
   /* State colors */
-  --color-state-ok: #66bb6a;
-  --color-state-warn: #ffb300;
-  --color-state-err: #ef5350;
+  --color-state-ok: var(--td-green);
+  --color-state-warn: var(--td-amber);
+  --color-state-err: var(--td-red);
   --color-state-idle: var(--td-fg-muted);
 
   color-scheme: dark;
index f26c8c11c76c2d6778812d1e1510162b175af605..2610101623685a520e3e327c16a3ab11f3185ba7 100644 (file)
@@ -12,6 +12,9 @@
   --tl-fg-muted: #697386;
   --tl-teal: #009688;
   --tl-teal-dim: #00796b;
+  --tl-green: #2e7d32;
+  --tl-orange: #ef6c00;
+  --tl-red: #c62828;
   --tl-white: #ffffff;
 
   /* Semantic */
@@ -39,9 +42,9 @@
   --color-bg-sunken: var(--tl-bg-active);
 
   /* State colors */
-  --color-state-ok: #2e7d32;
-  --color-state-warn: #ef6c00;
-  --color-state-err: #c62828;
+  --color-state-ok: var(--tl-green);
+  --color-state-warn: var(--tl-orange);
+  --color-state-err: var(--tl-red);
   --color-state-idle: var(--tl-fg-muted);
 
   color-scheme: light;
index f05fe5783824053629ae56d321200f0ba28bc6b1..ce6999ebd1bf219934625d5b14474a84d9ef1cb3 100644 (file)
@@ -12,6 +12,9 @@
   --tn-fg-muted: #8089b3;
   --tn-blue: #7aa2f7;
   --tn-accent: #3d59a1;
+  --tn-green: #66bb6a;
+  --tn-amber: #ffb300;
+  --tn-red: #ef5350;
   --tn-white: #ffffff;
 
   /* Semantic */
@@ -39,9 +42,9 @@
   --color-bg-sunken: var(--tn-bg-raised);
 
   /* State colors */
-  --color-state-ok: #66bb6a;
-  --color-state-warn: #ffb300;
-  --color-state-err: #ef5350;
+  --color-state-ok: var(--tn-green);
+  --color-state-warn: var(--tn-amber);
+  --color-state-err: var(--tn-red);
   --color-state-idle: var(--tn-fg-muted);
 
   color-scheme: dark;
index e92023c9ec811489fc8a5c58d7a945a07129e1b0..854f43c26dd9d6cfaaf43320533a470e07447b1e 100644 (file)
@@ -1,6 +1,9 @@
+import { type SKIN_IDS } from 'ui-common'
+
 // Local UI project constants
 
 export const ASYNC_COMPONENT_DELAY_MS = 200
+export const DEFAULT_SKIN: (typeof SKIN_IDS)[number] = 'classic'
 export const ASYNC_COMPONENT_TIMEOUT_MS = 10_000
 export const EMPTY_VALUE_PLACEHOLDER = 'Ø'
 export const MAX_SKIN_ERROR_RELOADS = 2
index c6a97d76b14c5d1aaa32f67713bc5c9d788dd1b5..18ff2732c46b53e53e5f4b2b822c51e93e399f43 100644 (file)
@@ -46,7 +46,7 @@ export class UIClient {
     return UIClient.instance
   }
 
-  public static isOCPP20x (version: OCPPVersion | undefined): boolean {
+  private static isOCPP20x (version: OCPPVersion | undefined): boolean {
     return version === OCPPVersion.VERSION_20 || version === OCPPVersion.VERSION_201
   }
 
index e730115f9cd3db608ba3ae9aa06e1b1f3dac2dc4..231af0bfa0f26d51d951341feec484bdc27ba165 100644 (file)
@@ -1,6 +1,7 @@
 export {
   ASYNC_COMPONENT_DELAY_MS,
   ASYNC_COMPONENT_TIMEOUT_MS,
+  DEFAULT_SKIN,
   EMPTY_VALUE_PLACEHOLDER,
   LEGACY_UI_SERVER_CONFIG_KEY,
   MAX_SKIN_ERROR_RELOADS,
@@ -22,7 +23,6 @@ export {
   useUIClient,
 } from './providers.js'
 export {
-  deleteFromLocalStorage,
   deleteLocalStorageByKeyPattern,
   getFromLocalStorage,
   getLocalStorage,
index 164b0be1fb880b4034bd5fbb186cb6a49cf0eaf9..21dbacb99c375af9a7efba7c5e1928ca0d880f6d 100644 (file)
@@ -11,6 +11,7 @@ import App from '@/App.vue'
 import {
   chargingStationsKey,
   configurationKey,
+  DEFAULT_SKIN,
   getFromLocalStorage,
   LEGACY_UI_SERVER_CONFIG_KEY,
   setToLocalStorage,
@@ -22,7 +23,6 @@ import {
 import { router } from '@/router/index.js'
 import { SKIN_STORAGE_KEY, useSkin } from '@/shared/composables/useSkin.js'
 import { DEFAULT_THEME, THEME_STORAGE_KEY, useTheme } from '@/shared/composables/useTheme.js'
-import { DEFAULT_SKIN } from '@/skins/registry.js'
 
 import 'vue-toast-notification/dist/theme-bootstrap.css'
 
index b1685a20da7f2bc8df850c0b823426f2d0fbbcd4..bd4ff989b1d2c9f07fd6e079571f9a3ad4ae2486 100644 (file)
@@ -3,9 +3,8 @@ import { h } from 'vue'
 import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vue-router'
 import { useToast } from 'vue-toast-notification'
 
-import { ROUTE_NAMES } from '@/core/index.js'
+import { DEFAULT_SKIN, ROUTE_NAMES } from '@/core/index.js'
 import { useSkin } from '@/shared/composables/useSkin.js'
-import { DEFAULT_SKIN } from '@/skins/registry.js'
 
 declare module 'vue-router' {
   interface RouteMeta {
index aad032821e8dccb53b396a7905d2d743dd721d3b..a60ebf80c8ec92e601ec01cb7a8c3e2d4b22bd5f 100644 (file)
 </template>
 
 <script setup lang="ts">
-import { MAX_SKIN_ERROR_RELOADS, setToLocalStorage } from '@/core/index.js'
+import { DEFAULT_SKIN, MAX_SKIN_ERROR_RELOADS, setToLocalStorage } from '@/core/index.js'
 import { SKIN_STORAGE_KEY } from '@/shared/composables/useSkin.js'
-import { DEFAULT_SKIN, skins } from '@/skins/registry.js'
+// Intentional: registry.ts is pure metadata (ids, labels, loaders) — no behavioral coupling.
+import { skins } from '@/skins/registry.js'
 
 defineEmits<{ retry: [] }>()
 
@@ -55,7 +56,7 @@ function resetToDefault (): void {
   gap: 1rem;
   min-height: 50vh;
   padding: 2rem;
-  color: var(--color-text, #e0e0e0);
+  color: var(--color-text);
   font-family: system-ui, sans-serif;
 }
 
@@ -70,6 +71,6 @@ function resetToDefault (): void {
 }
 
 .skin-load-error button:hover {
-  background: rgba(255, 255, 255, 0.08);
+  background: var(--color-bg-hover);
 }
 </style>
index bbfb70ed1535d7af7572e14eb32ec170bd90b3f9..05865d7f9d9267bde8cf18c1ca33b0833ab10663 100644 (file)
@@ -18,7 +18,7 @@ defineOptions({ name: 'SkinLoading' })
   gap: 1rem;
   min-height: 50vh;
   padding: 2rem;
-  color: var(--color-text, #e0e0e0);
+  color: var(--color-text);
   font-family: system-ui, sans-serif;
 }
 
diff --git a/ui/web/src/shared/composables/useServerSwitch.ts b/ui/web/src/shared/composables/useServerSwitch.ts
new file mode 100644 (file)
index 0000000..6666601
--- /dev/null
@@ -0,0 +1,125 @@
+import type { UIServerConfigurationSection } from 'ui-common'
+import type { Ref } from 'vue'
+
+import { onScopeDispose, readonly, ref } from 'vue'
+
+import {
+  getFromLocalStorage,
+  setToLocalStorage,
+  UI_SERVER_CONFIGURATION_INDEX_KEY,
+  useConfiguration,
+  useUIClient,
+} from '@/core/index.js'
+
+export interface ServerSwitchActions {
+  /** Switches the active UI server, with error rollback on connection failure. */
+  handleUIServerChange: (newIndex: number) => void
+  /** Whether a server switch operation is in progress. */
+  serverSwitchPending: Readonly<Ref<boolean>>
+}
+
+export interface ServerSwitchOptions {
+  /** Called after a successful server switch (e.g. to clear UI state). */
+  onServerSwitched?: () => void
+  /** Registers WS event listeners after connection reconfiguration. */
+  registerWSEventListeners: () => void
+  /** Unregisters WS event listeners before connection reconfiguration. */
+  unregisterWSEventListeners: () => void
+}
+
+const SERVER_SWITCH_TIMEOUT_MS = 15_000
+
+/**
+ * Composable encapsulating the UI server switch state machine.
+ *
+ * Handles optimistic connection switching with timeout-based rollback on failure.
+ * @param options - Callbacks for event listener management and switch completion
+ * @returns Server switch actions and pending state
+ */
+export function useServerSwitch (options: ServerSwitchOptions): ServerSwitchActions {
+  const $uiClient = useUIClient()
+  const $configuration = useConfiguration()
+
+  const { registerWSEventListeners, unregisterWSEventListeners } = options
+
+  const serverSwitchPending = ref(false)
+  let activeTimeoutId: ReturnType<typeof setTimeout> | undefined
+  let pendingOpenHandler: (() => void) | undefined
+  let pendingErrorHandler: (() => void) | undefined
+
+  const handleUIServerChange = (newIndex: number): void => {
+    const currentIndex = getFromLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, 0)
+    if (newIndex === currentIndex || serverSwitchPending.value) return
+
+    const servers = $configuration.value.uiServer as UIServerConfigurationSection[]
+    if (newIndex < 0 || newIndex >= servers.length) return
+
+    serverSwitchPending.value = true
+
+    $uiClient.setConfiguration(servers[newIndex])
+    unregisterWSEventListeners()
+    registerWSEventListeners()
+
+    let settled = false
+
+    const openHandler = (): void => {
+      if (settled) return
+      settled = true
+      clearTimeout(activeTimeoutId)
+      $uiClient.unregisterWSEventListener('error', errorHandler)
+      pendingOpenHandler = undefined
+      pendingErrorHandler = undefined
+      setToLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, newIndex)
+      serverSwitchPending.value = false
+      options.onServerSwitched?.()
+    }
+
+    const errorHandler = (): void => {
+      if (settled) return
+      settled = true
+      clearTimeout(activeTimeoutId)
+      $uiClient.unregisterWSEventListener('open', openHandler)
+      pendingOpenHandler = undefined
+      pendingErrorHandler = undefined
+      serverSwitchPending.value = false
+      const previousIndex = getFromLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, 0)
+      const rollbackServers = $configuration.value.uiServer as UIServerConfigurationSection[]
+      if (previousIndex >= 0 && previousIndex < rollbackServers.length) {
+        $uiClient.setConfiguration(rollbackServers[previousIndex])
+      }
+      unregisterWSEventListeners()
+      registerWSEventListeners()
+    }
+
+    $uiClient.registerWSEventListener('open', openHandler, { once: true })
+    $uiClient.registerWSEventListener('error', errorHandler, { once: true })
+    pendingOpenHandler = openHandler
+    pendingErrorHandler = errorHandler
+
+    activeTimeoutId = setTimeout(() => {
+      if (!settled) {
+        errorHandler()
+      }
+    }, SERVER_SWITCH_TIMEOUT_MS)
+  }
+
+  onScopeDispose(() => {
+    if (activeTimeoutId != null) {
+      clearTimeout(activeTimeoutId)
+      activeTimeoutId = undefined
+    }
+    if (pendingOpenHandler != null) {
+      $uiClient.unregisterWSEventListener('open', pendingOpenHandler)
+      pendingOpenHandler = undefined
+    }
+    if (pendingErrorHandler != null) {
+      $uiClient.unregisterWSEventListener('error', pendingErrorHandler)
+      pendingErrorHandler = undefined
+    }
+  })
+
+  return {
+    handleUIServerChange,
+    serverSwitchPending: readonly(serverSwitchPending),
+  }
+}
index ef30d32c5e91297686de00f0f9d9ee5119c6cede..30e2188c8ce6bea3a1b5731af6e0bdbd50e355c4 100644 (file)
@@ -1,18 +1,11 @@
-import type { UIServerConfigurationSection } from 'ui-common'
 import type { ComputedRef, Ref } from 'vue'
 
-import { computed, onScopeDispose, readonly, ref } from 'vue'
+import { computed } from 'vue'
 
-import {
-  getFromLocalStorage,
-  setToLocalStorage,
-  UI_SERVER_CONFIGURATION_INDEX_KEY,
-  useChargingStations,
-  useConfiguration,
-  useUIClient,
-} from '@/core/index.js'
+import { useChargingStations, useUIClient } from '@/core/index.js'
 import { useAsyncAction } from '@/shared/composables/useAsyncAction.js'
 import { type LayoutData } from '@/shared/composables/useLayoutData.js'
+import { useServerSwitch } from '@/shared/composables/useServerSwitch.js'
 
 export interface SimulatorControlActions {
   /** Switches the active UI server, with error rollback on connection failure. */
@@ -48,7 +41,6 @@ export function useSimulatorControl (
   options?: SimulatorControlOptions
 ): SimulatorControlActions {
   const $uiClient = useUIClient()
-  const $configuration = useConfiguration()
   const $chargingStations = useChargingStations()
 
   const { getSimulatorState, registerWSEventListeners } = layoutData
@@ -62,10 +54,12 @@ export function useSimulatorControl (
     { simulator: false },
     getSimulatorState
   )
-  const serverSwitchPending = ref(false)
-  let activeTimeoutId: ReturnType<typeof setTimeout> | undefined
-  let pendingOpenHandler: (() => void) | undefined
-  let pendingErrorHandler: (() => void) | undefined
+
+  const { handleUIServerChange, serverSwitchPending } = useServerSwitch({
+    onServerSwitched: options?.onServerSwitched,
+    registerWSEventListeners,
+    unregisterWSEventListeners,
+  })
 
   const startSimulator = (): void => {
     runSimulatorAction('simulator', {
@@ -87,82 +81,9 @@ export function useSimulatorControl (
     })
   }
 
-  const SERVER_SWITCH_TIMEOUT_MS = 15_000
-
-  const handleUIServerChange = (newIndex: number): void => {
-    const currentIndex = getFromLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, 0)
-    if (newIndex === currentIndex || serverSwitchPending.value) return
-
-    const servers = $configuration.value.uiServer as UIServerConfigurationSection[]
-    if (newIndex < 0 || newIndex >= servers.length) return
-
-    serverSwitchPending.value = true
-
-    $uiClient.setConfiguration(servers[newIndex])
-    unregisterWSEventListeners()
-    registerWSEventListeners()
-
-    let settled = false
-
-    const openHandler = (): void => {
-      if (settled) return
-      settled = true
-      clearTimeout(activeTimeoutId)
-      $uiClient.unregisterWSEventListener('error', errorHandler)
-      pendingOpenHandler = undefined
-      pendingErrorHandler = undefined
-      setToLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, newIndex)
-      serverSwitchPending.value = false
-      options?.onServerSwitched?.()
-    }
-
-    const errorHandler = (): void => {
-      if (settled) return
-      settled = true
-      clearTimeout(activeTimeoutId)
-      $uiClient.unregisterWSEventListener('open', openHandler)
-      pendingOpenHandler = undefined
-      pendingErrorHandler = undefined
-      serverSwitchPending.value = false
-      const previousIndex = getFromLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, 0)
-      const rollbackServers = $configuration.value.uiServer as UIServerConfigurationSection[]
-      if (previousIndex >= 0 && previousIndex < rollbackServers.length) {
-        $uiClient.setConfiguration(rollbackServers[previousIndex])
-      }
-      unregisterWSEventListeners()
-      registerWSEventListeners()
-    }
-
-    $uiClient.registerWSEventListener('open', openHandler, { once: true })
-    $uiClient.registerWSEventListener('error', errorHandler, { once: true })
-    pendingOpenHandler = openHandler
-    pendingErrorHandler = errorHandler
-
-    activeTimeoutId = setTimeout(() => {
-      if (!settled) {
-        errorHandler()
-      }
-    }, SERVER_SWITCH_TIMEOUT_MS)
-  }
-
-  onScopeDispose(() => {
-    if (activeTimeoutId != null) {
-      clearTimeout(activeTimeoutId)
-      activeTimeoutId = undefined
-    }
-    if (pendingOpenHandler != null) {
-      $uiClient.unregisterWSEventListener('open', pendingOpenHandler)
-      pendingOpenHandler = undefined
-    }
-    if (pendingErrorHandler != null) {
-      $uiClient.unregisterWSEventListener('error', pendingErrorHandler)
-      pendingErrorHandler = undefined
-    }
-  })
-
   return {
     handleUIServerChange,
-    serverSwitchPending: readonly(serverSwitchPending),
+    serverSwitchPending,
     simulatorPending: computed(() => simulatorPendingState.simulator),
     startSimulator,
     stopSimulator,
index 47f3363db400f5dc059058472ee738d73426f2a9..000ecad38d32cbd189f56f64c757d2f6904d7cab 100644 (file)
@@ -1,10 +1,10 @@
 import { type SKIN_IDS } from 'ui-common'
 import { readonly, ref, type Ref } from 'vue'
 
-import { getFromLocalStorage, setToLocalStorage } from '@/core/index.js'
+import { DEFAULT_SKIN, getFromLocalStorage, setToLocalStorage } from '@/core/index.js'
 import { validateTokenContract } from '@/shared/tokens/contract.js'
 // Intentional: registry.ts is pure metadata (ids, labels, loaders) — no behavioral coupling.
-import { DEFAULT_SKIN, type SkinDefinition, skins } from '@/skins/registry.js'
+import { type SkinDefinition, skins } from '@/skins/registry.js'
 
 export const SKIN_STORAGE_KEY = 'ecs-ui-skin'
 
diff --git a/ui/web/src/shared/types.ts b/ui/web/src/shared/types.ts
deleted file mode 100644 (file)
index 4eafd39..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/** Common station identification fields used across skin components. */
-export interface StationIdentifier {
-  chargingStationId: string
-  hashId: string
-}
index 62db02f2c28b2071d97a30ddc7841832c04a55bc..dbe3df0fbb95f0c51a29a9c6881a438f5ae06472 100644 (file)
@@ -1,5 +1,5 @@
 export { getSelectValue } from './dom.js'
-export { formatSupervisionUrl, type FormatSupervisionUrlOptions } from './formatSupervisionUrl.js'
+export { formatSupervisionUrl } from './formatSupervisionUrl.js'
 export type { StatusVariant } from './stationStatus.js'
 export {
   getATGStatus,
index 7047a58ed9a1d20bae31d55769b7dc6add19e8c2..a869cda304aca33cc1bf61ebb5472ca70654d162 100644 (file)
@@ -853,6 +853,32 @@ html[data-skin='modern'] #app {
   border-color: var(--skin-border-strong);
 }
 
+/* Light-theme pill overrides — state colours are already dark (Material 700)
+ * and readable on light backgrounds without white mixing. */
+:root[data-theme='catppuccin-latte'] .modern-pill--ok,
+:root[data-theme='sap-horizon'] .modern-pill--ok,
+:root[data-theme='teal-light'] .modern-pill--ok {
+  color: var(--color-state-ok);
+  background-color: color-mix(in srgb, var(--color-state-ok) 14%, transparent);
+  border-color: color-mix(in srgb, var(--color-state-ok) 35%, transparent);
+}
+
+:root[data-theme='catppuccin-latte'] .modern-pill--warn,
+:root[data-theme='sap-horizon'] .modern-pill--warn,
+:root[data-theme='teal-light'] .modern-pill--warn {
+  color: var(--color-state-warn);
+  background-color: color-mix(in srgb, var(--color-state-warn) 18%, transparent);
+  border-color: color-mix(in srgb, var(--color-state-warn) 50%, transparent);
+}
+
+:root[data-theme='catppuccin-latte'] .modern-pill--err,
+:root[data-theme='sap-horizon'] .modern-pill--err,
+:root[data-theme='teal-light'] .modern-pill--err {
+  color: var(--color-state-err);
+  background-color: color-mix(in srgb, var(--color-state-err) 14%, transparent);
+  border-color: color-mix(in srgb, var(--color-state-err) 35%, transparent);
+}
+
 /* ── Buttons — flat Material-style ─────────────────────────────────── */
 .modern-btn {
   display: inline-flex;
index 105e1f03f57bc522b502e2e8302cb4812d1cffb9..ee36d43391a0a69632c2d18840d717ec3cc005f7 100644 (file)
@@ -23,8 +23,6 @@ export interface SkinDefinition {
   readonly loadStyles: () => Promise<unknown>
 }
 
-export const DEFAULT_SKIN: (typeof SKIN_IDS)[number] = 'classic'
-
 export const skins: readonly SkinDefinition[] = [
   {
     id: 'classic',
index 30707415dfe65896b37835d1cf6a3fb65774d0b9..138e3750a380f884ba83c6e1a24859dac5a9fabf 100644 (file)
@@ -8,7 +8,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
 import App from '@/App.vue'
 
 vi.mock('@/skins/registry.js', () => ({
-  DEFAULT_SKIN: 'classic',
   skins: [
     {
       id: 'classic',
index 62d7e3c3ae8eca6f316ecb05948b2bbc948cb6f6..5a83173b11935d43374adc5850757c4f523f61f8 100644 (file)
@@ -55,24 +55,6 @@ describe('UIClient', () => {
     })
   })
 
-  describe('isOCPP20x', () => {
-    it('should return true for VERSION_20', () => {
-      expect(UIClient.isOCPP20x(OCPPVersion.VERSION_20)).toBe(true)
-    })
-
-    it('should return true for VERSION_201', () => {
-      expect(UIClient.isOCPP20x(OCPPVersion.VERSION_201)).toBe(true)
-    })
-
-    it('should return false for VERSION_16', () => {
-      expect(UIClient.isOCPP20x(OCPPVersion.VERSION_16)).toBe(false)
-    })
-
-    it('should return false for undefined', () => {
-      expect(UIClient.isOCPP20x(undefined)).toBe(false)
-    })
-  })
-
   describe('WebSocket connection', () => {
     it('should connect with ws:// URL format', () => {
       UIClient.getInstance(createUIServerConfig())
index d334cbca5c7fd91bd1af5f602edbd3dd26c30f87..d71492710a9efa2e26b30f1e1079e0fde7cbb028 100644 (file)
@@ -7,7 +7,6 @@ import { ResponseStatus } from 'ui-common'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 
 import {
-  deleteFromLocalStorage,
   getFromLocalStorage,
   getLocalStorage,
   resetToggleButtonState,
@@ -16,6 +15,7 @@ import {
   useConfiguration,
   useTemplates,
 } from '@/core/index.js'
+import { deleteFromLocalStorage } from '@/core/storage.js'
 import { useFetchData } from '@/shared/composables/useFetchData.js'
 
 import { toastMock } from '../setup.js'
index 0f5a371a990d2f79daa3e1cd539aeb21664f611e..ad2c8da1d6ea843b1a92724524ecd1ef2a61e28d 100644 (file)
@@ -4,11 +4,11 @@
  */
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 
+import { DEFAULT_SKIN } from '@/core/index.js'
 import { useSkin } from '@/shared/composables/useSkin.js'
-import { DEFAULT_SKIN, skins } from '@/skins/registry.js'
+import { skins } from '@/skins/registry.js'
 
 vi.mock('@/skins/registry.js', () => ({
-  DEFAULT_SKIN: 'classic',
   skins: [
     {
       description: 'Table-based layout with a sticky sidebar action panel.',
index 1fd47c8cb0c75428de9547d7696f7ba83bf7fbcf..154440ffadf391899ebd69ce94b25a1c88e9bbdf 100644 (file)
@@ -4,7 +4,8 @@
  */
 import { describe, expect, it } from 'vitest'
 
-import { DEFAULT_SKIN, skins } from '@/skins/registry.js'
+import { DEFAULT_SKIN } from '@/core/index.js'
+import { skins } from '@/skins/registry.js'
 
 describe('registry', () => {
   it('should export DEFAULT_SKIN as classic', () => {