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)} +

+ + } +