diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..d91c5ef4
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,70 @@
+name: CI Tests
+
+on:
+ push:
+ branches: [dev, master]
+ pull_request:
+ branches: [dev, master]
+
+jobs:
+ backend-tests:
+ name: Backend Tests (Python)
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python 3.13
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.13"
+ cache: "pip"
+ cache-dependency-path: requirements-test.txt
+
+ - name: Create virtual environment
+ run: |
+ python -m venv venv
+ echo "$GITHUB_WORKSPACE/venv/bin" >> $GITHUB_PATH
+
+ - name: Install test dependencies
+ run: |
+ pip install --upgrade pip
+ pip install -r requirements-test.txt
+
+ - name: Run unit tests
+ run: pytest tests/unit/ -v
+
+ - name: Run functional tests
+ run: pytest tests/functional/ -v
+
+ frontend-tests:
+ name: Frontend Tests (Node / Vitest)
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+ cache: "npm"
+ cache-dependency-path: frontend/package-lock.json
+
+ - name: Install frontend dependencies
+ working-directory: frontend
+ run: npm ci --legacy-peer-deps
+
+ - name: Run frontend tests
+ working-directory: frontend
+ run: npm test
+
+ - name: Upload coverage report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: frontend-coverage
+ path: frontend/coverage/
+ if-no-files-found: ignore
diff --git a/.gitignore b/.gitignore
old mode 100755
new mode 100644
index 12ded931..3014d5ce
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ node_modules/
venv/
dist/
frontend/dist/
+frontend/node_modules/
__pycache__/
@@ -18,4 +19,8 @@ __pycache__/
._*
create-package.sh
v-link.code-workspace
-logs/
\ No newline at end of file
+logs/
+
+#tooling
+.vscode/
+CLAUDE.md
diff --git a/V-Link.py b/V-Link.py
index 214f8a00..9bdc7857 100755
--- a/V-Link.py
+++ b/V-Link.py
@@ -18,19 +18,20 @@
along with this program. If not, see .
'''
-import sys
-import os
-import subprocess
+import sys
+import os
+import subprocess
-def activate_venv():
- venv_path = f"/home/{os.getenv('USER')}/v-link/venv"
- activate_script = os.path.join(venv_path, 'bin', 'activate')
-
- if not os.path.exists(activate_script):
- logger.error(f'[V-Link] Activation script for venv not found: {activate_script}')
- logger.error(f'[V-Link] Please ensure the virtual environment is set up correctly. Exiting...')
- sys.exit(0)
+def activate_venv():
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ venv_path = os.path.join(script_dir, 'venv')
+ activate_script = os.path.join(venv_path, 'bin', 'activate')
+
+ if not os.path.exists(activate_script):
+ sys.stderr.write(f'[V-Link] Activation script for venv not found: {activate_script}\n')
+ sys.stderr.write('[V-Link] Please ensure the virtual environment is set up correctly. Exiting...\n')
+ sys.exit(0)
os.system(f'. {activate_script}')
@@ -423,4 +424,4 @@ def display_thread_states():
except Exception as e:
logger.error(f'Update failed: {e}')
- sys.exit(0)
\ No newline at end of file
+ sys.exit(0)
diff --git a/backend/config/app.json b/backend/config/app.json
index 8ac9b7ca..1c0362d0 100755
--- a/backend/config/app.json
+++ b/backend/config/app.json
@@ -93,18 +93,75 @@
"label": "Open Screen on Start"
}
},
- "reverseCam": {
- "title": "Reverse Camera Settings",
+ "daylight_backlight": {
+ "ui": "range",
+ "label": "Daylight Backlight Level",
+ "value": 15,
+ "min": 1,
+ "max": 16,
+ "step": 1
+ },
+ "auto_backlight": {
+ "title": "Automatic Backlight",
"type": "system",
- "enabled": {
+ "autoOpen": {
"value": true,
- "label": "Enabled"
- },
- "delay": {
- "value": 7,
- "label": "Off Delay (sec)"
+ "label": "Enable Automatic Backlight"
}
},
+ "darkness_backlight": {
+ "ui": "range",
+ "label": "Darkness Backlight Level",
+ "value": 5,
+ "min": 1,
+ "max": 10,
+ "step": 1
+ },
+ "reverseCam": {
+ "title": "Reverse Camera Settings",
+ "type": "system",
+ "enabled": {
+ "value": true,
+ "label": "Enabled"
+ },
+ "delay": {
+ "value": 7,
+ "label": "Off Delay (sec)"
+ },
+ "deviceSelectionMode": {
+ "value": "auto",
+ "label": "Device Selection",
+ "options": [
+ "auto",
+ "deviceId",
+ "label"
+ ]
+ },
+ "deviceId": {
+ "value": "default",
+ "label": "Camera Device",
+ "options": [
+ "default"
+ ]
+ },
+ "deviceLabel": {
+ "type": "text",
+ "value": "",
+ "label": "Device Label Contains"
+ },
+ "videoWidth": {
+ "value": 1280,
+ "label": "Video Width"
+ },
+ "videoHeight": {
+ "value": 720,
+ "label": "Video Height"
+ },
+ "videoFps": {
+ "value": 30,
+ "label": "Video FPS"
+ }
+ },
"side_bars": {
"title": "Sidebar Settings",
"type": "system",
@@ -458,4 +515,4 @@
"label": "Show Advanced Settings"
}
}
-}
\ No newline at end of file
+}
diff --git a/backend/config/profiles/P1/T5/can.json b/backend/config/profiles/P1/T5/can.json
index ada42828..55f66f15 100755
--- a/backend/config/profiles/P1/T5/can.json
+++ b/backend/config/profiles/P1/T5/can.json
@@ -195,6 +195,50 @@
"min_value": 0,
"max_value": 260,
"limit_start": 240
+ },
+ "light": {
+ "interface": "can2",
+ "enabled": false,
+ "type": "diagnostic",
+ "parameter": [
+ "0x2F",
+ "0x60"
+ ],
+ "app_id": "lgt",
+ "req_id": "0x000FFFFE",
+ "rep_id": "0x00400021",
+ "action": "0xA6",
+ "target": "0x40",
+ "is_16bit": true,
+ "priority": 3,
+ "scale": "(value & 0x03FF) * 5.0 / 1023.0",
+ "label": "Light Sensor",
+ "unit": "V",
+ "min_value": 0,
+ "max_value": 5
+ }
+ },
+ "signal_sensors": [
+ {
+ "key": "light",
+ "label": "Light Sensor",
+ "interface": "can2",
+ "enabled": false,
+ "can_id": "0x0100082C",
+ "byte_index": 1,
+ "bit_index": 7,
+ "invert": false,
+ "scale": "(1 - value) * 5.0"
+ },
+ {
+ "key": "reverse",
+ "label": "Reverse Gear",
+ "interface": "can2",
+ "enabled": false,
+ "can_id": "0x00000000",
+ "byte_index": 0,
+ "bit_index": 0,
+ "invert": false
}
- }
-}
\ No newline at end of file
+ ]
+}
diff --git a/backend/config/profiles/P2/D5/can.json b/backend/config/profiles/P2/D5/can.json
index 19d3c2b2..6043d61b 100755
--- a/backend/config/profiles/P2/D5/can.json
+++ b/backend/config/profiles/P2/D5/can.json
@@ -113,7 +113,7 @@
"enabled": true,
"type": "diagnostic",
"parameter": [
- "0x0C",
+ "0x0C"
],
"app_id": "ld1",
"req_id": "0x000FFFFE",
@@ -195,5 +195,28 @@
"max_value": 260,
"limit_start": 240
}
- }
+ },
+ "signal_sensors": [
+ {
+ "key": "light",
+ "label": "Light Sensor",
+ "interface": "can2",
+ "enabled": false,
+ "can_id": "0x00000000",
+ "byte_index": 0,
+ "bit_index": 0,
+ "invert": false,
+ "scale": "(1 - value) * 5.0"
+ },
+ {
+ "key": "reverse",
+ "label": "Reverse Gear",
+ "interface": "can2",
+ "enabled": false,
+ "can_id": "0x00000000",
+ "byte_index": 0,
+ "bit_index": 0,
+ "invert": false
+ }
+ ]
}
diff --git a/backend/config/profiles/P2/T5/can.json b/backend/config/profiles/P2/T5/can.json
index a19dee3b..89ab6e4d 100644
--- a/backend/config/profiles/P2/T5/can.json
+++ b/backend/config/profiles/P2/T5/can.json
@@ -196,5 +196,28 @@
"max_value": 260,
"limit_start": 240
}
- }
+ },
+ "signal_sensors": [
+ {
+ "key": "light",
+ "label": "Light Sensor",
+ "interface": "can2",
+ "enabled": false,
+ "can_id": "0x00000000",
+ "byte_index": 0,
+ "bit_index": 0,
+ "invert": false,
+ "scale": "(1 - value) * 5.0"
+ },
+ {
+ "key": "reverse",
+ "label": "Reverse Gear",
+ "interface": "can2",
+ "enabled": false,
+ "can_id": "0x00000000",
+ "byte_index": 0,
+ "bit_index": 0,
+ "invert": false
+ }
+ ]
}
diff --git a/backend/server.py b/backend/server.py
index 6e153189..6c43eba7 100755
--- a/backend/server.py
+++ b/backend/server.py
@@ -159,6 +159,13 @@ def save_settings(data):
# rearcam no usa settings; mantenemos la firma y no persistimos
if module != "rearcam":
settings.save_settings(module, data)
+ if module == "app":
+ try:
+ shared_state.backlight_daylight = data.get('daylight_backlight', {}).get('value', shared_state.backlight_daylight)
+ shared_state.backlight_darkness = data.get('darkness_backlight', {}).get('value', shared_state.backlight_darkness)
+ shared_state.backlight_auto_enabled = data.get('auto_backlight', {}).get('autoOpen', {}).get('value', shared_state.backlight_auto_enabled)
+ except Exception as e:
+ logger.error(f'[Settings] Error updating backlight shared state from save: {e}')
# Emit module settings
def load_settings():
@@ -209,11 +216,34 @@ def toggle_state():
toggle_state.__name__ = f'handle_toggle_{module}'
emit_data.__name__ = f'handle_data_{module}'
+ def backlight_update(data):
+ if module != "app":
+ return
+ if not isinstance(data, dict):
+ return
+ try:
+ if 'daylight' in data:
+ shared_state.backlight_daylight = data['daylight']
+ if 'darkness' in data:
+ shared_state.backlight_darkness = data['darkness']
+ if 'auto_enabled' in data:
+ shared_state.backlight_auto_enabled = data['auto_enabled']
+ logger.info(
+ f"[Backlight] daylight={shared_state.backlight_daylight} "
+ f"darkness={shared_state.backlight_darkness} "
+ f"auto_enabled={shared_state.backlight_auto_enabled}"
+ )
+ except Exception as e:
+ logger.error(f'[Settings] Error updating backlight shared state: {e}')
+
+ backlight_update.__name__ = f'backlight_update_{module}'
+
socketio.on_event('load', load_settings, namespace=namespace)
socketio.on_event('save', save_settings, namespace=namespace)
socketio.on_event('ping', emit_state, namespace=namespace)
socketio.on_event('data', emit_data, namespace=namespace)
socketio.on_event('toggle', toggle_state, namespace=namespace)
+ socketio.on_event('backlight:update', backlight_update, namespace=namespace)
# ------- Eventos específicos para rearcam -------
if module == "rearcam":
@@ -300,6 +330,9 @@ def handle_system_task(args, payload=None):
# Checks whether .config/v-link/ exists
# Returns either true or an object with selectable profiles.
result = settings.check_settings()
+ if result is True:
+ # Push the current (potentially migrated) settings
+ socketio.emit('settings', settings.load_settings('app'), namespace='/app')
return result
elif args == 'load':
diff --git a/backend/settings.py b/backend/settings.py
index a53896a9..9c45b062 100755
--- a/backend/settings.py
+++ b/backend/settings.py
@@ -24,6 +24,26 @@ def load_directory():
return None
+def migrate_settings():
+ # Merge any top-level keys present in the default config but missing from the user config.
+ default_app = DEFAULT_CONFIG_DIR / 'app.json'
+ user_app = USER_CONFIG_DIR / 'app.json'
+ if not default_app.exists() or not user_app.exists():
+ return
+ try:
+ with default_app.open('r', encoding='utf-8') as f:
+ defaults = json.load(f)
+ with user_app.open('r', encoding='utf-8') as f:
+ user = json.load(f)
+ missing = {k: v for k, v in defaults.items() if k not in user}
+ if missing:
+ user.update(missing)
+ save_settings('app', user)
+ logger.info(f'[Settings] Migrated missing keys into user config: {list(missing.keys())}')
+ except Exception as e:
+ logger.error(f'[Settings] Error during settings migration: {e}')
+
+
def check_settings():
# Print directory paths for debugging
logger.info(f'[Settings] (Dir) App Root: {APP_ROOT}')
@@ -31,8 +51,9 @@ def check_settings():
logger.info(f'[Settings] (Dir) Default configs: {DEFAULT_CONFIG_DIR}')
logger.info(f'[Settings] (Dir) User configs: {USER_CONFIG_DIR}')
- # If user config already exists and contains files, return True
- if USER_CONFIG_DIR.exists() and any(USER_CONFIG_DIR.iterdir()):
+ # If user app config exists, return True
+ if (USER_CONFIG_DIR / 'app.json').exists():
+ migrate_settings()
load_modules()
return True
else:
@@ -155,6 +176,9 @@ def copy_json_files(src: Path, dst: Path):
# Setting a shared_state based on app.json
def load_modules():
settings = load_settings('app')
+ if settings is None:
+ logger.error('[Settings] Cannot load modules: app.json missing or invalid')
+ return
constants = settings.get('constants', {})
shared_state.canModule = constants['modules']['can']
diff --git a/backend/shared/backlight_helper.py b/backend/shared/backlight_helper.py
new file mode 100644
index 00000000..11995b79
--- /dev/null
+++ b/backend/shared/backlight_helper.py
@@ -0,0 +1,286 @@
+import time
+from .. import settings
+from .shared_state import shared_state
+
+
+# ============================================================
+# Backlight levels table (16 steps, 1:1 index)
+# The logical index is (slider - 1)
+# ============================================================
+
+BACKLIGHT_LEVELS = [
+ 0x20, 0x61, 0x62, 0x23,
+ 0x64, 0x25, 0x26, 0x67,
+ 0x68, 0x29, 0x2A, 0x2C,
+ 0x6B, 0x6D, 0x6E, 0x2F,
+]
+
+
+# ============================================================
+# Defaults
+# ============================================================
+
+DEFAULT_DAYLIGHT = {"value": 15, "min": 1, "max": 16}
+DEFAULT_DARKNESS = {"value": 5, "min": 1, "max": 16}
+DEFAULT_AUTO = True
+
+# Temporal hysteresis (seconds), per target mode
+HOLD_TIME_S = {
+ "night": 2.0,
+ "dim": 1.5,
+ "day": 3.0,
+}
+
+# Voltage thresholds from ambient light sensor
+# voltage <= NIGHT_MAX_V => night
+# voltage <= DIM_MAX_V => dim
+# voltage > DIM_MAX_V => day
+NIGHT_MAX_V = 0.9
+DIM_MAX_V = 2.4
+
+
+# ============================================================
+# Utilities
+# ============================================================
+
+def clamp(value, low, high):
+ return max(low, min(high, value))
+
+
+def level_from_step(step: int) -> int:
+ """
+ step: slider value (1–16)
+ """
+ index = clamp(step - 1, 0, len(BACKLIGHT_LEVELS) - 1)
+ return BACKLIGHT_LEVELS[index]
+
+
+# ============================================================
+# Config load (NO logic)
+# ============================================================
+
+def _read_range_setting(app, key, defaults):
+ cfg = app.get(key, {}) if isinstance(app, dict) else {}
+ if not isinstance(cfg, dict):
+ return defaults.copy()
+
+ value = cfg.get("value", defaults["value"])
+ if isinstance(value, dict):
+ value = value.get("value", defaults["value"])
+
+ return {
+ "value": value,
+ "min": cfg.get("min", defaults["min"]),
+ "max": cfg.get("max", defaults["max"]),
+ }
+
+
+def _read_toggle_setting(app, key, nested_key, default):
+ cfg = app.get(key, {}) if isinstance(app, dict) else {}
+ if isinstance(cfg, dict):
+ nested = cfg.get(nested_key)
+ if isinstance(nested, dict) and "value" in nested:
+ return bool(nested.get("value", default))
+ if "value" in cfg:
+ return bool(cfg.get("value", default))
+ return bool(default)
+
+
+def load_backlight_config():
+ runtime_daylight = getattr(shared_state, "backlight_daylight", None)
+ runtime_darkness = getattr(shared_state, "backlight_darkness", None)
+ runtime_auto = getattr(shared_state, "backlight_auto_enabled", None)
+
+ # Only read from disk if shared_state hasn't been initialized yet.
+ if not isinstance(runtime_daylight, (int, float)) or not isinstance(runtime_darkness, (int, float)) or not isinstance(runtime_auto, bool):
+ app = settings.load_settings("app") or {}
+ daylight = _read_range_setting(app, "daylight_backlight", DEFAULT_DAYLIGHT)
+ darkness = _read_range_setting(app, "darkness_backlight", DEFAULT_DARKNESS)
+ auto_enabled = _read_toggle_setting(app, "auto_backlight", "autoOpen", DEFAULT_AUTO)
+
+ if not isinstance(runtime_daylight, (int, float)):
+ shared_state.backlight_daylight = daylight["value"]
+ runtime_daylight = daylight["value"]
+ else:
+ daylight["value"] = runtime_daylight
+
+ if not isinstance(runtime_darkness, (int, float)):
+ shared_state.backlight_darkness = darkness["value"]
+ runtime_darkness = darkness["value"]
+ else:
+ darkness["value"] = runtime_darkness
+
+ if not isinstance(runtime_auto, bool):
+ shared_state.backlight_auto_enabled = auto_enabled
+ runtime_auto = auto_enabled
+ else:
+ daylight = {**DEFAULT_DAYLIGHT, "value": runtime_daylight}
+ darkness = {**DEFAULT_DARKNESS, "value": runtime_darkness}
+ auto_enabled = runtime_auto
+
+ return {
+ "daylight": daylight,
+ "darkness": darkness,
+ "auto": auto_enabled,
+ }
+
+
+# ============================================================
+# Day/night temporal hysteresis
+# ============================================================
+
+class LightStateHysteresis:
+ def __init__(self, hold_times):
+ self.state = "day"
+ self._candidate = None
+ self._since = None
+ self.hold_times = hold_times
+
+ def update(self, target: str) -> str:
+ now = time.monotonic()
+
+ if target == self.state:
+ self._candidate = None
+ self._since = None
+ return self.state
+
+ if self._candidate != target:
+ self._candidate = target
+ self._since = now
+ return self.state
+
+ elapsed = now - self._since
+ required = self.hold_times.get(target, 1.0)
+
+ if elapsed >= required:
+ self.state = target
+ self._candidate = None
+ self._since = None
+
+ return self.state
+
+
+# ============================================================
+# Mapper + change detection
+# ============================================================
+
+class BacklightMapper:
+ def __init__(self):
+ self._last_byte = None
+
+ def map(self, step: int):
+ byte = level_from_step(step)
+ changed = byte != self._last_byte
+ if changed:
+ self._last_byte = byte
+ return byte, changed
+
+
+# ============================================================
+# Main controller (reads CAN + decides everything)
+# ============================================================
+
+class BacklightController:
+ """
+ FULL BACKLIGHT BRAIN
+
+ - Reads CAN (shared state)
+ - Decides if it's dark
+ - Applies temporal hysteresis
+ - Selects day/night profile
+ - Translates slider -> byte
+ - Detects changes
+ """
+
+ def __init__(self):
+ self._mapper = BacklightMapper()
+ self._light_state = LightStateHysteresis(HOLD_TIME_S)
+
+ # --------------------------------------------------------
+ # CAN sensor interpretation
+ # --------------------------------------------------------
+
+ def _mode_from_can(self) -> str:
+ """
+ Interprets CAN light value and returns: day, dim, night.
+ """
+ raw = None
+
+ # Preferred path: CAN listeners publish sensors into shared_state.car_data
+ try:
+ with shared_state.car_data_lock:
+ raw = shared_state.car_data.get("light")
+ except Exception:
+ raw = None
+
+ # Backward-compatible fallback
+ if raw is None:
+ raw = getattr(getattr(shared_state, "can", None), "light_raw", None)
+
+ if raw is None:
+ return "day"
+
+ try:
+ voltage = float(raw)
+ except (TypeError, ValueError):
+ return "day"
+
+ if voltage <= NIGHT_MAX_V:
+ return "night"
+ if voltage <= DIM_MAX_V:
+ return "dim"
+ return "day"
+
+ def _dim_profile(self, cfg):
+ day = cfg["daylight"]
+ night = cfg["darkness"]
+ day_value = clamp(day["value"], day["min"], day["max"])
+ night_value = clamp(night["value"], night["min"], night["max"])
+ low = min(day["min"], night["min"])
+ high = max(day["max"], night["max"])
+ return {
+ "value": clamp(int(round((day_value + night_value) / 2)), low, high),
+ "min": low,
+ "max": high,
+ }
+
+
+ # --------------------------------------------------------
+ # Public API
+ # --------------------------------------------------------
+
+ def update(self):
+ """
+ :return: (byte, changed, mode)
+ """
+
+ cfg = load_backlight_config()
+
+ # 1. Read CAN and decide target mode
+ target_mode = self._mode_from_can()
+
+ # 2. Stable state with hysteresis
+ state = self._light_state.update(target_mode)
+
+ # 3. Profile selection
+ if not cfg["auto"]:
+ profile = cfg["daylight"]
+ mode = "day"
+ else:
+ if state == "night":
+ profile = cfg["darkness"]
+ mode = "night"
+ elif state == "dim":
+ profile = self._dim_profile(cfg)
+ mode = "dim"
+ else:
+ profile = cfg["daylight"]
+ mode = "day"
+
+ # 4. Slider -> byte
+ step = clamp(profile["value"], profile["min"], profile["max"])
+ byte, changed = self._mapper.map(step)
+
+ shared_state.backlight_byte = byte
+
+ return byte, changed, mode
diff --git a/backend/shared/shared_state.py b/backend/shared/shared_state.py
index c1da8415..c2e8eaac 100755
--- a/backend/shared/shared_state.py
+++ b/backend/shared/shared_state.py
@@ -27,11 +27,17 @@ def __init__(self):
#Display
self.isKiosk = True
self.rtiStatus = False
- self.hdmiStatus = False
-
- self.update = False
-
- #Thread States:
+ self.hdmiStatus = False
+
+ self.update = False
+
+ # Backlight settings (runtime). None means "not initialized yet".
+ self.backlight_daylight = None
+ self.backlight_darkness = None
+ self.backlight_auto_enabled = None
+ self.backlight_byte = None
+
+ #Thread States:
self.toggle_app = threading.Event()
self.toggle_can = threading.Event()
@@ -77,4 +83,4 @@ def update_car_data(self, key, value):
self.car_data[key] = value
-shared_state = SharedState()
\ No newline at end of file
+shared_state = SharedState()
diff --git a/backend/threads/app.py b/backend/threads/app.py
index 281c6135..58479567 100755
--- a/backend/threads/app.py
+++ b/backend/threads/app.py
@@ -1,13 +1,13 @@
import threading
import time
-import sys
-import os
-import subprocess
-import signal
-import shutil
-from ..shared.shared_state import shared_state
-
-class APPThread(threading.Thread):
+import sys
+import os
+import subprocess
+import signal
+import shutil
+from ..shared.shared_state import shared_state
+
+class APPThread(threading.Thread):
def __init__(self, logger):
super().__init__()
self.logger = logger
@@ -25,48 +25,51 @@ def run(self):
self.stop_thread()
time.sleep(.1)
- def stop_thread(self):
- self._stop_event.set()
- self.close_browser()
- shared_state.toggle_app.clear()
-
- def _browser_executable(self):
- codename = None
- try:
- with open('/etc/os-release', encoding='utf-8') as os_release:
- for line in os_release:
- if line.startswith('VERSION_CODENAME='):
- codename = line.split('=', 1)[1].strip().strip('"').lower()
- break
- except OSError as error:
- self.logger.warning(f'[Browser] Could not read /etc/os-release: {error}')
-
- preferred = 'chromium' if codename == 'trixie' else 'chromium-browser'
- fallback = 'chromium-browser' if preferred == 'chromium' else 'chromium'
-
- if shutil.which(preferred):
- return preferred
- if shutil.which(fallback):
- self.logger.warning(f'[Browser] "{preferred}" not found, using "{fallback}" instead.')
- return fallback
-
- return preferred
-
-
- def start_browser(self):
+ def stop_thread(self):
+ self._stop_event.set()
+ self.close_browser()
+ shared_state.toggle_app.clear()
+
+ def _browser_executable(self):
+ codename = None
+ try:
+ with open('/etc/os-release', encoding='utf-8') as os_release:
+ for line in os_release:
+ if line.startswith('VERSION_CODENAME='):
+ codename = line.split('=', 1)[1].strip().strip('"').lower()
+ break
+ except OSError as error:
+ self.logger.warning(f'[Browser] Could not read /etc/os-release: {error}')
+
+ preferred = 'chromium' if codename == 'trixie' else 'chromium-browser'
+ fallback = 'chromium-browser' if preferred == 'chromium' else 'chromium'
+
+ if shutil.which(preferred):
+ return preferred
+ if shutil.which(fallback):
+ self.logger.warning(f'[Browser] "{preferred}" not found, using "{fallback}" instead.')
+ return fallback
+
+ return preferred
+
+
+ def start_browser(self):
profile_dir = os.path.expanduser('~/.config/v-link/chromium-profile')
os.makedirs(profile_dir, exist_ok=True)
+
standard_flags = [
'--enable-experimental-web-platform-features',
'--enable-features=SharedArrayBuffer',
'--autoplay-policy=no-user-gesture-required',
+ '--use-fake-ui-for-media-stream',
'--disable-logging',
'--log-level=3',
'--disable-gpu',
- '--use-fake-ui-for-media-stream',
f'--user-data-dir={profile_dir}',
'--no-first-run',
'--no-default-browser-check'
+ '--allow-insecure-localhost',
+ '--unsafely-treat-insecure-origin-as-secure=http://localhost:4001,http://localhost:5173'
]
if shared_state.isKiosk:
@@ -84,7 +87,7 @@ def start_browser(self):
flags = standard_flags + mode
# Final command as list
- command = [self._browser_executable(), self.url] + flags
+ command = [self._browser_executable(), self.url] + flags
self.browser = subprocess.Popen(
command,
@@ -92,7 +95,7 @@ def start_browser(self):
stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL
)
- # self.browser = subprocess.Popen(command)
+ #self.browser = subprocess.Popen(command)
self.logger.info(f'[Browser] Chromium browser started with PID: "{self.browser.pid}"')
@@ -117,4 +120,3 @@ def close_browser(self):
self.logger.error(f'[Browser] Error stopping chromium: {e}')
else:
self.logger.error('[Browser] Chromium not found on this system.')
-
diff --git a/backend/threads/cam.py b/backend/threads/cam.py
index a52b9a8a..849cb0bb 100755
--- a/backend/threads/cam.py
+++ b/backend/threads/cam.py
@@ -133,18 +133,18 @@ def run(self):
_LGPIO_IMPORT_ERROR = e
-class CameraGPIO:
- # Driver GPIO para encender/apagar la alimentación de la cámara.
- # - line: nº de línea BCM (p.ej. 18)
- # - chip: nº de gpiochip (normalmente 0)
- # - active_high: True si nivel alto = ON (inverso si False)
-
- # Métodos:
- # - set(on: bool) → enciende/apaga
- # - get() -> bool → último estado lógico solicitado (no lectura física)
- # - toggle() -> bool
- # - level() -> int → nivel eléctrico actual (0/1) si es posible leerlo
- # - cleanup() → apaga y libera recursos
+class CameraGPIO:
+ # GPIO driver to power camera on/off.
+ # - line: BCM line number (e.g. 18)
+ # - chip: gpiochip number (usually 0)
+ # - active_high: True if high level = ON (inverted if False)
+
+ # Methods:
+ # - set(on: bool) -> power on/off
+ # - get() -> bool -> last requested logical state (not physical read)
+ # - toggle() -> bool
+ # - level() -> int -> current electrical level (0/1) if readable
+ # - cleanup() -> power off and release resources
@@ -164,22 +164,22 @@ def __init__(self, line: int = 26, chip: int = 0, active_high: bool = True, logg
self.logger.debug(f"CameraGPIO init: line={self.line}, chip={self._chip_num}, active_high={self.active_high}")
- def _require_lgpio(self) -> None:
- if not LGPIO_AVAILABLE:
- raise RuntimeError(
- f"lgpio no disponible: {_LGPIO_IMPORT_ERROR!r}. "
- "Instala 'lgpio' o ejecuta en hardware compatible."
- )
-
- def _open_chip(self) -> None:
- if self._chip is None:
- self._require_lgpio()
- try:
- self._chip = lgpio.gpiochip_open(self._chip_num)
- self.logger.debug(f"CameraGPIO: abierto gpiochip{self._chip_num}")
- except Exception as e:
- # disgnostic info
- raise RuntimeError(f"not able to open /dev/gpiochip{self._chip_num}: {e}") from e
+ def _require_lgpio(self) -> None:
+ if not LGPIO_AVAILABLE:
+ raise RuntimeError(
+ f"lgpio not available: {_LGPIO_IMPORT_ERROR!r}. "
+ "Install 'lgpio' or run on compatible hardware."
+ )
+
+ def _open_chip(self) -> None:
+ if self._chip is None:
+ self._require_lgpio()
+ try:
+ self._chip = lgpio.gpiochip_open(self._chip_num)
+ self.logger.debug(f"CameraGPIO: opened gpiochip{self._chip_num}")
+ except Exception as e:
+ # disgnostic info
+ raise RuntimeError(f"not able to open /dev/gpiochip{self._chip_num}: {e}") from e
def _ensure_claimed(self) -> None:
if not self._claimed:
@@ -192,24 +192,24 @@ def _ensure_claimed(self) -> None:
except Exception as e:
raise RuntimeError(f"not able to claim GPIO{self.line} as output: {e}") from e
- def _write_level(self, level: int) -> None:
- self._require_lgpio()
- try:
- lgpio.gpio_write(self._chip, self.line, int(level)) # type: ignore[arg-type]
- except Exception as e:
- raise RuntimeError(f"Fallo escribiendo nivel {level} en BCM{self.line}: {e}") from e
+ def _write_level(self, level: int) -> None:
+ self._require_lgpio()
+ try:
+ lgpio.gpio_write(self._chip, self.line, int(level)) # type: ignore[arg-type]
+ except Exception as e:
+ raise RuntimeError(f"Failed writing level {level} on BCM{self.line}: {e}") from e
- def set(self, on: bool) -> None:
- # On (True) o off (False) camera.
- with self._lock:
- self._open_chip()
- self._ensure_claimed()
- desired = bool(on)
- level = 1 if (desired == self.active_high) else 0
- self._write_level(level)
- self._on = desired
- self.logger.info(f"CameraGPIO: {'ON' if desired else 'OFF'} (nivel físico={level})")
+ def set(self, on: bool) -> None:
+ # On (True) or off (False) camera.
+ with self._lock:
+ self._open_chip()
+ self._ensure_claimed()
+ desired = bool(on)
+ level = 1 if (desired == self.active_high) else 0
+ self._write_level(level)
+ self._on = desired
+ self.logger.info(f"CameraGPIO: {'ON' if desired else 'OFF'} (physical level={level})")
def get(self) -> bool:
#returs the last logical state requested (not physical reading).
diff --git a/backend/threads/can.py b/backend/threads/can.py
index 57eb7e46..412ec574 100755
--- a/backend/threads/can.py
+++ b/backend/threads/can.py
@@ -13,9 +13,11 @@ def __init__(self, logger):
self.can_settings = settings.load_settings('can')
self.interfaces = []
self.sensors = {}
+ self.signal_sensors = {}
self.load_interfaces()
self.load_sensors()
+ self.load_signal_sensors()
def load_interfaces(self):
for iface in self.can_settings['interfaces']:
@@ -52,8 +54,9 @@ def load_sensors(self):
request_count = 0x01
payload: list[int] = [target, action, *params, request_count]
-
message_bytes: list[int] = [0x00, *payload]
+
+ # calculate dlc
message_bytes[0] = 0xC8 + (len(message_bytes) - 1)
# Pad with zeroes
@@ -82,6 +85,44 @@ def load_sensors(self):
except Exception as e:
self.logger.error(f'[CAN] Error loading sensor "{key}" on "{iface}": {e}')
+ def load_signal_sensors(self):
+ for sensor in self.can_settings.get('signal_sensors', []):
+ try:
+ iface = sensor['interface']
+
+ if not sensor.get('enabled', False):
+ continue
+
+ if iface not in self.signal_sensors:
+ self.signal_sensors[iface] = []
+
+ key = sensor['key']
+ can_id = int(sensor['can_id'], 16)
+ byte_index = int(sensor['byte_index'])
+ bit_index = int(sensor['bit_index'])
+ invert = bool(sensor.get('invert', False))
+ scale = sensor.get('scale')
+
+ if scale is not None and (not isinstance(scale, str) or 'value' not in scale):
+ raise ValueError(f'Invalid scale format for signal sensor "{key}"')
+
+ sensor_entry = {
+ 'key': key,
+ 'label': sensor.get('label', key),
+ 'channel': iface,
+ 'can_id': can_id,
+ 'byte_index': byte_index,
+ 'bit_index': bit_index,
+ 'invert': invert,
+ 'scale': scale,
+ }
+
+ self.signal_sensors[iface].append(sensor_entry)
+
+ except Exception as e:
+ key = sensor.get('key', 'unknown')
+ self.logger.error(f'[CAN] Error loading signal sensor "{key}": {e}')
+
class CANThread(threading.Thread):
def __init__(self, logger):
@@ -114,33 +155,38 @@ def initialize_can(self):
if shared_state.vCan:
self.logger.debug('[CAN] vCAN mode is enabled. Overriding CAN settings to use vcan0.')
all_sensors = [sensor for sensor_list in self.config.sensors.values() for sensor in sensor_list]
+ all_signal_sensors = [ sensor for sensor_list in self.config.signal_sensors.values() for sensor in sensor_list ]
- if all_sensors:
+ if all_sensors or all_signal_sensors:
interfaces_to_process.append({
'channel': 'vcan0',
'bustype': 'socketcan',
'bitrate': 500000,
'is_extended': True, # Assume extended for debugging simplicity
'sensors': all_sensors,
+ 'signal_sensors': all_signal_sensors,
})
else:
for interface in self.config.interfaces:
channel = interface['channel']
sensors_for_channel = self.config.sensors.get(channel, [])
+ signal_sensors_for_channel = self.config.signal_sensors.get(channel, [])
- if sensors_for_channel:
+ if sensors_for_channel or signal_sensors_for_channel:
interfaces_to_process.append({
'channel': channel,
'bustype': interface['bustype'],
'bitrate': interface['bitrate'],
'is_extended': interface['is_extended'],
'sensors': sensors_for_channel,
+ 'signal_sensors': signal_sensors_for_channel,
})
# --- Unified Initialization Loop ---
for iface_cfg in interfaces_to_process:
channel = iface_cfg['channel']
sensors = iface_cfg['sensors']
+ signal_sensors = iface_cfg.get('signal_sensors', [])
is_extended = iface_cfg['is_extended']
try:
@@ -152,23 +198,38 @@ def initialize_can(self):
self.can_buses[channel] = bus
# Gather all reply IDs for filtering
- rep_ids = {s['rep_id'][0] for s in sensors}
+ rep_ids = {s['rep_id'][0] for s in sensors} # TODO double check [0]
+ signal_ids = {s['can_id'] for s in signal_sensors}
+ filter_ids = rep_ids | signal_ids
# Apply filters
- if rep_ids:
- filters = [{'can_id': r_id, 'can_mask': 0x1FFFFFFF if is_extended else 0x7FF, 'extended': is_extended} for r_id in rep_ids]
+ if filter_ids:
+ filters = [
+ {
+ 'can_id': can_id,
+ 'can_mask': 0x1FFFFFFF if is_extended else 0x7FF,
+ 'extended': is_extended
+ }
+ for can_id in filter_ids
+ ]
bus.set_filters(filters)
- self.logger.info(f'[CAN] Applied {len(rep_ids)} filter(s) to "{channel}".')
+ self.logger.info(f'[CAN] Applied {len(filter_ids)} filter(s) to "{channel}".')
self.logger.info(f'[CAN] Initialized {bus}')
# Group sensors by reply ID for listeners
sensors_by_id = {}
for sensor in sensors:
- rep_id = sensor['rep_id'][0]
+ rep_id = sensor['rep_id'][0] # TODO doublecheck [0]
sensors_by_id.setdefault(rep_id, []).append(sensor)
+ signal_sensors_by_id = {}
+ for signal_sensor in signal_sensors:
+ can_id = signal_sensor['can_id']
+ signal_sensors_by_id.setdefault(can_id, []).append(signal_sensor)
+
# Start Scheduler only if there are sensors to request
+ canScheduler = None
if sensors:
canScheduler = CANScheduler(sensors_by_id, bus, is_extended, self.logger, wait_for_ecu=any(i.get('wait_for_ecu') for i in self.config.interfaces if i['channel'] == channel))
self.broadcast_tasks.append(canScheduler)
@@ -176,8 +237,15 @@ def initialize_can(self):
# Setup Listeners
listeners = []
- if sensors:
- listeners.append(CANListener(sensors_by_id, self.logger, canScheduler.events))
+ if sensors or signal_sensors:
+ listeners.append(
+ CANListener(
+ sensors_by_id,
+ signal_sensors_by_id,
+ self.logger,
+ canScheduler.events if canScheduler else None
+ )
+ )
if listeners:
notifier = can.Notifier(bus, listeners)
@@ -248,15 +316,15 @@ def __init__(self, sensors_config, can_bus, is_extended, logger, wait_for_ecu=Fa
1: 0,
2: 0,
3: 0
- }
+ }
# Token generator for smooth weighted scheduling
self.token_stream = self.token_generator({
1: 6,
2: 3,
3: 1
- })
-
+ })
+
def run(self):
self.logger.info('[CAN] Message Scheduler started.')
while not self._stop_event.is_set():
@@ -337,9 +405,10 @@ def stop(self):
#############################################################
class CANListener(can.Listener):
- def __init__(self, sensors_by_id, logger, events=None):
+ def __init__(self, sensors_by_id, signal_sensors_by_id, logger, events=None):
self.logger = logger
self.sensors_by_id = sensors_by_id
+ self.signal_sensors_by_id = signal_sensors_by_id
self.events = events or {}
def on_message_received(self, msg):
@@ -373,7 +442,26 @@ def on_message_received(self, msg):
if evt:
evt.set()
- return
+ return
+ for sensor in self.signal_sensors_by_id.get(msg.arbitration_id, []):
+ byte_index = sensor['byte_index']
+ bit_index = sensor['bit_index']
+
+ if byte_index < 0 or byte_index >= len(data):
+ continue
+ if bit_index < 0 or bit_index > 7:
+ continue
+
+ value = (data[byte_index] >> bit_index) & 0x01
+ if sensor.get('invert', False):
+ value = 0 if value else 1
+
+ if sensor.get('scale'):
+ value = eval(sensor['scale'], {'value': value, 'data': data})
+
+ shared_state.update_car_data(sensor['key'], float(value))
+ return
+
except Exception as e:
- self.logger.error(f'[CAN] CAN listener error: {e}')
\ No newline at end of file
+ self.logger.error(f'[CAN] CAN listener error: {e}')
diff --git a/backend/threads/rti.py b/backend/threads/rti.py
index 02db9527..4a00c51b 100755
--- a/backend/threads/rti.py
+++ b/backend/threads/rti.py
@@ -2,19 +2,21 @@
import time
import sys
import os
-import subprocess
-import serial
-
-from ..shared.shared_state import shared_state
-
-class RTIThread(threading.Thread):
- def __init__(self, logger):
- super().__init__()
- self.rti_serial = None
- self._stop_event = threading.Event()
- self.daemon = True
-
- self.logger = logger
+import subprocess
+import serial
+
+from ..shared.shared_state import shared_state
+from ..shared.backlight_helper import BacklightController
+
+class RTIThread(threading.Thread):
+ def __init__(self, logger):
+ super().__init__()
+ self.rti_serial = None
+ self._stop_event = threading.Event()
+ self.daemon = True
+
+ self.logger = logger
+ self._backlight = BacklightController()
try:
if(shared_state.rpiModel == 5):
@@ -40,19 +42,26 @@ def write(self, byte):
self.rti_serial.write(byte.to_bytes(1, 'big'))
time.sleep(0.1)
- def run_rti(self):
- while not self._stop_event.is_set():
- try:
- if shared_state.rtiStatus:
- self.write(0x40)
- else:
- self.write(0x46)
-
- self.write(0x20)
- self.write(0x83)
- except Exception as e:
- self.logger.error(f'[RTI] Error during operation: {e}')
- break
+ def run_rti(self):
+ try:
+ while not self._stop_event.is_set():
+ if not self.rti_serial or not self.rti_serial.is_open:
+ time.sleep(0.2)
+ continue
+ try:
+ if shared_state.rtiStatus:
+ self.write(0x40)
+ else:
+ self.write(0x46)
+
+ byte, _changed, _mode = self._backlight.update()
+ self.write(byte)
+ self.write(0x83)
+ except Exception as e:
+ self.logger.error(f'[RTI] Error during operation: {e}')
+ break
+ finally:
+ self.cleanup()
def cleanup(self):
if self.rti_serial and self.rti_serial.is_open:
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f12a75a9..8c722734 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -44,25 +44,96 @@
},
"devDependencies": {
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
+ "@vitest/coverage-v8": "^4.0.18",
"babel-plugin-styled-components": "^2.1.4",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
+ "jsdom": "^28.1.0",
+ "socket.io-mock": "^1.3.2",
"vite": "^4.5.0",
- "vite-plugin-node-polyfills": "^0.22.0"
+ "vite-plugin-node-polyfills": "^0.22.0",
+ "vitest": "^4.0.18"
}
},
+ "node_modules/@acemir/cssom": {
+ "version": "0.9.31",
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
+ "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@adobe/css-tools": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
"integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
"license": "MIT"
},
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz",
+ "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^3.1.1",
+ "@csstools/css-color-parser": "^4.0.2",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0",
+ "lru-cache": "^11.2.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz",
+ "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.1.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.2.6"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -81,6 +152,7 @@
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -90,6 +162,7 @@
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
@@ -120,6 +193,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -157,6 +231,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
@@ -173,6 +248,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -204,6 +280,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.28.6",
@@ -248,6 +325,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -257,6 +335,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
@@ -382,6 +461,29 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
"node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
@@ -391,6 +493,138 @@
"node": ">=0.1.90"
}
},
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
+ "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz",
+ "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz",
+ "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.0.2",
+ "@csstools/css-calc": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.0.tgz",
+ "integrity": "sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0"
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
"node_modules/@dabh/diagnostics": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
@@ -489,12 +723,12 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -506,12 +740,12 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -523,12 +757,12 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -540,12 +774,12 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -557,12 +791,12 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -574,12 +808,12 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -591,12 +825,12 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -608,12 +842,12 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -625,12 +859,12 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -642,12 +876,12 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -659,12 +893,12 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -676,12 +910,12 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -693,12 +927,12 @@
"cpu": [
"mips64el"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -710,12 +944,12 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -727,12 +961,12 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -744,12 +978,12 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -761,12 +995,12 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -778,12 +1012,12 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -795,12 +1029,12 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -812,12 +1046,12 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -829,12 +1063,12 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -846,12 +1080,12 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -863,12 +1097,12 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -880,12 +1114,12 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -897,12 +1131,12 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -914,12 +1148,12 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1011,6 +1245,24 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
+ "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -1176,6 +1428,7 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -1644,61 +1897,418 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/@sinclair/typebox": {
- "version": "0.34.48",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
- "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
- "license": "MIT"
- },
- "node_modules/@sindresorhus/is": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
- "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/is?sponsor=1"
- }
+ "optional": true,
+ "os": [
+ "android"
+ ]
},
- "node_modules/@so-ric/colorspace": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
- "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "color": "^5.0.2",
- "text-hex": "1.0.x"
- }
+ "optional": true,
+ "os": [
+ "android"
+ ]
},
- "node_modules/@socket.io/component-emitter": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
- "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
- "license": "MIT"
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
},
- "node_modules/@szmarczak/http-timer": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
- "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "defer-to-connect": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
},
- "node_modules/@testing-library/jest-dom": {
- "version": "5.17.0",
- "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
- "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==",
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@adobe/css-tools": "^4.0.1",
- "@babel/runtime": "^7.9.2",
- "@types/testing-library__jest-dom": "^5.9.1",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.34.48",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
+ "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
+ "license": "MIT"
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@so-ric/colorspace": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
+ "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
+ "license": "MIT",
+ "dependencies": {
+ "color": "^5.0.2",
+ "text-hex": "1.0.x"
+ }
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@szmarczak/http-timer": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+ "license": "MIT",
+ "dependencies": {
+ "defer-to-connect": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "5.17.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
+ "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.0.1",
+ "@babel/runtime": "^7.9.2",
+ "@types/testing-library__jest-dom": "^5.9.1",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css.escape": "^1.5.1",
@@ -1712,6 +2322,48 @@
"yarn": ">=1"
}
},
+ "node_modules/@testing-library/react": {
+ "version": "16.3.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1769,6 +2421,17 @@
"@types/responselike": "^1.0.0"
}
},
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
"node_modules/@types/color-convert": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz",
@@ -1793,6 +2456,13 @@
"@types/node": "*"
}
},
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1875,7 +2545,7 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -2191,6 +2861,178 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
+ "node_modules/@vitest/coverage-v8": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz",
+ "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.0.18",
+ "ast-v8-to-istanbul": "^0.3.10",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "magicast": "^0.5.1",
+ "obug": "^2.1.1",
+ "std-env": "^3.10.0",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "4.0.18",
+ "vitest": "4.0.18"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
+ "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "chai": "^6.2.1",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
+ "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.0.18",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/mocker/node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/@vitest/mocker/node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
+ "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
+ "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.0.18",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
+ "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.18",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot/node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
+ "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
+ "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.18",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/@webgpu/types": {
"version": "0.1.69",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
@@ -2233,6 +3075,16 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
@@ -2343,6 +3195,45 @@
"util": "^0.12.5"
}
},
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "0.3.12",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz",
+ "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -2420,6 +3311,7 @@
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -2428,6 +3320,16 @@
"node": ">=6.0.0"
}
},
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
@@ -2717,6 +3619,7 @@
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -2899,6 +3802,7 @@
"version": "1.0.30001776",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz",
"integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -2927,6 +3831,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
@@ -3094,6 +4008,16 @@
"node": ">= 10"
}
},
+ "node_modules/component-emitter": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
+ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3135,6 +4059,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -3307,11 +4232,51 @@
"postcss-value-parser": "^4.0.2"
}
},
- "node_modules/css.escape": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
- "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
- "license": "MIT"
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz",
+ "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.0.1",
+ "@csstools/css-syntax-patches-for-csstree": "^1.0.28",
+ "css-tree": "^3.1.0",
+ "lru-cache": "^11.2.6"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/cssstyle/node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
},
"node_modules/csstype": {
"version": "3.2.3",
@@ -3720,6 +4685,20 @@
"node": ">=12"
}
},
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -3743,6 +4722,13 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -3991,6 +4977,7 @@
"version": "1.5.307",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
"integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==",
+ "dev": true,
"license": "ISC"
},
"node_modules/elliptic": {
@@ -4115,6 +5102,19 @@
}
}
},
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@@ -4156,6 +5156,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -4179,9 +5186,9 @@
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -4221,6 +5228,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4591,6 +5599,16 @@
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -4681,6 +5699,24 @@
"pend": "~1.2.0"
}
},
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@@ -4862,6 +5898,7 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -5224,6 +6261,26 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -5244,6 +6301,20 @@
"node": ">=8.0.0"
}
},
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/http-proxy-middleware": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
@@ -5288,6 +6359,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -5631,6 +6716,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-redirect": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
@@ -5720,6 +6812,61 @@
"node": ">=10"
}
},
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/jest-diff": {
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz",
@@ -5905,6 +7052,47 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsdom": {
+ "version": "28.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz",
+ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@acemir/cssom": "^0.9.31",
+ "@asamuzakjp/dom-selector": "^6.8.1",
+ "@bramus/specificity": "^2.4.2",
+ "@exodus/bytes": "^1.11.0",
+ "cssstyle": "^6.0.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "parse5": "^8.0.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.0",
+ "undici": "^7.21.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -5948,6 +7136,7 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@@ -6083,6 +7272,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
@@ -6098,6 +7288,18 @@
"sourcemap-codec": "^1.4.8"
}
},
+ "node_modules/magicast": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz",
+ "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "source-map-js": "^1.2.1"
+ }
+ },
"node_modules/make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
@@ -6144,6 +7346,13 @@
"safe-buffer": "^5.1.2"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@@ -6366,6 +7575,7 @@
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/node-stdlib-browser": {
@@ -6542,6 +7752,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -6749,6 +7970,19 @@
"node": ">= 0.10"
}
},
+ "node_modules/parse5": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
+ "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -6809,6 +8043,13 @@
"node": ">=8"
}
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/pbkdf2": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz",
@@ -7639,6 +8880,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -7985,6 +9236,19 @@
"@parcel/watcher": "^2.4.1"
}
},
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -8226,6 +9490,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -8305,6 +9576,16 @@
"node": ">=10.0.0"
}
},
+ "node_modules/socket.io-mock": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/socket.io-mock/-/socket.io-mock-1.3.2.tgz",
+ "integrity": "sha512-p4MQBue3NAR8bXIHynRJxK/C+J3I3NpnnpgjptgLFSWv4u9Bdkubf2t0GCmyLmUTi03up0Cx/hQwzQfOpD187g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "component-emitter": "^1.3.0"
+ }
+ },
"node_modules/socket.io-parser": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
@@ -8408,6 +9689,20 @@
"node": ">=8"
}
},
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/stream-browserify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
@@ -8603,53 +9898,137 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/term-size": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz",
"integrity": "sha512-7dPUZQGy/+m3/wjVz3ZW5dobSoD/02NxJpoXUX0WIyjfVS3l0c+b/+9phIDFA7FHzkYtwtMFgeGZ/Y8jVTeqQQ==",
"license": "MIT",
"dependencies": {
- "execa": "^0.7.0"
+ "execa": "^0.7.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/text-hex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
+ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
+ "license": "MIT"
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/timed-out": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
+ "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/timers-browserify": {
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
+ "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "setimmediate": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
+ "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
},
"engines": {
- "node": ">=4"
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
}
},
- "node_modules/text-hex": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
- "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
- "license": "MIT"
- },
- "node_modules/text-table": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
},
- "node_modules/timed-out": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
- "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==",
+ "node_modules/tinyrainbow": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
+ "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">=0.10.0"
+ "node": ">=14.0.0"
}
},
- "node_modules/timers-browserify": {
- "version": "2.0.12",
- "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
- "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==",
+ "node_modules/tldts": {
+ "version": "7.0.25",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz",
+ "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "setimmediate": "^1.0.4"
+ "tldts-core": "^7.0.25"
},
- "engines": {
- "node": ">=0.6.0"
+ "bin": {
+ "tldts": "bin/cli.js"
}
},
+ "node_modules/tldts-core": {
+ "version": "7.0.25",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz",
+ "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/to-buffer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
@@ -8677,6 +10056,32 @@
"node": ">=8.0"
}
},
+ "node_modules/tough-cookie": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
+ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/triple-beam": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
@@ -8766,6 +10171,16 @@
"node": ">=14.17"
}
},
+ "node_modules/undici": {
+ "version": "7.22.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz",
+ "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"node_modules/unique-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz",
@@ -8814,6 +10229,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -9521,6 +10937,256 @@
"@esbuild/win32-x64": "0.18.20"
}
},
+ "node_modules/vitest": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
+ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.0.18",
+ "@vitest/mocker": "4.0.18",
+ "@vitest/pretty-format": "4.0.18",
+ "@vitest/runner": "4.0.18",
+ "@vitest/snapshot": "4.0.18",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "es-module-lexer": "^1.7.0",
+ "expect-type": "^1.2.2",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^3.10.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.0.3",
+ "vite": "^6.0.0 || ^7.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.0.18",
+ "@vitest/browser-preview": "4.0.18",
+ "@vitest/browser-webdriverio": "4.0.18",
+ "@vitest/ui": "4.0.18",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest/node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/vitest/node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/vitest/node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vm-browserify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
@@ -9528,12 +11194,60 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/web-vitals": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.2.tgz",
"integrity": "sha512-c0rhqNcHXRkY/ogGDJQxZ9Im9D19hDihbzSQJrsioex+KnFgmMzBiy57Z1EjkhX/+OjyBpclDCzz2ITtjokFmg==",
"license": "Apache-2.0"
},
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -9571,6 +11285,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/widest-line": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",
@@ -9688,6 +11419,23 @@
"node": ">=4"
}
},
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
@@ -9710,6 +11458,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
"license": "ISC"
},
"node_modules/yauzl": {
diff --git a/frontend/package.json b/frontend/package.json
index f1efa9ae..a3140376 100755
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,7 +13,10 @@
"tsc": "tsc",
"build": "vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:coverage": "vitest run --coverage"
},
"dependencies": {
"@emotion/is-prop-valid": "^1.3.1",
@@ -52,17 +55,23 @@
},
"devDependencies": {
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
+ "@vitest/coverage-v8": "^4.0.18",
"babel-plugin-styled-components": "^2.1.4",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
+ "jsdom": "^28.1.0",
+ "socket.io-mock": "^1.3.2",
"vite": "^4.5.0",
- "vite-plugin-node-polyfills": "^0.22.0"
+ "vite-plugin-node-polyfills": "^0.22.0",
+ "vitest": "^4.0.18"
},
"resolutions": {
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest"
diff --git a/frontend/public/assets/svg/buttons/brightness.svg b/frontend/public/assets/svg/buttons/brightness.svg
new file mode 100644
index 00000000..c04eadbf
--- /dev/null
+++ b/frontend/public/assets/svg/buttons/brightness.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/frontend/public/assets/svg/buttons/display.svg b/frontend/public/assets/svg/buttons/display.svg
new file mode 100644
index 00000000..12369f90
--- /dev/null
+++ b/frontend/public/assets/svg/buttons/display.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/__tests__/socket-io-mock.d.ts b/frontend/src/__tests__/socket-io-mock.d.ts
new file mode 100644
index 00000000..8c043611
--- /dev/null
+++ b/frontend/src/__tests__/socket-io-mock.d.ts
@@ -0,0 +1,11 @@
+declare module 'socket.io-mock' {
+ class SocketMock {
+ socketClient: {
+ on(event: string, callback: (...args: any[]) => void): void;
+ emit(event: string, ...args: any[]): void;
+ };
+ emit(event: string, ...args: any[]): void;
+ on(event: string, callback: (...args: any[]) => void): void;
+ }
+ export default SocketMock;
+}
diff --git a/frontend/src/__tests__/socket.test.ts b/frontend/src/__tests__/socket.test.ts
new file mode 100644
index 00000000..3d67fd66
--- /dev/null
+++ b/frontend/src/__tests__/socket.test.ts
@@ -0,0 +1,74 @@
+/**
+ * Tests for socket event handling logic.
+ *
+ * Uses socket.io-mock to simulate server to client events without a real backend.
+ * We test the handler functions that would be wired up in Socket.tsx, not the
+ * component itself — keeping these as pure unit tests with no React rendering.
+ */
+
+import { describe, it, expect, beforeEach } from 'vitest';
+import SocketMock from 'socket.io-mock';
+import { APP } from '../store/Store';
+
+const appStore = APP as any;
+
+/** Simulate the ignition event handler used in Socket.tsx */
+function bindIgnitionHandler(socket: SocketMock) {
+ socket.socketClient.on('ign', (ignStatus: any) => {
+ appStore.getState().update((state: any) => {
+ state.system.ignState = ignStatus as boolean;
+ });
+ });
+}
+
+type ModuleStateKey = 'canState' | 'adcState' | 'swcState' | 'rtiState';
+
+/** Simulate the module state handler for a given store */
+function bindModuleStateHandler(socket: SocketMock, key: ModuleStateKey) {
+ socket.socketClient.on('state', (value: any) => {
+ appStore.getState().update((state: any) => {
+ state.system[key] = value as boolean;
+ });
+ });
+}
+
+describe('ignition socket event to app store', () => {
+ let mockSocket: InstanceType;
+
+ beforeEach(() => {
+ mockSocket = new SocketMock();
+ appStore.setState((s: any) => ({ ...s, system: { ...s.system, ignState: true } }));
+ bindIgnitionHandler(mockSocket);
+ });
+
+ it('ign=false sets APP.system.ignState to false', () => {
+ mockSocket.emit('ign', false);
+ expect(appStore.getState().system.ignState).toBe(false);
+ });
+
+ it('ign=true restores APP.system.ignState to true', () => {
+ appStore.getState().update((s: any) => { s.system.ignState = false; });
+ mockSocket.emit('ign', true);
+ expect(appStore.getState().system.ignState).toBe(true);
+ });
+});
+
+describe('module state socket event to app store', () => {
+ let mockSocket: InstanceType;
+
+ beforeEach(() => {
+ mockSocket = new SocketMock();
+ appStore.setState((s: any) => ({ ...s, system: { ...s.system, canState: false } }));
+ bindModuleStateHandler(mockSocket, 'canState');
+ });
+
+ it('state=true sets APP.system.canState to true', () => {
+ mockSocket.emit('state', true);
+ expect(appStore.getState().system.canState).toBe(true);
+ });
+
+ it('state=false keeps APP.system.canState false', () => {
+ mockSocket.emit('state', false);
+ expect(appStore.getState().system.canState).toBe(false);
+ });
+});
diff --git a/frontend/src/__tests__/store.test.ts b/frontend/src/__tests__/store.test.ts
new file mode 100644
index 00000000..d6011b63
--- /dev/null
+++ b/frontend/src/__tests__/store.test.ts
@@ -0,0 +1,121 @@
+/**
+ * Unit tests for Zustand stores (Store.ts).
+ *
+ * These run in jsdom without any React rendering — pure store state operations.
+ */
+import { describe, it, expect, beforeEach } from 'vitest';
+import { APP, CAN, DATA } from '../store/Store';
+
+const appStore = APP as any;
+const canStore = CAN as any;
+const dataStore = DATA as any;
+
+// APP store
+describe('APP store', () => {
+ beforeEach(() => {
+ appStore.setState((s: any) => ({
+ ...s,
+ system: {
+ ...s.system,
+ ignState: true,
+ canState: false,
+ startedUp: false,
+ configLoaded: false,
+ initialized: false,
+ },
+ keyStroke: '',
+ }));
+ });
+
+ it('has correct initial system defaults', () => {
+ const { system } = appStore.getState();
+ expect(system.ignState).toBe(true);
+ expect(system.startedUp).toBe(false);
+ expect(system.configLoaded).toBe(false);
+ });
+
+ it('update() merges partial state via immer', () => {
+ const systemVersion = appStore.getState().system.version;
+ appStore.getState().update((state: any) => {
+ state.system.version = systemVersion
+ state.system.ignState = false;
+ state.system.startedUp = true;
+ });
+ const { system } = appStore.getState();
+ expect(system.ignState).toBe(false);
+ expect(system.startedUp).toBe(true);
+ });
+
+ it('update() does not clobber unrelated state', () => {
+ const systemVersion = appStore.getState().system.version;
+ appStore.getState().update((state: any) => {
+ state.system.canState = true;
+ });
+ expect(appStore.getState().system.version).toBe(systemVersion);
+ });
+
+ it('setKeyStroke() sets the keystroke value', () => {
+ appStore.getState().setKeyStroke('ArrowUp');
+ expect(appStore.getState().keyStroke).toBe('ArrowUp');
+ });
+
+ it('setKeyStroke() clears the keystroke asynchronously', async () => {
+ appStore.getState().setKeyStroke('ArrowDown');
+ expect(appStore.getState().keyStroke).toBe('ArrowDown');
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ expect(appStore.getState().keyStroke).toBe('');
+ });
+});
+
+// CAN store
+describe('CAN store', () => {
+ beforeEach(() => {
+ canStore.setState((s: any) => ({
+ ...s,
+ system: { state: false },
+ }));
+ });
+
+ it('initializes with system.state = false', () => {
+ expect(canStore.getState().system.state).toBe(false);
+ });
+
+ it('update() can set state to true', () => {
+ canStore.getState().update((state: any) => {
+ state.system.state = true;
+ });
+ expect(canStore.getState().system.state).toBe(true);
+ });
+});
+
+// DATA store
+describe('DATA store', () => {
+ beforeEach(() => {
+ dataStore.setState((s: any) => ({ ...s, data: {} }));
+ });
+
+ it('initializes with an empty data object', () => {
+ expect(dataStore.getState().data).toEqual({});
+ });
+
+ it('update() merges new values into data', () => {
+ dataStore.getState().update({ rpm: 1500, boost: 1.2 });
+ const { data } = dataStore.getState();
+ expect(data.rpm).toBe(1500);
+ expect(data.boost).toBe(1.2);
+ });
+
+ it('update() preserves existing keys when adding new ones', () => {
+ dataStore.getState().update({ rpm: 2000 });
+ dataStore.getState().update({ boost: 0.9 });
+ const { data } = dataStore.getState();
+ expect(data.rpm).toBe(2000);
+ expect(data.boost).toBe(0.9);
+ });
+
+ it('update() overwrites an existing key', () => {
+ dataStore.getState().update({ rpm: 1000 });
+ dataStore.getState().update({ rpm: 3500 });
+ expect(dataStore.getState().data.rpm).toBe(3500);
+ });
+});
diff --git a/frontend/src/app/components/CustomSlider.jsx b/frontend/src/app/components/CustomSlider.jsx
new file mode 100644
index 00000000..b33db3ea
--- /dev/null
+++ b/frontend/src/app/components/CustomSlider.jsx
@@ -0,0 +1,98 @@
+import styled from 'styled-components';
+
+/* ========= Styled Components (UI pura) ========= */
+
+const SliderContainer = styled.div`
+ width: ${({ $width }) => $width || '100%'};
+ height: ${({ theme }) => theme.interaction.buttonHeight}px;
+ display: flex;
+ align-items: center;
+ position: relative;
+ padding: 0 9px;
+ box-sizing: border-box;
+`;
+
+const Track = styled.div`
+ position: relative;
+ width: 100%;
+ height: 6px;
+ border-radius: 3px;
+ background-color: ${({ theme, backgroundColor }) =>
+ backgroundColor || theme.colors.medium};
+`;
+
+const Fill = styled.div`
+ position: absolute;
+ height: 100%;
+ border-radius: 3px;
+ width: ${({ percent }) => percent}%;
+ background-color: ${({ theme, fillColor }) =>
+ fillColor || theme.colors.theme.white.default};
+`;
+
+const Thumb = styled.div`
+ position: absolute;
+ top: 50%;
+ left: ${({ percent }) => percent}%;
+ transform: translate(-50%, -50%);
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background-color: ${({ theme, thumbColor }) =>
+ thumbColor || theme.colors.theme.white.active};
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
+ pointer-events: none; /* El input invisible captura el drag */
+`;
+
+/* Input invisible que realmente gestiona el slider */
+const HiddenRange = styled.input`
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ cursor: pointer;
+`;
+
+/* ========= Componente ========= */
+
+export default function CustomSlider({
+ value,
+ min = 0,
+ max = 100,
+ step = 1,
+ onChange,
+ backgroundColor,
+ defaultColor,
+ activeColor,
+ width
+}) {
+ /* Protección básica */
+ const safeMin = Number.isFinite(Number(min)) ? Number(min) : 0;
+ const safeMax = Number.isFinite(Number(max)) ? Number(max) : 100;
+ const clampedValue = Number.isFinite(Number(value)) ? Number(value) : safeMin;
+ const safeValue = Math.min(Math.max(clampedValue, safeMin), safeMax);
+
+ /* Cálculo del porcentaje visual */
+ const percent =
+ safeMax > safeMin
+ ? ((safeValue - safeMin) / (safeMax - safeMin)) * 100
+ : 0;
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/pages/rearcam/Rearcam.tsx b/frontend/src/app/pages/rearcam/Rearcam.tsx
index 288c8d83..c5eb905c 100644
--- a/frontend/src/app/pages/rearcam/Rearcam.tsx
+++ b/frontend/src/app/pages/rearcam/Rearcam.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import styled, { useTheme } from 'styled-components';
import { useNamespaces } from "../../../socket/Namespaces";
import { Typography } from "../../../theme/styles/Typography";
+import { APP } from "../../../store/Store";
const Container = styled.div`
position: relative;
@@ -52,16 +53,18 @@ const CenterMsg = styled.div`
export default function Rearcam() {
const videoRef = useRef(null);
const streamRef = useRef(null);
+ const openingRef = useRef(false);
const socket = useNamespaces();
const Caption = Typography.Subtitle;
const theme = useTheme();
+ const reverseCamSettings = APP((state) => state.settings.reverseCam);
const [status, setStatus] =
useState<"idle" | "opening" | "playing" | "error" | "denied">("idle");
const [err, setErr] = useState("");
- // Encender GPIO al entrar y apagar al salir
+ // Turn GPIO on when entering and off when leaving
useEffect(() => {
socket.cam.emit('mount');
return () => {
@@ -69,7 +72,7 @@ export default function Rearcam() {
};
}, []);
- // Texto inferior configurable
+ // Configurable bottom text
const overlayText = "CHECK SURROUNDINGS";
const stopStream = () => {
@@ -78,12 +81,52 @@ export default function Rearcam() {
if (videoRef.current) videoRef.current.srcObject = null;
};
- const openDefaultCamera = async () => {
+ const buildVideoConstraints = async () => {
+ const constraints: MediaTrackConstraints = {};
+ const selectionMode = reverseCamSettings?.deviceSelectionMode?.value ?? "auto";
+ const deviceId = reverseCamSettings?.deviceId?.value ?? "";
+ const deviceLabel = reverseCamSettings?.deviceLabel?.value ?? "";
+ const width = Number(reverseCamSettings?.videoWidth?.value ?? 0);
+ const height = Number(reverseCamSettings?.videoHeight?.value ?? 0);
+ const fps = Number(reverseCamSettings?.videoFps?.value ?? 0);
+
+ if (Number.isFinite(width) && width > 0) constraints.width = { ideal: width };
+ if (Number.isFinite(height) && height > 0) constraints.height = { ideal: height };
+ if (Number.isFinite(fps) && fps > 0) constraints.frameRate = { ideal: fps };
+
+ if (selectionMode === "deviceId" && deviceId && deviceId !== "default") {
+ constraints.deviceId = { exact: deviceId };
+ } else if (selectionMode === "label" && deviceLabel && navigator?.mediaDevices?.enumerateDevices) {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const match = devices.find(
+ (device) =>
+ device.kind === "videoinput" &&
+ device.label.toLowerCase().includes(deviceLabel.toLowerCase())
+ );
+ if (match?.deviceId) {
+ constraints.deviceId = { exact: match.deviceId };
+ }
+ }
+
+ return constraints;
+ };
+
+ const openCamera = async () => {
+ if (openingRef.current) return;
+ openingRef.current = true;
setStatus("opening"); setErr("");
try {
stopStream();
+ if (!navigator?.mediaDevices?.getUserMedia) {
+ throw new Error("Media devices API not available");
+ }
+
+ const constraints = await buildVideoConstraints();
+ const videoConstraint =
+ Object.keys(constraints).length > 0 ? constraints : true;
+
const stream = await navigator.mediaDevices.getUserMedia({
- video: true,
+ video: videoConstraint,
audio: false,
});
streamRef.current = stream;
@@ -95,23 +138,31 @@ export default function Rearcam() {
} catch (e: any) {
setStatus(e?.name === "NotAllowedError" ? "denied" : "error");
setErr(e?.message || "Failed to open camera");
+ } finally {
+ openingRef.current = false;
}
};
- // Auto-inicio al entrar
+ // Auto-start on enter
useEffect(() => {
- openDefaultCamera();
+ openCamera();
return () => { stopStream(); };
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [
+ reverseCamSettings?.deviceSelectionMode?.value,
+ reverseCamSettings?.deviceId?.value,
+ reverseCamSettings?.deviceLabel?.value,
+ reverseCamSettings?.videoWidth?.value,
+ reverseCamSettings?.videoHeight?.value,
+ reverseCamSettings?.videoFps?.value,
+ ]);
- // Reintento si se conecta/desconecta el grabber
+ // Retry when the capture device connects/disconnects
useEffect(() => {
const h = async () => {
- if (status === "playing") {
- await openDefaultCamera();
- }
+ if (status !== "opening" && status !== "denied") await openCamera();
};
+ if (!navigator?.mediaDevices?.addEventListener) return;
navigator.mediaDevices.addEventListener("devicechange", h);
return () => navigator.mediaDevices.removeEventListener("devicechange", h);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -127,10 +178,10 @@ export default function Rearcam() {
alt="Rear camera guidelines"
/>
- {/* Texto inferior */}
+ {/* Bottom text */}
{overlayText}
- {/* Mensajes de error */}
+ {/* Error messages */}
{status === "error" && Error: {err}}
{status === "denied" && (
Camera access denied. Check browser permissions.
diff --git a/frontend/src/app/pages/settings/Settings.tsx b/frontend/src/app/pages/settings/Settings.tsx
index e04bb63d..20a18cbe 100755
--- a/frontend/src/app/pages/settings/Settings.tsx
+++ b/frontend/src/app/pages/settings/Settings.tsx
@@ -4,6 +4,7 @@ import styled, { useTheme } from 'styled-components';
import ScrollContainer from 'react-indiana-drag-scroll'
import { ToggleSwitch, Select, Input, Button } from '../../../theme/styles/Inputs';
+import CustomSlider from '../../components/CustomSlider';
import { Typography } from '../../../theme/styles/Typography';
import { APP } from '../../../store/Store';
@@ -88,11 +89,16 @@ const Settings = () => {
const swcState = APP((state) => state.system.swcState);
const theme = useTheme();
+ const rangeWidth = Number(theme.interaction.buttonWidth) * 2;
const [save, setSave] = useState(true)
const [reset, setReset] = useState(false)
const [currentSettings, setCurrentSettings] = useState(structuredClone(settings));
+ const [cameraDevices, setCameraDevices] = useState<{ deviceId: string; label: string }[]>([]);
+ const saveTimeoutRef = useRef(null);
+ const pendingSettingsRef = useRef(currentSettings);
+ const SAVE_DEBOUNCE_MS = 500;
const setKeyStroke = APP((state) => state.setKeyStroke);
const setSwitchPage = APP((state) => state.setSwitchPage);
@@ -109,6 +115,41 @@ const Settings = () => {
});
}, [modules]);
+ useEffect(() => {
+ if (!navigator?.mediaDevices?.enumerateDevices) return;
+
+ const updateDevices = async () => {
+ try {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const videoInputs = devices
+ .filter((device) => device.kind === 'videoinput')
+ .map((device, index) => ({
+ deviceId: device.deviceId,
+ label: device.label || `Camera ${index + 1}`,
+ }));
+ setCameraDevices(videoInputs);
+ } catch {
+ setCameraDevices([]);
+ }
+ };
+
+ updateDevices();
+ navigator.mediaDevices.addEventListener('devicechange', updateDevices);
+ return () => navigator.mediaDevices.removeEventListener('devicechange', updateDevices);
+ }, []);
+
+ useEffect(() => {
+ pendingSettingsRef.current = currentSettings;
+ }, [currentSettings]);
+
+ useEffect(() => {
+ return () => {
+ if (saveTimeoutRef.current) {
+ clearTimeout(saveTimeoutRef.current);
+ }
+ };
+ }, []);
+
/* Reset container to top when settings are reset */
useEffect(() => {
if (reset) {
@@ -132,6 +173,19 @@ const Settings = () => {
});
/* Add Settings */
+ const scheduleSave = (nextSettings) => {
+ setSave(false);
+ pendingSettingsRef.current = nextSettings;
+
+ if (saveTimeoutRef.current) {
+ clearTimeout(saveTimeoutRef.current);
+ }
+
+ saveTimeoutRef.current = setTimeout(() => {
+ saveSettings(pendingSettingsRef.current);
+ }, SAVE_DEBOUNCE_MS);
+ };
+
const handleAddSetting = (key, currentSettings) => {
if (currentSettings.constants.chart_input_current < currentSettings.constants.chart_input_max) {
const newSetting = {
@@ -147,14 +201,16 @@ const Settings = () => {
updatedSettingsForKey[newSettingId] = newSetting;
// Update the state with the new settings
- setCurrentSettings({
+ const nextSettings = {
...currentSettings,
constants: {
...currentSettings.constants,
chart_input_current: currentSettings.constants.chart_input_current + 1,
},
[key]: updatedSettingsForKey,
- });
+ };
+ setCurrentSettings(nextSettings);
+ scheduleSave(nextSettings);
} else {
console.error(`Key "${key}" not found in settings.`);
}
@@ -169,14 +225,16 @@ const Settings = () => {
delete updatedSettingsForKey[settingIdToRemove];
// Update the state with the minus the removed one
- setCurrentSettings({
+ const nextSettings = {
...currentSettings,
constants: {
...currentSettings.constants,
chart_input_current: currentSettings.constants.chart_input_current - 1,
},
[key]: updatedSettingsForKey,
- });
+ };
+ setCurrentSettings(nextSettings);
+ scheduleSave(nextSettings);
} else {
console.error("Cannot remove setting, minimum limit reached.");
}
@@ -187,8 +245,6 @@ const Settings = () => {
// Change Settings
const handleSettingChange = (selectStore, key, name, targetSetting, currentSettings) => {
- setSave(false)
-
const newSettings = structuredClone(currentSettings);
let convertedValue
if (selectStore != 'app') {
@@ -202,15 +258,19 @@ const Settings = () => {
}
setCurrentSettings(newSettings);
+ scheduleSave(newSettings);
};
// Save Settings
- function saveSettings() {
+ function saveSettings(settingsToSave = currentSettings) {
+ if (saveTimeoutRef.current) {
+ clearTimeout(saveTimeoutRef.current);
+ }
setSave(true)
appUpdate((state) => {
- state.settings = currentSettings;
+ state.settings = settingsToSave;
});
- socket.app.emit("save", currentSettings);
+ socket.app.emit("save", settingsToSave);
}
// System Tasks
@@ -299,7 +359,52 @@ const Settings = () => {
/* Based on the "type", either data or interface settings are provided to the main settings file.
*/
- if (!settingsObj) return null;
+ if (!settingsObj || !settingsObj[key]) return null;
+
+ if (settingsObj[key]?.ui === 'range') {
+ const { label, value, min, max, step } = settingsObj[key];
+
+ const handleRangeChange = (event) => {
+ const newValue = Number(event.target.value);
+ const nextSettings = {
+ ...settingsObj,
+ [key]: {
+ ...settingsObj[key],
+ value: newValue,
+ },
+ };
+ setCurrentSettings(nextSettings);
+ if (key === 'daylight_backlight') {
+ socket.app.emit('backlight:update', { daylight: newValue });
+ }
+ if (key === 'darkness_backlight') {
+ socket.app.emit('backlight:update', { darkness: newValue });
+ }
+ scheduleSave(nextSettings);
+ };
+
+ const labelStyle = key === 'daylight_backlight' ? { whiteSpace: 'nowrap' } : undefined;
+
+ return (
+
+ {label}
+
+
+
+
+
+ );
+ }
// Get label, type, and nested options from setting block
const { title, type, ...nestedSettings } = settingsObj[key];
@@ -333,11 +438,24 @@ const Settings = () => {
//Check if value is a number or boolean
const isText = (content.type === 'text')
+ const isRearcamDeviceId = key === 'reverseCam' && setting === 'deviceId';
+ const rearcamDeviceOptions = isRearcamDeviceId
+ ? [
+ { value: 'default', label: 'Default' },
+ ...cameraDevices.map((device) => ({
+ value: device.deviceId,
+ label: device.label,
+ })),
+ ]
+ : null;
+
const dropdown = (isText || typeof value === 'number' || typeof value === 'boolean' || key.includes('bindings'))
? null //Yes? Return null
- : (content.options || Object.keys(dataOptions).map((key) => //No? Create dropdown from options
- key
- ))
+ : ((rearcamDeviceOptions && rearcamDeviceOptions.length > 0)
+ ? rearcamDeviceOptions
+ : (content.options || Object.keys(dataOptions).map((key) => //No? Create dropdown from options
+ key
+ )))
// Check for boolean setting
const isBoolean = typeof value === 'boolean'; // Checks if the setting is a boolean.
@@ -357,6 +475,9 @@ const Settings = () => {
: "app";
const targetSetting = isBoolean ? checked : newValue // Handle targetSetting based on type
+ if (key === 'auto_backlight' && setting === 'autoOpen') {
+ socket.app.emit('backlight:update', { auto_enabled: targetSetting });
+ }
handleSettingChange(selectStore, key, name, targetSetting, settingsObj); // Execute change of settings
};
@@ -442,9 +563,11 @@ const Settings = () => {
return (
<>
-
- {title.toUpperCase()}
-
+ {title && (
+
+ {title.toUpperCase()}
+
+ )}
{nestedElements}
>
);
@@ -488,7 +611,6 @@ const Settings = () => {
{settingPage === 1 &&
<>
{renderSetting("general", currentSettings)}
- {renderSetting("screen", currentSettings)}
{renderSetting("shutdown", currentSettings)}
{renderSetting("side_bars", currentSettings)}
{renderSetting("reverseCam", currentSettings)}
@@ -541,21 +663,6 @@ const Settings = () => {
}
-
- {settings.constants.modules.rti &&
-
- {`RTI ${rtiState ? '(Active)' : '(Inactive)'}`}
-
-
- { handleIO("rti", socket.rti) }} disabled={!settings.constants.modules.rti} />
-
-
-
- }
>
}
@@ -611,6 +718,41 @@ const Settings = () => {
>
}
+ {settingPage === 6 &&
+ <>
+ {renderSetting("screen", currentSettings)}
+
+ Backlight Settings
+
+ {renderSetting("daylight_backlight", currentSettings)}
+ {renderSetting("auto_backlight", currentSettings)}
+ {renderSetting("darkness_backlight", currentSettings)}
+
+ {settings.constants.modules.rti &&
+
+ {`RTI ${rtiState ? '(Active)' : '(Inactive)'}`}
+
+
+ { handleIO("rti", socket.rti) }} disabled={!settings.constants.modules.rti} />
+
+
+
+ }
+
+ >
+ }
+
+ {settingPage === 7 &&
+ <>
+ {renderSetting("reverseCam", currentSettings)}
+
+ >
+ }
+