diff --git a/frigate/api/app.py b/frigate/api/app.py index 04d1c2238..a28f174de 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -49,12 +49,13 @@ from frigate.stats.prometheus import get_metrics, update_metrics from frigate.types import JobStatusTypesEnum from frigate.util.builtin import ( clean_camera_user_pass, + deep_merge, flatten_config_data, load_labels, process_config_query_string, update_yaml_file_bulk, ) -from frigate.util.config import find_config_file +from frigate.util.config import apply_section_update, find_config_file from frigate.util.schema import get_config_schema from frigate.util.services import ( get_nvidia_driver_info, @@ -422,9 +423,100 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")): ) +def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONResponse: + """Apply config changes in-memory only, without writing to YAML. + + Used for temporary config changes like debug replay camera tuning. + Updates the in-memory Pydantic config and publishes ZMQ updates, + bypassing YAML parsing entirely. + """ + try: + updates = {} + if body.config_data: + updates = flatten_config_data(body.config_data) + updates = {k: ("" if v is None else v) for k, v in updates.items()} + + if not updates: + return JSONResponse( + content={"success": False, "message": "No configuration data provided"}, + status_code=400, + ) + + config: FrigateConfig = request.app.frigate_config + + # Group flat key paths into nested per-camera, per-section dicts + grouped: dict[str, dict[str, dict]] = {} + for key_path, value in updates.items(): + parts = key_path.split(".") + if len(parts) < 3 or parts[0] != "cameras": + continue + + cam, section = parts[1], parts[2] + grouped.setdefault(cam, {}).setdefault(section, {}) + + # Build nested dict from remaining path (e.g. "filters.person.threshold") + target = grouped[cam][section] + for part in parts[3:-1]: + target = target.setdefault(part, {}) + if len(parts) > 3: + target[parts[-1]] = value + elif isinstance(value, dict): + grouped[cam][section] = deep_merge( + grouped[cam][section], value, override=True + ) + else: + grouped[cam][section] = value + + # Apply each section update + for cam_name, sections in grouped.items(): + camera_config = config.cameras.get(cam_name) + if not camera_config: + return JSONResponse( + content={ + "success": False, + "message": f"Camera '{cam_name}' not found", + }, + status_code=400, + ) + + for section_name, update in sections.items(): + err = apply_section_update(camera_config, section_name, update) + if err is not None: + return JSONResponse( + content={"success": False, "message": err}, + status_code=400, + ) + + # Publish ZMQ updates so processing threads pick up changes + if body.update_topic and body.update_topic.startswith("config/cameras/"): + _, _, camera, field = body.update_topic.split("/") + settings = getattr(config.cameras.get(camera, None), field, None) + + if settings is not None: + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum[field], camera), + settings, + ) + + return JSONResponse( + content={"success": True, "message": "Config applied in-memory"}, + status_code=200, + ) + except Exception as e: + logger.error(f"Error applying config in-memory: {e}") + return JSONResponse( + content={"success": False, "message": "Error applying config"}, + status_code=500, + ) + + @router.put("/config/set", dependencies=[Depends(require_role(["admin"]))]) def config_set(request: Request, body: AppConfigSetBody): config_file = find_config_file() + + if body.skip_save: + return _config_set_in_memory(request, body) + lock = FileLock(f"{config_file}.lock", timeout=5) try: diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 04a5bd19a..39089b583 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -32,6 +32,12 @@ from frigate.models import User logger = logging.getLogger(__name__) +# In-memory cache to track which clients we've logged for an anonymous access event. +# Keyed by a hashed value combining remote address + user-agent. The value is +# an expiration timestamp (float). +FIRST_LOAD_TTL_SECONDS = 60 * 60 * 24 * 7 # 7 days +_first_load_seen: dict[str, float] = {} + def require_admin_by_default(): """ @@ -284,6 +290,15 @@ def get_remote_addr(request: Request): return remote_addr or "127.0.0.1" +def _cleanup_first_load_seen() -> None: + """Cleanup expired entries in the in-memory first-load cache.""" + now = time.time() + # Build list for removal to avoid mutating dict during iteration + expired = [k for k, exp in _first_load_seen.items() if exp <= now] + for k in expired: + del _first_load_seen[k] + + def get_jwt_secret() -> str: jwt_secret = None # check env var @@ -744,10 +759,30 @@ def profile(request: Request): roles_dict = request.app.frigate_config.auth.roles allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names) - return JSONResponse( + response = JSONResponse( content={"username": username, "role": role, "allowed_cameras": allowed_cameras} ) + if username == "anonymous": + try: + remote_addr = get_remote_addr(request) + except Exception: + remote_addr = ( + request.client.host if hasattr(request, "client") else "unknown" + ) + + ua = request.headers.get("user-agent", "") + key_material = f"{remote_addr}|{ua}" + cache_key = hashlib.sha256(key_material.encode()).hexdigest() + + _cleanup_first_load_seen() + now = time.time() + if cache_key not in _first_load_seen: + _first_load_seen[cache_key] = now + FIRST_LOAD_TTL_SECONDS + logger.info(f"Anonymous user access from {remote_addr} ua={ua[:200]}") + + return response + @router.get( "/logout", diff --git a/frigate/api/debug_replay.py b/frigate/api/debug_replay.py new file mode 100644 index 000000000..027d4e50c --- /dev/null +++ b/frigate/api/debug_replay.py @@ -0,0 +1,176 @@ +"""Debug replay API endpoints.""" + +import asyncio +import logging +from datetime import datetime + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field + +from frigate.api.auth import require_role +from frigate.api.defs.tags import Tags + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.app]) + + +class DebugReplayStartBody(BaseModel): + """Request body for starting a debug replay session.""" + + camera: str = Field(title="Source camera name") + start_time: float = Field(title="Start timestamp") + end_time: float = Field(title="End timestamp") + + +class DebugReplayStartResponse(BaseModel): + """Response for starting a debug replay session.""" + + success: bool + replay_camera: str + + +class DebugReplayStatusResponse(BaseModel): + """Response for debug replay status.""" + + active: bool + replay_camera: str | None = None + source_camera: str | None = None + start_time: float | None = None + end_time: float | None = None + live_ready: bool = False + + +class DebugReplayStopResponse(BaseModel): + """Response for stopping a debug replay session.""" + + success: bool + + +@router.post( + "/debug_replay/start", + response_model=DebugReplayStartResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Start debug replay", + description="Start a debug replay session from camera recordings.", +) +async def start_debug_replay(request: Request, body: DebugReplayStartBody): + """Start a debug replay session.""" + replay_manager = request.app.replay_manager + + if replay_manager.active: + return JSONResponse( + content={ + "success": False, + "message": "A replay session is already active", + }, + status_code=409, + ) + + try: + replay_camera = await asyncio.to_thread( + replay_manager.start, + source_camera=body.camera, + start_ts=body.start_time, + end_ts=body.end_time, + frigate_config=request.app.frigate_config, + config_publisher=request.app.config_publisher, + ) + except ValueError: + logger.exception("Invalid parameters for debug replay start request") + return JSONResponse( + content={ + "success": False, + "message": "Invalid debug replay request parameters", + }, + status_code=400, + ) + except RuntimeError: + logger.exception("Error while starting debug replay session") + return JSONResponse( + content={ + "success": False, + "message": "An internal error occurred while starting debug replay", + }, + status_code=500, + ) + + return DebugReplayStartResponse( + success=True, + replay_camera=replay_camera, + ) + + +@router.get( + "/debug_replay/status", + response_model=DebugReplayStatusResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Get debug replay status", + description="Get the status of the current debug replay session.", +) +def get_debug_replay_status(request: Request): + """Get the current replay session status.""" + replay_manager = request.app.replay_manager + + live_ready = False + replay_camera = replay_manager.replay_camera_name + + if replay_manager.active and replay_camera: + frame_processor = request.app.detected_frames_processor + frame = frame_processor.get_current_frame(replay_camera) + + if frame is not None: + frame_time = frame_processor.get_current_frame_time(replay_camera) + camera_config = request.app.frigate_config.cameras.get(replay_camera) + retry_interval = 10 + + if camera_config is not None: + retry_interval = float(camera_config.ffmpeg.retry_interval or 10) + + live_ready = datetime.now().timestamp() <= frame_time + retry_interval + + return DebugReplayStatusResponse( + active=replay_manager.active, + replay_camera=replay_camera, + source_camera=replay_manager.source_camera, + start_time=replay_manager.start_ts, + end_time=replay_manager.end_ts, + live_ready=live_ready, + ) + + +@router.post( + "/debug_replay/stop", + response_model=DebugReplayStopResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Stop debug replay", + description="Stop the active debug replay session and clean up all artifacts.", +) +async def stop_debug_replay(request: Request): + """Stop the active replay session.""" + replay_manager = request.app.replay_manager + + if not replay_manager.active: + return JSONResponse( + content={"success": False, "message": "No active replay session"}, + status_code=400, + ) + + try: + await asyncio.to_thread( + replay_manager.stop, + frigate_config=request.app.frigate_config, + config_publisher=request.app.config_publisher, + ) + except (ValueError, RuntimeError, OSError) as e: + logger.error("Error stopping replay: %s", e) + return JSONResponse( + content={ + "success": False, + "message": "Failed to stop replay session due to an internal error.", + }, + status_code=500, + ) + + return DebugReplayStopResponse(success=True) diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py index 6059daf6e..3d2ab5961 100644 --- a/frigate/api/defs/request/app_body.py +++ b/frigate/api/defs/request/app_body.py @@ -7,6 +7,7 @@ class AppConfigSetBody(BaseModel): requires_restart: int = 1 update_topic: str | None = None config_data: Optional[Dict[str, Any]] = None + skip_save: bool = False class AppPutPasswordBody(BaseModel): diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 54fac7f80..0a731bcee 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -18,6 +18,7 @@ from frigate.api import ( camera, chat, classification, + debug_replay, event, export, media, @@ -33,6 +34,7 @@ from frigate.comms.event_metadata_updater import ( ) from frigate.config import FrigateConfig from frigate.config.camera.updater import CameraConfigUpdatePublisher +from frigate.debug_replay import DebugReplayManager from frigate.embeddings import EmbeddingsContext from frigate.genai import GenAIClientManager from frigate.ptz.onvif import OnvifController @@ -66,6 +68,7 @@ def create_fastapi_app( stats_emitter: StatsEmitter, event_metadata_updater: EventMetadataPublisher, config_publisher: CameraConfigUpdatePublisher, + replay_manager: DebugReplayManager, enforce_default_admin: bool = True, ): logger.info("Starting FastAPI app") @@ -135,6 +138,7 @@ def create_fastapi_app( app.include_router(media.router) app.include_router(motion_search.router) app.include_router(record.router) + app.include_router(debug_replay.router) # App Properties app.frigate_config = frigate_config app.genai_manager = GenAIClientManager(frigate_config) @@ -146,6 +150,7 @@ def create_fastapi_app( app.stats_emitter = stats_emitter app.event_metadata_updater = event_metadata_updater app.config_publisher = config_publisher + app.replay_manager = replay_manager if frigate_config.auth.enabled: secret = get_jwt_secret() diff --git a/frigate/api/media.py b/frigate/api/media.py index 3cfd97674..2ddabc631 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -24,6 +24,7 @@ from tzlocal import get_localzone_name from frigate.api.auth import ( allow_any_authenticated, require_camera_access, + require_role, ) from frigate.api.defs.query.media_query_parameters import ( Extension, @@ -1005,6 +1006,23 @@ def grid_snapshot( ) +@router.delete( + "/{camera_name}/region_grid", dependencies=[Depends(require_role("admin"))] +) +def clear_region_grid(request: Request, camera_name: str): + """Clear the region grid for a camera.""" + if camera_name not in request.app.frigate_config.cameras: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + Regions.delete().where(Regions.camera == camera_name).execute() + return JSONResponse( + content={"success": True, "message": "Region grid cleared"}, + ) + + @router.get( "/events/{event_id}/snapshot-clean.webp", dependencies=[Depends(require_camera_access)], diff --git a/frigate/app.py b/frigate/app.py index 724ed1d72..0add3e3b8 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -43,6 +43,10 @@ from frigate.const import ( ) from frigate.data_processing.types import DataProcessorMetrics from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.debug_replay import ( + DebugReplayManager, + cleanup_replay_cameras, +) from frigate.embeddings import EmbeddingProcess, EmbeddingsContext from frigate.events.audio import AudioProcessor from frigate.events.cleanup import EventCleanup @@ -140,6 +144,9 @@ class FrigateApp: else: logger.debug(f"Skipping directory: {d}") + def init_debug_replay_manager(self) -> None: + self.replay_manager = DebugReplayManager() + def init_camera_metrics(self) -> None: # create camera_metrics for camera_name in self.config.cameras.keys(): @@ -532,6 +539,7 @@ class FrigateApp: set_file_limit() # Start frigate services. + self.init_debug_replay_manager() self.init_camera_metrics() self.init_queues() self.init_database() @@ -542,6 +550,10 @@ class FrigateApp: self.init_embeddings_manager() self.bind_database() self.check_db_data_migrations() + + # Clean up any stale replay camera artifacts (filesystem + DB) + cleanup_replay_cameras() + self.init_inter_process_communicator() self.start_detectors() self.init_dispatcher() @@ -573,6 +585,7 @@ class FrigateApp: self.stats_emitter, self.event_metadata_updater, self.inter_config_updater, + self.replay_manager, ), host="127.0.0.1", port=5001, @@ -641,6 +654,7 @@ class FrigateApp: self.record_cleanup.join() self.stats_emitter.join() self.frigate_watchdog.join() + self.camera_maintainer.join() self.db.stop() # Save embeddings stats to disk diff --git a/frigate/camera/activity_manager.py b/frigate/camera/activity_manager.py index c2dfa891d..3f229e490 100644 --- a/frigate/camera/activity_manager.py +++ b/frigate/camera/activity_manager.py @@ -57,6 +57,9 @@ class CameraActivityManager: all_objects: list[dict[str, Any]] = [] for camera in new_activity.keys(): + if camera not in self.config.cameras: + continue + # handle cameras that were added dynamically if camera not in self.camera_all_object_counts: self.__init_camera(self.config.cameras[camera]) @@ -124,7 +127,11 @@ class CameraActivityManager: any_changed = False # run through each object and check what topics need to be updated - for label in self.config.cameras[camera].objects.track: + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return + + for label in camera_config.objects.track: if label in self.config.model.non_logo_attributes: continue @@ -174,6 +181,9 @@ class AudioActivityManager: now = datetime.datetime.now().timestamp() for camera in new_activity.keys(): + if camera not in self.config.cameras: + continue + # handle cameras that were added dynamically if camera not in self.current_audio_detections: self.__init_camera(self.config.cameras[camera]) @@ -193,7 +203,11 @@ class AudioActivityManager: def compare_audio_activity( self, camera: str, new_detections: list[tuple[str, float]], now: float ) -> None: - max_not_heard = self.config.cameras[camera].audio.max_not_heard + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return False + + max_not_heard = camera_config.audio.max_not_heard current = self.current_audio_detections[camera] any_changed = False @@ -222,6 +236,7 @@ class AudioActivityManager: None, "audio", {}, + None, ), EventMetadataTypeEnum.manual_event_create.value, ) diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py index 815e650e9..9cfdcc7f3 100644 --- a/frigate/camera/maintainer.py +++ b/frigate/camera/maintainer.py @@ -55,8 +55,20 @@ class CameraMaintainer(threading.Thread): self.shm_count = self.__calculate_shm_frame_count() self.camera_processes: dict[str, mp.Process] = {} self.capture_processes: dict[str, mp.Process] = {} + self.camera_stop_events: dict[str, MpEvent] = {} self.metrics_manager = metrics_manager + def __ensure_camera_stop_event(self, camera: str) -> MpEvent: + camera_stop_event = self.camera_stop_events.get(camera) + + if camera_stop_event is None: + camera_stop_event = mp.Event() + self.camera_stop_events[camera] = camera_stop_event + else: + camera_stop_event.clear() + + return camera_stop_event + def __init_historical_regions(self) -> None: # delete region grids for removed or renamed cameras cameras = list(self.config.cameras.keys()) @@ -99,6 +111,8 @@ class CameraMaintainer(threading.Thread): logger.info(f"Camera processor not started for disabled camera {name}") return + camera_stop_event = self.__ensure_camera_stop_event(name) + if runtime: self.camera_metrics[name] = CameraMetrics(self.metrics_manager) self.ptz_metrics[name] = PTZMetrics(autotracker_enabled=False) @@ -135,7 +149,7 @@ class CameraMaintainer(threading.Thread): self.camera_metrics[name], self.ptz_metrics[name], self.region_grids[name], - self.stop_event, + camera_stop_event, self.config.logger, ) self.camera_processes[config.name] = camera_process @@ -150,6 +164,8 @@ class CameraMaintainer(threading.Thread): logger.info(f"Capture process not started for disabled camera {name}") return + camera_stop_event = self.__ensure_camera_stop_event(name) + # pre-create shms count = 10 if runtime else self.shm_count for i in range(count): @@ -160,7 +176,7 @@ class CameraMaintainer(threading.Thread): config, count, self.camera_metrics[name], - self.stop_event, + camera_stop_event, self.config.logger, ) capture_process.daemon = True @@ -170,18 +186,36 @@ class CameraMaintainer(threading.Thread): logger.info(f"Capture process started for {name}: {capture_process.pid}") def __stop_camera_capture_process(self, camera: str) -> None: - capture_process = self.capture_processes[camera] + capture_process = self.capture_processes.get(camera) if capture_process is not None: logger.info(f"Waiting for capture process for {camera} to stop") - capture_process.terminate() - capture_process.join() + camera_stop_event = self.camera_stop_events.get(camera) + + if camera_stop_event is not None: + camera_stop_event.set() + + capture_process.join(timeout=10) + if capture_process.is_alive(): + logger.warning( + f"Capture process for {camera} didn't exit, forcing termination" + ) + capture_process.terminate() + capture_process.join() def __stop_camera_process(self, camera: str) -> None: - camera_process = self.camera_processes[camera] + camera_process = self.camera_processes.get(camera) if camera_process is not None: logger.info(f"Waiting for process for {camera} to stop") - camera_process.terminate() - camera_process.join() + camera_stop_event = self.camera_stop_events.get(camera) + + if camera_stop_event is not None: + camera_stop_event.set() + + camera_process.join(timeout=10) + if camera_process.is_alive(): + logger.warning(f"Process for {camera} didn't exit, forcing termination") + camera_process.terminate() + camera_process.join() logger.info(f"Closing frame queue for {camera}") empty_and_close_queue(self.camera_metrics[camera].frame_queue) @@ -199,6 +233,12 @@ class CameraMaintainer(threading.Thread): for update_type, updated_cameras in updates.items(): if update_type == CameraConfigUpdateEnum.add.name: for camera in updated_cameras: + if ( + camera in self.camera_processes + or camera in self.capture_processes + ): + continue + self.__start_camera_processor( camera, self.update_subscriber.camera_configs[camera], @@ -210,15 +250,22 @@ class CameraMaintainer(threading.Thread): runtime=True, ) elif update_type == CameraConfigUpdateEnum.remove.name: - self.__stop_camera_capture_process(camera) - self.__stop_camera_process(camera) + for camera in updated_cameras: + self.__stop_camera_capture_process(camera) + self.__stop_camera_process(camera) + self.capture_processes.pop(camera, None) + self.camera_processes.pop(camera, None) + self.camera_stop_events.pop(camera, None) + self.region_grids.pop(camera, None) + self.camera_metrics.pop(camera, None) + self.ptz_metrics.pop(camera, None) # ensure the capture processes are done - for camera in self.camera_processes.keys(): + for camera in self.capture_processes.keys(): self.__stop_camera_capture_process(camera) # ensure the camera processors are done - for camera in self.capture_processes.keys(): + for camera in self.camera_processes.keys(): self.__stop_camera_process(camera) self.update_subscriber.stop() diff --git a/frigate/comms/config_updater.py b/frigate/comms/config_updater.py index 447089a94..4552abc11 100644 --- a/frigate/comms/config_updater.py +++ b/frigate/comms/config_updater.py @@ -26,8 +26,8 @@ class ConfigPublisher: def stop(self) -> None: self.stop_event.set() - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) class ConfigSubscriber: @@ -55,5 +55,5 @@ class ConfigSubscriber: return (None, None) def stop(self) -> None: - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 6da154814..490a829dc 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -110,6 +110,9 @@ class Dispatcher: payload: str, sub_command: str | None = None, ) -> None: + if camera_name not in self.config.cameras: + return + try: if command_type == "set": if sub_command: @@ -131,6 +134,9 @@ class Dispatcher: def handle_request_region_grid() -> Any: camera = payload + if camera not in self.config.cameras: + return None + grid = get_camera_regions_grid( camera, self.config.cameras[camera].detect, @@ -243,7 +249,11 @@ class Dispatcher: self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy())) def handle_on_connect() -> None: - camera_status = self.camera_activity.last_camera_activity.copy() + camera_status = { + camera: status + for camera, status in self.camera_activity.last_camera_activity.copy().items() + if camera in self.config.cameras + } audio_detections = self.audio_activity.current_audio_detections.copy() cameras_with_status = camera_status.keys() @@ -346,7 +356,8 @@ class Dispatcher: # example /cam_name/notifications/suspend payload=duration camera_name = parts[-3] command = parts[-2] - self._on_camera_notification_suspend(camera_name, payload) + if camera_name in self.config.cameras: + self._on_camera_notification_suspend(camera_name, payload) except IndexError: logger.error( f"Received invalid {topic.split('/')[-1]} command: {topic}" diff --git a/frigate/comms/inter_process.py b/frigate/comms/inter_process.py index e4aad9107..5e76da5eb 100644 --- a/frigate/comms/inter_process.py +++ b/frigate/comms/inter_process.py @@ -61,8 +61,8 @@ class InterProcessCommunicator(Communicator): def stop(self) -> None: self.stop_event.set() self.reader_thread.join() - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) class InterProcessRequestor: @@ -82,5 +82,5 @@ class InterProcessRequestor: return "" def stop(self) -> None: - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) diff --git a/frigate/comms/zmq_proxy.py b/frigate/comms/zmq_proxy.py index 29329ec59..4a4a0492a 100644 --- a/frigate/comms/zmq_proxy.py +++ b/frigate/comms/zmq_proxy.py @@ -43,7 +43,7 @@ class ZmqProxy: def stop(self) -> None: # destroying the context will tell the proxy to stop - self.context.destroy() + self.context.destroy(linger=0) self.runner.join() @@ -66,8 +66,8 @@ class Publisher(Generic[T]): self.socket.send_string(f"{self.topic}{sub_topic} {json.dumps(payload)}") def stop(self) -> None: - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) class Subscriber(Generic[T]): @@ -96,8 +96,8 @@ class Subscriber(Generic[T]): return self._return_object("", None) def stop(self) -> None: - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) def _return_object(self, topic: str, payload: T | None) -> T | None: return payload diff --git a/frigate/config/camera/updater.py b/frigate/config/camera/updater.py index 125094f10..44aea527d 100644 --- a/frigate/config/camera/updater.py +++ b/frigate/config/camera/updater.py @@ -80,8 +80,8 @@ class CameraConfigUpdateSubscriber: self.camera_configs[camera] = updated_config return elif update_type == CameraConfigUpdateEnum.remove: - self.config.cameras.pop(camera) - self.camera_configs.pop(camera) + self.config.cameras.pop(camera, None) + self.camera_configs.pop(camera, None) return config = self.camera_configs.get(camera) diff --git a/frigate/const.py b/frigate/const.py index 87fdb8e70..6b1e227d5 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -14,6 +14,8 @@ RECORD_DIR = f"{BASE_DIR}/recordings" TRIGGER_DIR = f"{CLIPS_DIR}/triggers" BIRDSEYE_PIPE = "/tmp/cache/birdseye" CACHE_DIR = "/tmp/cache" +REPLAY_CAMERA_PREFIX = "_replay_" +REPLAY_DIR = os.path.join(CACHE_DIR, "replay") PLUS_ENV_VAR = "PLUS_API_KEY" PLUS_API_HOST = "https://api.frigate.video" diff --git a/frigate/debug_replay.py b/frigate/debug_replay.py new file mode 100644 index 000000000..504184667 --- /dev/null +++ b/frigate/debug_replay.py @@ -0,0 +1,443 @@ +"""Debug replay camera management for replaying recordings with detection overlays.""" + +import logging +import os +import shutil +import subprocess as sp +import threading + +from ruamel.yaml import YAML + +from frigate.config import FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdatePublisher, + CameraConfigUpdateTopic, +) +from frigate.const import ( + CLIPS_DIR, + RECORD_DIR, + REPLAY_CAMERA_PREFIX, + REPLAY_DIR, + THUMB_DIR, +) +from frigate.models import Event, Recordings, ReviewSegment, Timeline +from frigate.util.config import find_config_file + +logger = logging.getLogger(__name__) + + +class DebugReplayManager: + """Manages a single debug replay session.""" + + def __init__(self) -> None: + self._lock = threading.Lock() + self.replay_camera_name: str | None = None + self.source_camera: str | None = None + self.clip_path: str | None = None + self.start_ts: float | None = None + self.end_ts: float | None = None + + @property + def active(self) -> bool: + """Whether a replay session is currently active.""" + return self.replay_camera_name is not None + + def start( + self, + source_camera: str, + start_ts: float, + end_ts: float, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + ) -> str: + """Start a debug replay session. + + Args: + source_camera: Name of the source camera to replay + start_ts: Start timestamp + end_ts: End timestamp + frigate_config: Current Frigate configuration + config_publisher: Publisher for camera config updates + + Returns: + The replay camera name + + Raises: + ValueError: If a session is already active or parameters are invalid + RuntimeError: If clip generation fails + """ + with self._lock: + return self._start_locked( + source_camera, start_ts, end_ts, frigate_config, config_publisher + ) + + def _start_locked( + self, + source_camera: str, + start_ts: float, + end_ts: float, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + ) -> str: + if self.active: + raise ValueError("A replay session is already active") + + if source_camera not in frigate_config.cameras: + raise ValueError(f"Camera '{source_camera}' not found") + + if end_ts <= start_ts: + raise ValueError("End time must be after start time") + + # Query recordings for the source camera in the time range + recordings = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + Recordings.end_time, + ) + .where( + Recordings.start_time.between(start_ts, end_ts) + | Recordings.end_time.between(start_ts, end_ts) + | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) + ) + .where(Recordings.camera == source_camera) + .order_by(Recordings.start_time.asc()) + ) + + if not recordings.count(): + raise ValueError( + f"No recordings found for camera '{source_camera}' in the specified time range" + ) + + # Create replay directory + os.makedirs(REPLAY_DIR, exist_ok=True) + + # Generate replay camera name + replay_name = f"{REPLAY_CAMERA_PREFIX}{source_camera}" + + # Build concat file for ffmpeg + concat_file = os.path.join(REPLAY_DIR, f"{replay_name}_concat.txt") + clip_path = os.path.join(REPLAY_DIR, f"{replay_name}.mp4") + + with open(concat_file, "w") as f: + for recording in recordings: + f.write(f"file '{recording.path}'\n") + + # Concatenate recordings into a single clip with -c copy (fast) + ffmpeg_cmd = [ + frigate_config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-y", + "-f", + "concat", + "-safe", + "0", + "-i", + concat_file, + "-c", + "copy", + "-movflags", + "+faststart", + clip_path, + ] + + logger.info( + "Generating replay clip for %s (%.1f - %.1f)", + source_camera, + start_ts, + end_ts, + ) + + try: + result = sp.run( + ffmpeg_cmd, + capture_output=True, + text=True, + timeout=120, + ) + if result.returncode != 0: + logger.error("FFmpeg error: %s", result.stderr) + raise RuntimeError( + f"Failed to generate replay clip: {result.stderr[-500:]}" + ) + except sp.TimeoutExpired: + raise RuntimeError("Clip generation timed out") + finally: + # Clean up concat file + if os.path.exists(concat_file): + os.remove(concat_file) + + if not os.path.exists(clip_path): + raise RuntimeError("Clip file was not created") + + # Build camera config dict for the replay camera + source_config = frigate_config.cameras[source_camera] + camera_dict = self._build_camera_config_dict( + source_config, replay_name, clip_path + ) + + # Build an in-memory config with the replay camera added + config_file = find_config_file() + yaml_parser = YAML() + with open(config_file, "r") as f: + config_data = yaml_parser.load(f) + + if "cameras" not in config_data or config_data["cameras"] is None: + config_data["cameras"] = {} + config_data["cameras"][replay_name] = camera_dict + + try: + new_config = FrigateConfig.parse_object(config_data) + except Exception as e: + raise RuntimeError(f"Failed to validate replay camera config: {e}") + + # Update the running config + frigate_config.cameras[replay_name] = new_config.cameras[replay_name] + + # Publish the add event + config_publisher.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.add, replay_name), + new_config.cameras[replay_name], + ) + + # Store session state + self.replay_camera_name = replay_name + self.source_camera = source_camera + self.clip_path = clip_path + self.start_ts = start_ts + self.end_ts = end_ts + + logger.info("Debug replay started: %s -> %s", source_camera, replay_name) + return replay_name + + def stop( + self, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + ) -> None: + """Stop the active replay session and clean up all artifacts. + + Args: + frigate_config: Current Frigate configuration + config_publisher: Publisher for camera config updates + """ + with self._lock: + self._stop_locked(frigate_config, config_publisher) + + def _stop_locked( + self, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + ) -> None: + if not self.active: + logger.warning("No active replay session to stop") + return + + replay_name = self.replay_camera_name + + # Publish remove event so subscribers stop and remove from their config + if replay_name in frigate_config.cameras: + config_publisher.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, replay_name), + frigate_config.cameras[replay_name], + ) + # Do NOT pop here — let subscribers handle removal from the shared + # config dict when they process the ZMQ message to avoid race conditions + + # Defensive DB cleanup + self._cleanup_db(replay_name) + + # Remove filesystem artifacts + self._cleanup_files(replay_name) + + # Reset state + self.replay_camera_name = None + self.source_camera = None + self.clip_path = None + self.start_ts = None + self.end_ts = None + + logger.info("Debug replay stopped and cleaned up: %s", replay_name) + + def _build_camera_config_dict( + self, + source_config, + replay_name: str, + clip_path: str, + ) -> dict: + """Build a camera config dictionary for the replay camera. + + Args: + source_config: Source camera's CameraConfig + replay_name: Name for the replay camera + clip_path: Path to the replay clip file + + Returns: + Camera config as a dictionary + """ + # Extract detect config (exclude computed fields) + detect_dict = source_config.detect.model_dump( + exclude={"min_initialized", "max_disappeared", "enabled_in_config"} + ) + + # Extract objects config, using .dict() on filters to convert + # RuntimeFilterConfig ndarray masks back to string coordinates + objects_dict = { + "track": source_config.objects.track, + "mask": { + mask_id: ( + mask_cfg.model_dump( + exclude={"raw_coordinates", "enabled_in_config"} + ) + if mask_cfg is not None + else None + ) + for mask_id, mask_cfg in source_config.objects.mask.items() + } + if source_config.objects.mask + else {}, + "filters": { + name: filt.dict() if hasattr(filt, "dict") else filt.model_dump() + for name, filt in source_config.objects.filters.items() + }, + } + + # Extract zones (exclude_defaults avoids serializing empty defaults + # like distances=[] that fail validation on re-parse) + zones_dict = {} + for zone_name, zone_config in source_config.zones.items(): + zone_dump = zone_config.model_dump( + exclude={"contour", "color"}, exclude_defaults=True + ) + # Always include required fields + zone_dump.setdefault("coordinates", zone_config.coordinates) + zones_dict[zone_name] = zone_dump + + # Extract motion config (exclude runtime fields) + motion_dict = {} + if source_config.motion is not None: + motion_dict = source_config.motion.model_dump( + exclude={ + "frame_shape", + "raw_mask", + "mask", + "improved_contrast_enabled", + "rasterized_mask", + } + ) + + return { + "enabled": True, + "ffmpeg": { + "inputs": [ + { + "path": clip_path, + "roles": ["detect"], + "input_args": "-re -stream_loop -1 -fflags +genpts", + } + ], + "hwaccel_args": [], + }, + "detect": detect_dict, + "objects": objects_dict, + "zones": zones_dict, + "motion": motion_dict, + "record": {"enabled": False}, + "snapshots": {"enabled": False}, + "review": { + "alerts": {"enabled": False}, + "detections": {"enabled": False}, + }, + "birdseye": {"enabled": False}, + "audio": {"enabled": False}, + "lpr": {"enabled": False}, + "face_recognition": {"enabled": False}, + } + + def _cleanup_db(self, camera_name: str) -> None: + """Defensively remove any database rows for the replay camera.""" + try: + Event.delete().where(Event.camera == camera_name).execute() + except Exception as e: + logger.error("Failed to delete replay events: %s", e) + + try: + Timeline.delete().where(Timeline.camera == camera_name).execute() + except Exception as e: + logger.error("Failed to delete replay timeline: %s", e) + + try: + Recordings.delete().where(Recordings.camera == camera_name).execute() + except Exception as e: + logger.error("Failed to delete replay recordings: %s", e) + + try: + ReviewSegment.delete().where(ReviewSegment.camera == camera_name).execute() + except Exception as e: + logger.error("Failed to delete replay review segments: %s", e) + + def _cleanup_files(self, camera_name: str) -> None: + """Remove filesystem artifacts for the replay camera.""" + dirs_to_clean = [ + os.path.join(RECORD_DIR, camera_name), + os.path.join(CLIPS_DIR, camera_name), + os.path.join(THUMB_DIR, camera_name), + ] + + for dir_path in dirs_to_clean: + if os.path.exists(dir_path): + try: + shutil.rmtree(dir_path) + logger.debug("Removed replay directory: %s", dir_path) + except Exception as e: + logger.error("Failed to remove %s: %s", dir_path, e) + + # Remove replay clip and any related files + if os.path.exists(REPLAY_DIR): + try: + shutil.rmtree(REPLAY_DIR) + logger.debug("Removed replay cache directory") + except Exception as e: + logger.error("Failed to remove replay cache: %s", e) + + +def cleanup_replay_cameras() -> None: + """Remove any stale replay camera artifacts on startup. + + Since replay cameras are memory-only and never written to YAML, they + won't appear in the config after a restart. This function cleans up + filesystem and database artifacts from any replay that was running when + the process stopped. + + Must be called AFTER the database is bound. + """ + stale_cameras: set[str] = set() + + # Scan filesystem for leftover replay artifacts to derive camera names + for dir_path in [RECORD_DIR, CLIPS_DIR, THUMB_DIR]: + if os.path.isdir(dir_path): + for entry in os.listdir(dir_path): + if entry.startswith(REPLAY_CAMERA_PREFIX): + stale_cameras.add(entry) + + if os.path.isdir(REPLAY_DIR): + for entry in os.listdir(REPLAY_DIR): + if entry.startswith(REPLAY_CAMERA_PREFIX) and entry.endswith(".mp4"): + stale_cameras.add(entry.removesuffix(".mp4")) + + if not stale_cameras: + return + + logger.info("Cleaning up stale replay camera artifacts: %s", list(stale_cameras)) + + manager = DebugReplayManager() + for camera_name in stale_cameras: + manager._cleanup_db(camera_name) + manager._cleanup_files(camera_name) + + if os.path.exists(REPLAY_DIR): + try: + shutil.rmtree(REPLAY_DIR) + except Exception as e: + logger.error("Failed to remove replay cache directory: %s", e) diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 54831942a..8e45af498 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -421,7 +421,9 @@ class EmbeddingMaintainer(threading.Thread): if self.config.semantic_search.enabled: self.embeddings.update_stats() - camera_config = self.config.cameras[camera] + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return # no need to process updated objects if no processors are active if len(self.realtime_processors) == 0 and len(self.post_processors) == 0: @@ -639,7 +641,10 @@ class EmbeddingMaintainer(threading.Thread): if not camera or camera not in self.config.cameras: return - camera_config = self.config.cameras[camera] + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return + dedicated_lpr_enabled = ( camera_config.type == CameraTypeEnum.lpr and "license_plate" not in camera_config.objects.track diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index f6ab777c1..77f6eee5f 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -7,6 +7,7 @@ from typing import Dict from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber from frigate.config import FrigateConfig from frigate.config.classification import ObjectClassificationType +from frigate.const import REPLAY_CAMERA_PREFIX from frigate.events.types import EventStateEnum, EventTypeEnum from frigate.models import Event from frigate.util.builtin import to_relative_box @@ -146,7 +147,9 @@ class EventProcessor(threading.Thread): if should_update_db(self.events_in_process[event_data["id"]], event_data): updated_db = True - camera_config = self.config.cameras[camera] + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return width = camera_config.detect.width height = camera_config.detect.height first_detector = list(self.config.detectors.values())[0] @@ -283,6 +286,10 @@ class EventProcessor(threading.Thread): def handle_external_detection( self, event_type: EventStateEnum, event_data: Event ) -> None: + # Skip replay cameras + if event_data.get("camera", "").startswith(REPLAY_CAMERA_PREFIX): + return + if event_type == EventStateEnum.start: event = { Event.id: event_data["id"], diff --git a/frigate/genai/ollama.py b/frigate/genai/ollama.py index 73ba2171e..e98f6ab07 100644 --- a/frigate/genai/ollama.py +++ b/frigate/genai/ollama.py @@ -1,5 +1,6 @@ """Ollama Provider for Frigate AI.""" +import json import logging from typing import Any, Optional @@ -108,7 +109,22 @@ class OllamaClient(GenAIClient): if msg.get("name"): msg_dict["name"] = msg["name"] if msg.get("tool_calls"): - msg_dict["tool_calls"] = msg["tool_calls"] + # Ollama requires tool call arguments as dicts, but the + # conversation format (OpenAI-style) stores them as JSON + # strings. Convert back to dicts for Ollama. + ollama_tool_calls = [] + for tc in msg["tool_calls"]: + func = tc.get("function") or {} + args = func.get("arguments") or {} + if isinstance(args, str): + try: + args = json.loads(args) + except (json.JSONDecodeError, TypeError): + args = {} + ollama_tool_calls.append( + {"function": {"name": func.get("name", ""), "arguments": args}} + ) + msg_dict["tool_calls"] = ollama_tool_calls request_messages.append(msg_dict) request_params: dict[str, Any] = { @@ -120,25 +136,27 @@ class OllamaClient(GenAIClient): request_params["stream"] = True if tools: request_params["tools"] = tools - if tool_choice: - request_params["tool_choice"] = ( - "none" - if tool_choice == "none" - else "required" - if tool_choice == "required" - else "auto" - ) return request_params def _message_from_response(self, response: dict[str, Any]) -> dict[str, Any]: """Parse Ollama chat response into {content, tool_calls, finish_reason}.""" if not response or "message" not in response: + logger.debug("Ollama response empty or missing 'message' key") return { "content": None, "tool_calls": None, "finish_reason": "error", } message = response["message"] + logger.debug( + "Ollama response message keys: %s, content_len=%s, thinking_len=%s, " + "tool_calls=%s, done=%s", + list(message.keys()) if hasattr(message, "keys") else "N/A", + len(message.get("content", "") or "") if message.get("content") else 0, + len(message.get("thinking", "") or "") if message.get("thinking") else 0, + bool(message.get("tool_calls")), + response.get("done"), + ) content = message.get("content", "").strip() if message.get("content") else None tool_calls = parse_tool_calls_from_message(message) finish_reason = "error" @@ -198,7 +216,13 @@ class OllamaClient(GenAIClient): tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", ): - """Stream chat with tools; yields content deltas then final message.""" + """Stream chat with tools; yields content deltas then final message. + + When tools are provided, Ollama streaming does not include tool_calls + in the response chunks. To work around this, we use a non-streaming + call when tools are present to ensure tool calls are captured, then + emit the content as a single delta followed by the final message. + """ if self.provider is None: logger.warning( "Ollama provider has not been initialized. Check your Ollama configuration." @@ -213,6 +237,27 @@ class OllamaClient(GenAIClient): ) return try: + # Ollama does not return tool_calls in streaming mode, so fall + # back to a non-streaming call when tools are provided. + if tools: + logger.debug( + "Ollama: tools provided, using non-streaming call for tool support" + ) + request_params = self._build_request_params( + messages, tools, tool_choice, stream=False + ) + async_client = OllamaAsyncClient( + host=self.genai_config.base_url, + timeout=self.timeout, + ) + response = await async_client.chat(**request_params) + result = self._message_from_response(response) + content = result.get("content") + if content: + yield ("content_delta", content) + yield ("message", result) + return + request_params = self._build_request_params( messages, tools, tool_choice, stream=True ) @@ -222,27 +267,23 @@ class OllamaClient(GenAIClient): ) content_parts: list[str] = [] final_message: dict[str, Any] | None = None - try: - stream = await async_client.chat(**request_params) - async for chunk in stream: - if not chunk or "message" not in chunk: - continue - msg = chunk.get("message", {}) - delta = msg.get("content") or "" - if delta: - content_parts.append(delta) - yield ("content_delta", delta) - if chunk.get("done"): - full_content = "".join(content_parts).strip() or None - tool_calls = parse_tool_calls_from_message(msg) - final_message = { - "content": full_content, - "tool_calls": tool_calls, - "finish_reason": "tool_calls" if tool_calls else "stop", - } - break - finally: - await async_client.close() + stream = await async_client.chat(**request_params) + async for chunk in stream: + if not chunk or "message" not in chunk: + continue + msg = chunk.get("message", {}) + delta = msg.get("content") or "" + if delta: + content_parts.append(delta) + yield ("content_delta", delta) + if chunk.get("done"): + full_content = "".join(content_parts).strip() or None + final_message = { + "content": full_content, + "tool_calls": None, + "finish_reason": "stop", + } + break if final_message is not None: yield ("message", final_message) diff --git a/frigate/genai/utils.py b/frigate/genai/utils.py index 93d4552b9..44f982059 100644 --- a/frigate/genai/utils.py +++ b/frigate/genai/utils.py @@ -23,21 +23,26 @@ def parse_tool_calls_from_message( if not raw or not isinstance(raw, list): return None result = [] - for tool_call in raw: + for idx, tool_call in enumerate(raw): function_data = tool_call.get("function") or {} - try: - arguments_str = function_data.get("arguments") or "{}" - arguments = json.loads(arguments_str) - except (json.JSONDecodeError, KeyError, TypeError) as e: - logger.warning( - "Failed to parse tool call arguments: %s, tool: %s", - e, - function_data.get("name", "unknown"), - ) + raw_arguments = function_data.get("arguments") or {} + if isinstance(raw_arguments, dict): + arguments = raw_arguments + elif isinstance(raw_arguments, str): + try: + arguments = json.loads(raw_arguments) + except (json.JSONDecodeError, KeyError, TypeError) as e: + logger.warning( + "Failed to parse tool call arguments: %s, tool: %s", + e, + function_data.get("name", "unknown"), + ) + arguments = {} + else: arguments = {} result.append( { - "id": tool_call.get("id", ""), + "id": tool_call.get("id", "") or f"call_{idx}", "name": function_data.get("name", ""), "arguments": arguments, } diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index eb23c2573..d3717d281 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -420,7 +420,8 @@ class BirdsEyeFrameManager: [ cam for cam, cam_data in self.cameras.items() - if self.config.cameras[cam].birdseye.enabled + if cam in self.config.cameras + and self.config.cameras[cam].birdseye.enabled and self.config.cameras[cam].enabled_in_config and self.config.cameras[cam].enabled and cam_data["last_active_frame"] > 0 @@ -723,8 +724,11 @@ class BirdsEyeFrameManager: Update birdseye for a specific camera with new frame data. Returns (frame_changed, layout_changed) to indicate if the frame or layout changed. """ - # don't process if birdseye is disabled for this camera - camera_config = self.config.cameras[camera] + # don't process if camera was removed or birdseye is disabled + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return False, False + force_update = False # disabling birdseye is a little tricky diff --git a/frigate/output/output.py b/frigate/output/output.py index a44415000..38b1ddc52 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -22,7 +22,12 @@ from frigate.config.camera.updater import ( CameraConfigUpdateEnum, CameraConfigUpdateSubscriber, ) -from frigate.const import CACHE_DIR, CLIPS_DIR, PROCESS_PRIORITY_MED +from frigate.const import ( + CACHE_DIR, + CLIPS_DIR, + PROCESS_PRIORITY_MED, + REPLAY_CAMERA_PREFIX, +) from frigate.output.birdseye import Birdseye from frigate.output.camera import JsmpegCamera from frigate.output.preview import PreviewRecorder @@ -79,6 +84,32 @@ class OutputProcess(FrigateProcess): ) self.config = config + def is_debug_replay_camera(self, camera: str) -> bool: + return camera.startswith(REPLAY_CAMERA_PREFIX) + + def add_camera( + self, + camera: str, + websocket_server: WSGIServer, + jsmpeg_cameras: dict[str, JsmpegCamera], + preview_recorders: dict[str, PreviewRecorder], + preview_write_times: dict[str, float], + birdseye: Birdseye | None, + ) -> None: + camera_config = self.config.cameras[camera] + jsmpeg_cameras[camera] = JsmpegCamera( + camera_config, self.stop_event, websocket_server + ) + preview_recorders[camera] = PreviewRecorder(camera_config) + preview_write_times[camera] = 0 + + if ( + birdseye is not None + and self.config.birdseye.enabled + and camera_config.birdseye.enabled + ): + birdseye.add_camera(camera) + def run(self) -> None: self.pre_run_setup(self.config.logger) @@ -118,14 +149,17 @@ class OutputProcess(FrigateProcess): move_preview_frames("cache") for camera, cam_config in self.config.cameras.items(): - if not cam_config.enabled_in_config: + if not cam_config.enabled_in_config or self.is_debug_replay_camera(camera): continue - jsmpeg_cameras[camera] = JsmpegCamera( - cam_config, self.stop_event, websocket_server + self.add_camera( + camera, + websocket_server, + jsmpeg_cameras, + preview_recorders, + preview_write_times, + birdseye, ) - preview_recorders[camera] = PreviewRecorder(cam_config) - preview_write_times[camera] = 0 if self.config.birdseye.enabled: birdseye = Birdseye(self.config, self.stop_event, websocket_server) @@ -138,19 +172,15 @@ class OutputProcess(FrigateProcess): if CameraConfigUpdateEnum.add in updates: for camera in updates["add"]: - jsmpeg_cameras[camera] = JsmpegCamera( - self.config.cameras[camera], self.stop_event, websocket_server - ) - preview_recorders[camera] = PreviewRecorder( - self.config.cameras[camera] - ) - preview_write_times[camera] = 0 - - if ( - self.config.birdseye.enabled - and self.config.cameras[camera].birdseye.enabled - ): - birdseye.add_camera(camera) + if not self.is_debug_replay_camera(camera): + self.add_camera( + camera, + websocket_server, + jsmpeg_cameras, + preview_recorders, + preview_write_times, + birdseye, + ) (topic, data) = detection_subscriber.check_for_update(timeout=1) now = datetime.datetime.now().timestamp() @@ -174,7 +204,11 @@ class OutputProcess(FrigateProcess): _, ) = data - if not self.config.cameras[camera].enabled: + if ( + camera not in self.config.cameras + or not self.config.cameras[camera].enabled + or self.is_debug_replay_camera(camera) + ): continue frame = frame_manager.get( diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index c850b9af9..68040476a 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -289,11 +289,12 @@ class RecordingMaintainer(threading.Thread): ) # publish most recently available recording time and None if disabled + camera_cfg = self.config.cameras.get(camera) self.recordings_publisher.publish( ( camera, recordings[0]["start_time"].timestamp() - if self.config.cameras[camera].record.enabled + if camera_cfg and camera_cfg.record.enabled else None, None, ), @@ -317,9 +318,8 @@ class RecordingMaintainer(threading.Thread): ) -> Optional[Recordings]: cache_path: str = recording["cache_path"] start_time: datetime.datetime = recording["start_time"] - record_config = self.config.cameras[camera].record - # Just delete files if recordings are turned off + # Just delete files if camera removed or recordings are turned off if ( camera not in self.config.cameras or not self.config.cameras[camera].record.enabled diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index 6afdc8de9..a51c73f88 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -652,6 +652,9 @@ class ReviewSegmentMaintainer(threading.Thread): if camera not in self.indefinite_events: self.indefinite_events[camera] = {} + if camera not in self.config.cameras: + continue + if ( not self.config.cameras[camera].enabled or not self.config.cameras[camera].record.enabled diff --git a/frigate/stats/util.py b/frigate/stats/util.py index f4f91f83f..40337268e 100644 --- a/frigate/stats/util.py +++ b/frigate/stats/util.py @@ -340,6 +340,9 @@ def stats_snapshot( stats["cameras"] = {} for name, camera_stats in camera_metrics.items(): + if name not in config.cameras: + continue + total_camera_fps += camera_stats.camera_fps.value total_process_fps += camera_stats.process_fps.value total_skipped_fps += camera_stats.skipped_fps.value diff --git a/frigate/storage.py b/frigate/storage.py index feabe06ff..93463c542 100644 --- a/frigate/storage.py +++ b/frigate/storage.py @@ -8,7 +8,7 @@ from pathlib import Path from peewee import SQL, fn from frigate.config import FrigateConfig -from frigate.const import RECORD_DIR +from frigate.const import RECORD_DIR, REPLAY_CAMERA_PREFIX from frigate.models import Event, Recordings from frigate.util.builtin import clear_and_unlink @@ -32,6 +32,10 @@ class StorageMaintainer(threading.Thread): def calculate_camera_bandwidth(self) -> None: """Calculate an average MB/hr for each camera.""" for camera in self.config.cameras.keys(): + # Skip replay cameras + if camera.startswith(REPLAY_CAMERA_PREFIX): + continue + # cameras with < 50 segments should be refreshed to keep size accurate # when few segments are available if self.camera_storage_stats.get(camera, {}).get("needs_refresh", True): @@ -77,6 +81,10 @@ class StorageMaintainer(threading.Thread): usages: dict[str, dict] = {} for camera in self.config.cameras.keys(): + # Skip replay cameras + if camera.startswith(REPLAY_CAMERA_PREFIX): + continue + camera_storage = ( Recordings.select(fn.SUM(Recordings.segment_size)) .where(Recordings.camera == camera, Recordings.segment_size != 0) diff --git a/frigate/test/http_api/base_http_test.py b/frigate/test/http_api/base_http_test.py index 16ded63f8..2ca4aafd0 100644 --- a/frigate/test/http_api/base_http_test.py +++ b/frigate/test/http_api/base_http_test.py @@ -13,6 +13,7 @@ from pydantic import Json from frigate.api.fastapi_app import create_fastapi_app from frigate.config import FrigateConfig from frigate.const import BASE_DIR, CACHE_DIR +from frigate.debug_replay import DebugReplayManager from frigate.models import Event, Recordings, ReviewSegment from frigate.review.types import SeverityEnum from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS @@ -141,6 +142,7 @@ class BaseTestHttp(unittest.TestCase): stats, event_metadata_publisher, None, + DebugReplayManager(), enforce_default_admin=False, ) diff --git a/frigate/test/http_api/test_http_app.py b/frigate/test/http_api/test_http_app.py index b04b1cf55..bf8e9c72a 100644 --- a/frigate/test/http_api/test_http_app.py +++ b/frigate/test/http_api/test_http_app.py @@ -22,3 +22,32 @@ class TestHttpApp(BaseTestHttp): response = client.get("/stats") response_json = response.json() assert response_json == self.test_stats + + def test_config_set_in_memory_replaces_objects_track_list(self): + self.minimal_config["cameras"]["front_door"]["objects"] = { + "track": ["person", "car"], + } + app = super().create_app() + app.config_publisher = Mock() + + with AuthTestClient(app) as client: + response = client.put( + "/config/set", + json={ + "requires_restart": 0, + "skip_save": True, + "update_topic": "config/cameras/front_door/objects", + "config_data": { + "cameras": { + "front_door": { + "objects": { + "track": ["person"], + } + } + } + }, + }, + ) + + assert response.status_code == 200 + assert app.frigate_config.cameras["front_door"].objects.track == ["person"] diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index 98799fcf0..e903c2ac3 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -151,6 +151,22 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) assert "dog" in frigate_config.cameras["back"].objects.track + def test_deep_merge_override_replaces_list_values(self): + base = {"objects": {"track": ["person", "face"]}} + update = {"objects": {"track": ["person"]}} + + merged = deep_merge(base, update, override=True) + + assert merged["objects"]["track"] == ["person"] + + def test_deep_merge_merge_lists_still_appends(self): + base = {"track": ["person"]} + update = {"track": ["face"]} + + merged = deep_merge(base, update, override=True, merge_lists=True) + + assert merged["track"] == ["person", "face"] + def test_override_birdseye(self): config = { "mqtt": {"host": "mqtt"}, diff --git a/frigate/timeline.py b/frigate/timeline.py index cf2f5e8c7..3ec866176 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -86,7 +86,9 @@ class TimelineProcessor(threading.Thread): event_data: dict[Any, Any], ) -> bool: """Handle object detection.""" - camera_config = self.config.cameras[camera] + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return False event_id = event_data["id"] # Base timeline entry data that all entries will share diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index 9ac04b42a..a699fab23 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -690,9 +690,13 @@ class TrackedObjectProcessor(threading.Thread): self.create_camera_state(camera) elif "remove" in updated_topics: for camera in updated_topics["remove"]: - camera_state = self.camera_states[camera] - camera_state.shutdown() + removed_camera_state = self.camera_states[camera] + removed_camera_state.shutdown() self.camera_states.pop(camera) + self.camera_activity.pop(camera, None) + self.last_motion_detected.pop(camera, None) + + self.requestor.send_data(UPDATE_CAMERA_ACTIVITY, self.camera_activity) # manage camera disabled state for camera, config in self.config.cameras.items(): @@ -700,6 +704,10 @@ class TrackedObjectProcessor(threading.Thread): continue current_enabled = config.enabled + camera_state = self.camera_states.get(camera) + if camera_state is None: + continue + camera_state = self.camera_states[camera] if camera_state.prev_enabled and not current_enabled: @@ -752,7 +760,11 @@ class TrackedObjectProcessor(threading.Thread): except queue.Empty: continue - if not self.config.cameras[camera].enabled: + camera_config = self.config.cameras.get(camera) + if camera_config is None: + continue + + if not camera_config.enabled: logger.debug(f"Camera {camera} disabled, skipping update") continue diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py index c4398dec6..4eb600fb8 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -16,7 +16,7 @@ from frigate.config import ( SnapshotsConfig, UIConfig, ) -from frigate.const import CLIPS_DIR, THUMB_DIR +from frigate.const import CLIPS_DIR, REPLAY_CAMERA_PREFIX, THUMB_DIR from frigate.detectors.detector_config import ModelConfig from frigate.review.types import SeverityEnum from frigate.util.builtin import sanitize_float @@ -621,6 +621,9 @@ class TrackedObject: if not self.camera_config.name: return + if self.camera_config.name.startswith(REPLAY_CAMERA_PREFIX): + return + directory = os.path.join(THUMB_DIR, self.camera_config.name) if not os.path.exists(directory): diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py index bcdc2feda..aa2417a5c 100644 --- a/frigate/util/builtin.py +++ b/frigate/util/builtin.py @@ -84,7 +84,8 @@ def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dic """ :param dct1: First dict to merge :param dct2: Second dict to merge - :param override: if same key exists in both dictionaries, should override? otherwise ignore. (default=True) + :param override: if same key exists in both dictionaries, should override? otherwise ignore. + :param merge_lists: if True, lists will be merged. :return: The merge dictionary """ merged = copy.deepcopy(dct1) @@ -96,6 +97,8 @@ def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dic elif isinstance(v1, list) and isinstance(v2, list): if merge_lists: merged[k] = v1 + v2 + elif override: + merged[k] = copy.deepcopy(v2) else: if override: merged[k] = copy.deepcopy(v2) diff --git a/frigate/util/config.py b/frigate/util/config.py index c689d16e4..238671563 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -9,6 +9,7 @@ from typing import Any, Optional, Union from ruamel.yaml import YAML from frigate.const import CONFIG_DIR, EXPORT_DIR +from frigate.util.builtin import deep_merge from frigate.util.services import get_video_properties logger = logging.getLogger(__name__) @@ -688,3 +689,78 @@ class StreamInfoRetriever: info = asyncio.run(get_video_properties(ffmpeg, path)) self.stream_cache[path] = info return info + + +def apply_section_update(camera_config, section: str, update: dict) -> Optional[str]: + """Merge an update dict into a camera config section and rebuild runtime variants. + + For motion and object filter sections, the plain Pydantic models are rebuilt + as RuntimeMotionConfig / RuntimeFilterConfig so that rasterized numpy masks + are recomputed. This mirrors the logic in FrigateConfig.post_validation. + + Args: + camera_config: The CameraConfig instance to update. + section: Config section name (e.g. "motion", "objects"). + update: Nested dict of field updates to merge. + + Returns: + None on success, or an error message string on failure. + """ + from frigate.config.config import RuntimeFilterConfig, RuntimeMotionConfig + + current = getattr(camera_config, section, None) + if current is None: + return f"Section '{section}' not found on camera '{camera_config.name}'" + + try: + frame_shape = camera_config.frame_shape + + if section == "motion": + merged = deep_merge( + current.model_dump(exclude_unset=True, exclude={"rasterized_mask"}), + update, + override=True, + ) + camera_config.motion = RuntimeMotionConfig( + frame_shape=frame_shape, **merged + ) + + elif section == "objects": + merged = deep_merge( + current.model_dump( + exclude={"filters": {"__all__": {"rasterized_mask"}}} + ), + update, + override=True, + ) + new_objects = current.__class__.model_validate(merged) + + # Preserve private _all_objects from original config + try: + new_objects._all_objects = current._all_objects + except AttributeError: + pass + + # Rebuild RuntimeFilterConfig with merged global + per-object masks + for obj_name, filt in new_objects.filters.items(): + merged_mask = dict(filt.mask) + if new_objects.mask: + for gid, gmask in new_objects.mask.items(): + merged_mask[f"global_{gid}"] = gmask + + new_objects.filters[obj_name] = RuntimeFilterConfig( + frame_shape=frame_shape, + mask=merged_mask, + **filt.model_dump(exclude_unset=True, exclude={"mask", "raw_mask"}), + ) + camera_config.objects = new_objects + + else: + merged = deep_merge(current.model_dump(), update, override=True) + setattr(camera_config, section, current.__class__.model_validate(merged)) + + except Exception: + logger.exception("Config validation error") + return "Validation error. Check logs for details." + + return None diff --git a/frigate/util/rknn_converter.py b/frigate/util/rknn_converter.py index f7ebbf5e6..5660c7601 100644 --- a/frigate/util/rknn_converter.py +++ b/frigate/util/rknn_converter.py @@ -110,6 +110,7 @@ def ensure_torch_dependencies() -> bool: "pip", "install", "--break-system-packages", + "setuptools<81", "torch", "torchvision", ], diff --git a/web/package-lock.json b/web/package-lock.json index 237a1ab13..a3135a345 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "web-new", "version": "0.0.0", + "hasInstallScript": true, "dependencies": { "@cycjimmy/jsmpeg-player": "^6.1.2", "@hookform/resolvers": "^3.9.0", @@ -27,7 +28,7 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "1.2.4", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toggle": "^1.1.2", @@ -45,8 +46,7 @@ "copy-to-clipboard": "^3.3.3", "date-fns": "^3.6.0", "date-fns-tz": "^3.2.0", - "embla-carousel-react": "^8.2.0", - "framer-motion": "^11.5.4", + "framer-motion": "^12.35.0", "hls.js": "^1.5.20", "i18next": "^24.2.0", "i18next-http-backend": "^3.0.1", @@ -56,30 +56,28 @@ "lodash": "^4.17.23", "lucide-react": "^0.477.0", "monaco-yaml": "^5.3.1", - "next-themes": "^0.3.0", + "next-themes": "^0.4.6", "nosleep.js": "^0.12.0", - "react": "^18.3.1", + "react": "^19.2.4", "react-apexcharts": "^1.4.1", "react-day-picker": "^9.7.0", "react-device-detect": "^2.2.3", - "react-dom": "^18.3.1", + "react-dom": "^19.2.4", "react-dropzone": "^14.3.8", - "react-grid-layout": "^1.5.0", + "react-grid-layout": "^2.2.2", "react-hook-form": "^7.52.1", "react-i18next": "^15.2.0", "react-icons": "^5.5.0", - "react-konva": "^18.2.10", + "react-konva": "^19.2.3", "react-markdown": "^9.0.1", "react-router-dom": "^6.30.3", "react-swipeable": "^7.0.2", "react-tracked": "^2.0.1", - "react-transition-group": "^4.4.5", "react-use-websocket": "^4.8.1", - "react-zoom-pan-pinch": "3.4.4", - "recoil": "^0.7.7", + "react-zoom-pan-pinch": "^3.7.0", "remark-gfm": "^4.0.0", "scroll-into-view-if-needed": "^3.1.0", - "sonner": "^1.5.0", + "sonner": "^2.0.7", "sort-by": "^1.2.0", "strftime": "^0.10.3", "swr": "^2.3.2", @@ -87,7 +85,7 @@ "tailwind-scrollbar": "^3.1.0", "tailwindcss-animate": "^1.0.7", "use-long-press": "^3.2.0", - "vaul": "^0.9.1", + "vaul": "^1.1.2", "vite-plugin-monaco-editor": "^1.1.0", "zod": "^3.23.8" }, @@ -96,11 +94,8 @@ "@testing-library/jest-dom": "^6.6.2", "@types/lodash": "^4.17.12", "@types/node": "^20.14.10", - "@types/react": "^18.3.2", - "@types/react-dom": "^18.3.0", - "@types/react-grid-layout": "^1.3.5", - "@types/react-icons": "^3.0.0", - "@types/react-transition-group": "^4.4.10", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/strftime": "^0.9.8", "@typescript-eslint/eslint-plugin": "^7.5.0", "@typescript-eslint/parser": "^7.5.0", @@ -111,18 +106,20 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.2.0", "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.8", "eslint-plugin-vitest-globals": "^1.5.0", "fake-indexeddb": "^6.0.0", "jest-websocket-mock": "^2.5.0", "jsdom": "^24.1.1", + "monaco-editor": "^0.52.0", "msw": "^2.3.5", + "patch-package": "^8.0.1", "postcss": "^8.4.47", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.5", "tailwindcss": "^3.4.9", - "typescript": "^5.8.2", + "typescript": "^5.9.3", "vite": "^6.4.1", "vitest": "^3.0.7" } @@ -766,31 +763,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", @@ -798,9 +795,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@hookform/resolvers": { @@ -1294,13 +1291,89 @@ } } }, - "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -1313,12 +1386,35 @@ } }, "node_modules/@radix-ui/react-arrow": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", - "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.2" + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -1359,19 +1455,19 @@ } }, "node_modules/@radix-ui/react-checkbox": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz", - "integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1388,6 +1484,141 @@ } } }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", @@ -1424,21 +1655,6 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", @@ -1579,28 +1795,10 @@ } } }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1697,21 +1895,6 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", @@ -1727,33 +1910,6 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", @@ -1769,31 +1925,6 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -1812,30 +1943,6 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", @@ -1883,21 +1990,6 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", @@ -1917,24 +2009,6 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", @@ -1966,16 +2040,16 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", - "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1992,6 +2066,50 @@ } } }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dropdown-menu": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", @@ -2037,14 +2155,14 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", - "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2061,6 +2179,44 @@ } } }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-hover-card": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz", @@ -2092,6 +2248,75 @@ } } }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-icons": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", @@ -2120,12 +2345,35 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", - "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.2" + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", @@ -2182,95 +2430,17 @@ } } }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", - "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", - "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" + "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -2287,7 +2457,32 @@ } } }, - "node_modules/@radix-ui/react-portal": { + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", @@ -2311,6 +2506,376 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-presence": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", @@ -2358,24 +2923,6 @@ } } }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-progress": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", @@ -2400,21 +2947,6 @@ } } }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", @@ -2453,40 +2985,22 @@ } } }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-radio-group": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz", - "integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2503,6 +3017,246 @@ } } }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", @@ -2566,30 +3320,30 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", - "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.0", - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.2", + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, @@ -2608,31 +3362,28 @@ } } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" }, - "node_modules/@radix-ui/react-separator": { + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -2649,7 +3400,70 @@ } } }, - "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", @@ -2672,23 +3486,77 @@ } } }, - "node_modules/@radix-ui/react-slider": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.3.tgz", - "integrity": "sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==", + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.0", - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", @@ -2705,13 +3573,161 @@ } } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2723,10 +3739,10 @@ } } }, - "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2738,6 +3754,57 @@ } } }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-switch": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz", @@ -2798,14 +3865,14 @@ } }, "node_modules/@radix-ui/react-toggle": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz", - "integrity": "sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -2851,6 +3918,94 @@ } } }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-toggle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz", + "integrity": "sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", @@ -2891,44 +4046,6 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", @@ -2944,33 +4061,6 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -2989,62 +4079,6 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", @@ -3092,21 +4126,6 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", @@ -3126,24 +4145,6 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", @@ -3159,71 +4160,6 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -3291,12 +4227,12 @@ } }, "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3308,6 +4244,21 @@ } } }, + "node_modules/@radix-ui/react-use-escape-keydown/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", @@ -3339,12 +4290,12 @@ } }, "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.0" + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3375,12 +4326,35 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", - "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.2" + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3398,9 +4372,9 @@ } }, "node_modules/@radix-ui/rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, "node_modules/@react-icons/all-files": { @@ -3477,1120 +4451,6 @@ "react": ">=18" } }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-label": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", - "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-radio-group": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", - "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-separator": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", - "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", - "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rjsf/shadcn/node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@rjsf/shadcn/node_modules/cmdk": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", - "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-primitive": "^2.0.2" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, "node_modules/@rjsf/shadcn/node_modules/lucide-react": { "version": "0.548.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz", @@ -5237,12 +5097,6 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, "node_modules/@types/estree-jsx": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", @@ -5261,6 +5115,12 @@ "@types/unist": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.12", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz", @@ -5294,65 +5154,33 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" - }, "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "peer": true, "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, + "license": "MIT", "peer": true, - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-grid-layout": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz", - "integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-icons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/react-icons/-/react-icons-3.0.0.tgz", - "integrity": "sha512-Vefs6LkLqF61vfV7AiAqls+vpR94q67gunhMueDznG+msAkrYgRxl7gYjNem/kZ+as2l2mNChmF1jRZzzQQtMg==", - "deprecated": "This is a stub types definition. react-icons provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "react-icons": "*" + "peerDependencies": { + "@types/react": "^19.2.0" } }, "node_modules/@types/react-reconciler": { - "version": "0.28.8", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz", - "integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-transition-group": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", - "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", - "dev": true, - "dependencies": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g==", + "license": "MIT", + "peerDependencies": { "@types/react": "*" } }, @@ -5748,6 +5576,13 @@ "@types/json-schema": "^7.0.15" } }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/@yr/monotone-cubic-spline": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", @@ -6170,6 +6005,25 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -6183,6 +6037,23 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -6352,6 +6223,22 @@ "node": ">= 6" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -6414,381 +6301,19 @@ } }, "node_modules/cmdk": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", - "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", "license": "MIT", "dependencies": { - "@radix-ui/react-dialog": "1.0.5", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", - "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", - "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", - "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", - "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", - "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "node_modules/color-convert": { @@ -6919,9 +6444,10 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" }, "node_modules/data-urls": { "version": "5.0.0", @@ -7098,6 +6624,24 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -7196,15 +6740,6 @@ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", "dev": true }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -7233,35 +6768,6 @@ "dev": true, "license": "ISC" }, - "node_modules/embla-carousel": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.2.0.tgz", - "integrity": "sha512-rf2GIX8rab9E6ZZN0Uhz05746qu2KrDje9IfFyHzjwxLwhvGjUt6y9+uaY1Sf+B0OPSa3sgas7BE2hWZCtopTA==", - "license": "MIT", - "peer": true - }, - "node_modules/embla-carousel-react": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.2.0.tgz", - "integrity": "sha512-dWqbmaEBQjeAcy/EKrcAX37beVr0ubXuHPuLZkx27z58V1FIvRbbMb4/c3cLZx0PAv/ofngX2QFrwUB+62SPnw==", - "license": "MIT", - "dependencies": { - "embla-carousel": "8.2.0", - "embla-carousel-reactive-utils": "8.2.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.1 || ^18.0.0" - } - }, - "node_modules/embla-carousel-reactive-utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.2.0.tgz", - "integrity": "sha512-ZdaPNgMydkPBiDRUv+wRIz3hpZJ3LKrTyz+XWi286qlwPyZFJDjbzPBiXnC3czF9N/nsabSc7LTRvGauUzwKEg==", - "license": "MIT", - "peerDependencies": { - "embla-carousel": "8.2.0" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -7545,16 +7051,16 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react-refresh": { @@ -7842,6 +7348,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -7930,17 +7446,19 @@ } }, "node_modules/framer-motion": { - "version": "11.5.4", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.5.4.tgz", - "integrity": "sha512-E+tb3/G6SO69POkdJT+3EpdMuhmtCh9EWuK4I1DnIC23L7tFPrl8vxP+LSovwaw6uUr73rUbpb4FgK011wbRJQ==", + "version": "12.35.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.0.tgz", + "integrity": "sha512-w8hghCMQ4oq10j6aZh3U2yeEQv5K69O/seDI/41PK4HtgkLrcBovUNc0ayBC3UyyU7V1mrY2yLzvYdWJX9pGZQ==", "license": "MIT", "dependencies": { + "motion-dom": "^12.35.0", + "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/is-prop-valid": { @@ -7954,6 +7472,31 @@ } } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8115,6 +7658,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -8130,11 +7680,6 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, - "node_modules/hamt_plus": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", - "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -8144,6 +7689,19 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -8638,6 +8196,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -8699,14 +8264,24 @@ } }, "node_modules/its-fine": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.1.3.tgz", - "integrity": "sha512-mncCA+yb6tuh5zK26cHqKlsSyxm4zdm4YgJpxycyx6p9fgxgK5PLu3iDVpKhzTn57Yrv3jk/r0aK0RFTT1OjFw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", "dependencies": { - "@types/react-reconciler": "^0.28.0" + "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { - "react": ">=18.0" + "react": "^19.0.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" } }, "node_modules/jackspeak": { @@ -8847,6 +8422,26 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -8858,6 +8453,39 @@ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonpointer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", @@ -8877,6 +8505,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/konva": { "version": "9.3.18", "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.18.tgz", @@ -9049,6 +8687,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/markdown-to-jsx": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-8.0.0.tgz", + "integrity": "sha512-hWEaRxeCDjes1CVUQqU+Ov0mCqBqkGhLKjL98KdbwHSgEWZZSJQeGlJQatVfeZ3RaxrfTrZZ3eczl2dhp5c/pA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9989,6 +9644,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -10015,9 +9680,10 @@ } }, "node_modules/monaco-editor": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", - "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==", + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT", "peer": true }, "node_modules/monaco-languageserver-types": { @@ -10090,6 +9756,21 @@ "monaco-editor": ">=0.36" } }, + "node_modules/motion-dom": { + "version": "12.35.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.35.0.tgz", + "integrity": "sha512-FFMLEnIejK/zDABn+vqGVAUN4T0+3fw+cVAY8MMT65yR+j5uMuvWdd4npACWhh94OVWQs79CrBBuwOwGRZAQiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10199,12 +9880,13 @@ "dev": true }, "node_modules/next-themes": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", - "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", "peerDependencies": { - "react": "^16.8 || ^17 || ^18", - "react-dom": "^16.8 || ^17 || ^18" + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "node_modules/node-fetch": { @@ -10328,6 +10010,16 @@ "node": ">= 6" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object-path": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.6.0.tgz", @@ -10488,6 +10180,79 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -10964,13 +10729,11 @@ ] }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } @@ -11031,24 +10794,25 @@ } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", "peer": true, "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.4" } }, "node_modules/react-draggable": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", - "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", + "license": "MIT", "dependencies": { - "clsx": "^1.1.1", + "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { @@ -11056,14 +10820,6 @@ "react-dom": ">= 16.3.0" } }, - "node_modules/react-draggable/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "engines": { - "node": ">=6" - } - }, "node_modules/react-dropzone": { "version": "14.3.8", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", @@ -11082,15 +10838,15 @@ } }, "node_modules/react-grid-layout": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.0.tgz", - "integrity": "sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz", + "integrity": "sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==", "license": "MIT", "dependencies": { - "clsx": "^2.0.0", + "clsx": "^2.1.1", "fast-equals": "^4.0.3", "prop-types": "^15.8.1", - "react-draggable": "^4.4.5", + "react-draggable": "^4.4.6", "react-resizable": "^3.0.5", "resize-observer-polyfill": "^1.5.1" }, @@ -11154,9 +10910,9 @@ "license": "MIT" }, "node_modules/react-konva": { - "version": "18.2.10", - "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.10.tgz", - "integrity": "sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-19.2.3.tgz", + "integrity": "sha512-VsO5CJZwUo12xFa33UEIDOQn6ZZBeE6jlkStGFvpR/3NiDA/9RPQTzw6Ri++C0Pnh3Arco1AehB8qJNv9YCRwg==", "funding": [ { "type": "patreon", @@ -11171,16 +10927,17 @@ "url": "https://github.com/sponsors/lavrton" } ], + "license": "MIT", "dependencies": { - "@types/react-reconciler": "^0.28.2", - "its-fine": "^1.1.1", - "react-reconciler": "~0.29.0", - "scheduler": "^0.23.0" + "@types/react-reconciler": "^0.33.0", + "its-fine": "^2.0.0", + "react-reconciler": "0.33.0", + "scheduler": "0.27.0" }, "peerDependencies": { - "konva": "^8.0.1 || ^7.2.5 || ^9.0.0", - "react": ">=18.0.0", - "react-dom": ">=18.0.0" + "konva": "^8.0.1 || ^7.2.5 || ^9.0.0 || ^10.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" } }, "node_modules/react-markdown": { @@ -11211,18 +10968,18 @@ } }, "node_modules/react-reconciler": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz", - "integrity": "sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.27.0" }, "engines": { "node": ">=0.10.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^19.2.0" } }, "node_modules/react-remove-scroll": { @@ -11273,15 +11030,17 @@ } }, "node_modules/react-resizable": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", - "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz", + "integrity": "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==", + "license": "MIT", "dependencies": { "prop-types": "15.x", - "react-draggable": "^4.0.3" + "react-draggable": "^4.5.0" }, "peerDependencies": { - "react": ">= 16.3" + "react": ">= 16.3", + "react-dom": ">= 16.3" } }, "node_modules/react-router": { @@ -11370,21 +11129,6 @@ "scheduler": ">=0.19.0" } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, "node_modules/react-use-websocket": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.8.1.tgz", @@ -11395,9 +11139,9 @@ } }, "node_modules/react-zoom-pan-pinch": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.4.4.tgz", - "integrity": "sha512-lGTu7D9lQpYEQ6sH+NSlLA7gicgKRW8j+D/4HO1AbSV2POvKRFzdWQ8eI0r3xmOsl4dYQcY+teV6MhULeg1xBw==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz", + "integrity": "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==", "license": "MIT", "engines": { "node": ">=8", @@ -11427,25 +11171,6 @@ "node": ">=8.10.0" } }, - "node_modules/recoil": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", - "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", - "dependencies": { - "hamt_plus": "1.0.2" - }, - "peerDependencies": { - "react": ">=16.13.1" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -11821,13 +11546,11 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true }, "node_modules/scroll-into-view-if-needed": { "version": "3.1.0", @@ -11850,6 +11573,24 @@ "node": ">=10" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11901,13 +11642,13 @@ } }, "node_modules/sonner": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", - "integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", "license": "MIT", "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "node_modules/sort-by": { @@ -12491,6 +12232,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12540,6 +12282,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -12654,9 +12406,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", "peer": true, @@ -12943,15 +12695,16 @@ } }, "node_modules/vaul": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.1.tgz", - "integrity": "sha512-fAhd7i4RNMinx+WEm6pF3nOl78DFkAazcN04ElLPFF9BMCNGbY/kou8UMhIcicm0rJCNePJP0Yyza60gGOD0Jw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", "dependencies": { - "@radix-ui/react-dialog": "^1.0.4" + "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "node_modules/vfile": { @@ -13143,6 +12896,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/web/package.json b/web/package.json index cee1050fe..acbbd8d88 100644 --- a/web/package.json +++ b/web/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite --host", + "postinstall": "patch-package", "build": "tsc && vite build --base=/BASE_PATH/", "lint": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore .", "lint:fix": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore --fix .", @@ -33,7 +34,7 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "1.2.4", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toggle": "^1.1.2", @@ -51,8 +52,7 @@ "copy-to-clipboard": "^3.3.3", "date-fns": "^3.6.0", "date-fns-tz": "^3.2.0", - "embla-carousel-react": "^8.2.0", - "framer-motion": "^11.5.4", + "framer-motion": "^12.35.0", "hls.js": "^1.5.20", "i18next": "^24.2.0", "i18next-http-backend": "^3.0.1", @@ -62,30 +62,28 @@ "lodash": "^4.17.23", "lucide-react": "^0.477.0", "monaco-yaml": "^5.3.1", - "next-themes": "^0.3.0", + "next-themes": "^0.4.6", "nosleep.js": "^0.12.0", - "react": "^18.3.1", + "react": "^19.2.4", "react-apexcharts": "^1.4.1", "react-day-picker": "^9.7.0", "react-device-detect": "^2.2.3", - "react-dom": "^18.3.1", + "react-dom": "^19.2.4", "react-dropzone": "^14.3.8", - "react-grid-layout": "^1.5.0", + "react-grid-layout": "^2.2.2", "react-hook-form": "^7.52.1", "react-i18next": "^15.2.0", "react-icons": "^5.5.0", - "react-konva": "^18.2.10", - "react-router-dom": "^6.30.3", + "react-konva": "^19.2.3", "react-markdown": "^9.0.1", - "remark-gfm": "^4.0.0", + "react-router-dom": "^6.30.3", "react-swipeable": "^7.0.2", "react-tracked": "^2.0.1", - "react-transition-group": "^4.4.5", "react-use-websocket": "^4.8.1", - "react-zoom-pan-pinch": "3.4.4", - "recoil": "^0.7.7", + "react-zoom-pan-pinch": "^3.7.0", + "remark-gfm": "^4.0.0", "scroll-into-view-if-needed": "^3.1.0", - "sonner": "^1.5.0", + "sonner": "^2.0.7", "sort-by": "^1.2.0", "strftime": "^0.10.3", "swr": "^2.3.2", @@ -93,7 +91,7 @@ "tailwind-scrollbar": "^3.1.0", "tailwindcss-animate": "^1.0.7", "use-long-press": "^3.2.0", - "vaul": "^0.9.1", + "vaul": "^1.1.2", "vite-plugin-monaco-editor": "^1.1.0", "zod": "^3.23.8" }, @@ -102,11 +100,8 @@ "@testing-library/jest-dom": "^6.6.2", "@types/lodash": "^4.17.12", "@types/node": "^20.14.10", - "@types/react": "^18.3.2", - "@types/react-dom": "^18.3.0", - "@types/react-grid-layout": "^1.3.5", - "@types/react-icons": "^3.0.0", - "@types/react-transition-group": "^4.4.10", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/strftime": "^0.9.8", "@typescript-eslint/eslint-plugin": "^7.5.0", "@typescript-eslint/parser": "^7.5.0", @@ -117,19 +112,26 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.2.0", "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.8", "eslint-plugin-vitest-globals": "^1.5.0", "fake-indexeddb": "^6.0.0", "jest-websocket-mock": "^2.5.0", "jsdom": "^24.1.1", + "monaco-editor": "^0.52.0", "msw": "^2.3.5", + "patch-package": "^8.0.1", "postcss": "^8.4.47", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.5", "tailwindcss": "^3.4.9", - "typescript": "^5.8.2", + "typescript": "^5.9.3", "vite": "^6.4.1", "vitest": "^3.0.7" + }, + "overrides": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-slot": "1.2.4" } } diff --git a/web/patches/@radix-ui+react-compose-refs+1.1.2.patch b/web/patches/@radix-ui+react-compose-refs+1.1.2.patch new file mode 100644 index 000000000..0cb022b22 --- /dev/null +++ b/web/patches/@radix-ui+react-compose-refs+1.1.2.patch @@ -0,0 +1,75 @@ +diff --git a/node_modules/@radix-ui/react-compose-refs/dist/index.js b/node_modules/@radix-ui/react-compose-refs/dist/index.js +index 5ba7a95..65aa7be 100644 +--- a/node_modules/@radix-ui/react-compose-refs/dist/index.js ++++ b/node_modules/@radix-ui/react-compose-refs/dist/index.js +@@ -69,6 +69,31 @@ function composeRefs(...refs) { + }; + } + function useComposedRefs(...refs) { +- return React.useCallback(composeRefs(...refs), refs); ++ const refsRef = React.useRef(refs); ++ React.useLayoutEffect(() => { ++ refsRef.current = refs; ++ }); ++ return React.useCallback((node) => { ++ let hasCleanup = false; ++ const cleanups = refsRef.current.map((ref) => { ++ const cleanup = setRef(ref, node); ++ if (!hasCleanup && typeof cleanup === "function") { ++ hasCleanup = true; ++ } ++ return cleanup; ++ }); ++ if (hasCleanup) { ++ return () => { ++ for (let i = 0; i < cleanups.length; i++) { ++ const cleanup = cleanups[i]; ++ if (typeof cleanup === "function") { ++ cleanup(); ++ } else { ++ setRef(refsRef.current[i], null); ++ } ++ } ++ }; ++ } ++ }, []); + } + //# sourceMappingURL=index.js.map +diff --git a/node_modules/@radix-ui/react-compose-refs/dist/index.mjs b/node_modules/@radix-ui/react-compose-refs/dist/index.mjs +index 7dd9172..d1b53a5 100644 +--- a/node_modules/@radix-ui/react-compose-refs/dist/index.mjs ++++ b/node_modules/@radix-ui/react-compose-refs/dist/index.mjs +@@ -32,7 +32,32 @@ function composeRefs(...refs) { + }; + } + function useComposedRefs(...refs) { +- return React.useCallback(composeRefs(...refs), refs); ++ const refsRef = React.useRef(refs); ++ React.useLayoutEffect(() => { ++ refsRef.current = refs; ++ }); ++ return React.useCallback((node) => { ++ let hasCleanup = false; ++ const cleanups = refsRef.current.map((ref) => { ++ const cleanup = setRef(ref, node); ++ if (!hasCleanup && typeof cleanup === "function") { ++ hasCleanup = true; ++ } ++ return cleanup; ++ }); ++ if (hasCleanup) { ++ return () => { ++ for (let i = 0; i < cleanups.length; i++) { ++ const cleanup = cleanups[i]; ++ if (typeof cleanup === "function") { ++ cleanup(); ++ } else { ++ setRef(refsRef.current[i], null); ++ } ++ } ++ }; ++ } ++ }, []); + } + export { + composeRefs, diff --git a/web/patches/@radix-ui+react-slot+1.2.4.patch b/web/patches/@radix-ui+react-slot+1.2.4.patch new file mode 100644 index 000000000..62c2467e2 --- /dev/null +++ b/web/patches/@radix-ui+react-slot+1.2.4.patch @@ -0,0 +1,46 @@ +diff --git a/node_modules/@radix-ui/react-slot/dist/index.js b/node_modules/@radix-ui/react-slot/dist/index.js +index 3691205..3b62ea8 100644 +--- a/node_modules/@radix-ui/react-slot/dist/index.js ++++ b/node_modules/@radix-ui/react-slot/dist/index.js +@@ -85,11 +85,12 @@ function createSlotClone(ownerName) { + if (isLazyComponent(children) && typeof use === "function") { + children = use(children._payload); + } ++ const childrenRef = React.isValidElement(children) ? getElementRef(children) : null; ++ const composedRef = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, childrenRef); + if (React.isValidElement(children)) { +- const childrenRef = getElementRef(children); + const props2 = mergeProps(slotProps, children.props); + if (children.type !== React.Fragment) { +- props2.ref = forwardedRef ? (0, import_react_compose_refs.composeRefs)(forwardedRef, childrenRef) : childrenRef; ++ props2.ref = forwardedRef ? composedRef : childrenRef; + } + return React.cloneElement(children, props2); + } +diff --git a/node_modules/@radix-ui/react-slot/dist/index.mjs b/node_modules/@radix-ui/react-slot/dist/index.mjs +index d7ea374..a990150 100644 +--- a/node_modules/@radix-ui/react-slot/dist/index.mjs ++++ b/node_modules/@radix-ui/react-slot/dist/index.mjs +@@ -1,6 +1,6 @@ + // src/slot.tsx + import * as React from "react"; +-import { composeRefs } from "@radix-ui/react-compose-refs"; ++import { composeRefs, useComposedRefs } from "@radix-ui/react-compose-refs"; + import { Fragment as Fragment2, jsx } from "react/jsx-runtime"; + var REACT_LAZY_TYPE = Symbol.for("react.lazy"); + var use = React[" use ".trim().toString()]; +@@ -45,11 +45,12 @@ function createSlotClone(ownerName) { + if (isLazyComponent(children) && typeof use === "function") { + children = use(children._payload); + } ++ const childrenRef = React.isValidElement(children) ? getElementRef(children) : null; ++ const composedRef = useComposedRefs(forwardedRef, childrenRef); + if (React.isValidElement(children)) { +- const childrenRef = getElementRef(children); + const props2 = mergeProps(slotProps, children.props); + if (children.type !== React.Fragment) { +- props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef; ++ props2.ref = forwardedRef ? composedRef : childrenRef; + } + return React.cloneElement(children, props2); + } diff --git a/web/patches/react-use-websocket+4.8.1.patch b/web/patches/react-use-websocket+4.8.1.patch new file mode 100644 index 000000000..5de81c525 --- /dev/null +++ b/web/patches/react-use-websocket+4.8.1.patch @@ -0,0 +1,23 @@ +diff --git a/node_modules/react-use-websocket/dist/lib/use-websocket.js b/node_modules/react-use-websocket/dist/lib/use-websocket.js +index f01db48..b30aff2 100644 +--- a/node_modules/react-use-websocket/dist/lib/use-websocket.js ++++ b/node_modules/react-use-websocket/dist/lib/use-websocket.js +@@ -139,15 +139,15 @@ var useWebSocket = function (url, options, connect) { + } + protectedSetLastMessage = function (message) { + if (!expectClose_1) { +- (0, react_dom_1.flushSync)(function () { return setLastMessage(message); }); ++ setLastMessage(message); + } + }; + protectedSetReadyState = function (state) { + if (!expectClose_1) { +- (0, react_dom_1.flushSync)(function () { return setReadyState(function (prev) { ++ setReadyState(function (prev) { + var _a; + return (__assign(__assign({}, prev), (convertedUrl.current && (_a = {}, _a[convertedUrl.current] = state, _a)))); +- }); }); ++ }); + } + }; + if (createOrJoin_1) { diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index 1181554af..37566117a 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -117,6 +117,7 @@ "button": { "add": "Add", "apply": "Apply", + "applying": "Applying…", "reset": "Reset", "undo": "Undo", "done": "Done", @@ -252,6 +253,7 @@ "review": "Review", "explore": "Explore", "export": "Export", + "actions": "Actions", "uiPlayground": "UI Playground", "faceLibrary": "Face Library", "classification": "Classification", diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index 53b04e6c4..661a9a5e9 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -216,6 +216,10 @@ }, "hideObjectDetails": { "label": "Hide object path" + }, + "debugReplay": { + "label": "Debug replay", + "aria": "View this tracked object in the debug replay view" } }, "dialog": { diff --git a/web/public/locales/en/views/replay.json b/web/public/locales/en/views/replay.json new file mode 100644 index 000000000..a966626f5 --- /dev/null +++ b/web/public/locales/en/views/replay.json @@ -0,0 +1,54 @@ +{ + "title": "Debug Replay", + "description": "Replay camera recordings for debugging. The object list shows a time-delayed summary of detected objects and the Messages tab shows a stream of Frigate's internal messages from the replay footage.", + "websocket_messages": "Messages", + "dialog": { + "title": "Start Debug Replay", + "description": "Create a temporary replay camera that loops historical footage for debugging object detection and tracking issues. The replay camera will have the same detection configuration as the source camera. Choose a time range to begin.", + "camera": "Source Camera", + "timeRange": "Time Range", + "preset": { + "1m": "Last 1 Minute", + "5m": "Last 5 Minutes", + "timeline": "From Timeline", + "custom": "Custom" + }, + "startButton": "Start Replay", + "selectFromTimeline": "Select", + "starting": "Starting replay...", + "startLabel": "Start", + "endLabel": "End", + "toast": { + "success": "Debug replay started successfully", + "error": "Failed to start debug replay: {{error}}", + "alreadyActive": "A replay session is already active", + "stopped": "Debug replay stopped", + "stopError": "Failed to stop debug replay: {{error}}", + "goToReplay": "Go to Replay" + } + }, + "page": { + "noSession": "No Active Replay Session", + "noSessionDesc": "Start a debug replay from the History view by clicking the Debug Replay button in the toolbar.", + "goToRecordings": "Go to History", + "sourceCamera": "Source Camera", + "replayCamera": "Replay Camera", + "initializingReplay": "Initializing replay...", + "stoppingReplay": "Stopping replay...", + "stopReplay": "Stop Replay", + "confirmStop": { + "title": "Stop Debug Replay?", + "description": "This will stop the replay session and clean up all temporary data. Are you sure?", + "confirm": "Stop Replay", + "cancel": "Cancel" + }, + "activity": "Activity", + "objects": "Object List", + "audioDetections": "Audio Detections", + "noActivity": "No activity detected", + "activeTracking": "Active tracking", + "noActiveTracking": "No active tracking", + "configuration": "Configuration", + "configurationDesc": "Fine tune motion detection and object tracking settings for the debug replay camera. No changes are saved to your Frigate configuration file." + } +} diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index ca772377b..81c9b8075 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -83,7 +83,8 @@ "triggers": "Triggers", "debug": "Debug", "frigateplus": "Frigate+", - "maintenance": "Maintenance" + "mediaSync": "Media sync", + "regionGrid": "Region grid" }, "dialog": { "unsavedChanges": { @@ -1232,6 +1233,16 @@ "previews": "Previews", "exports": "Exports", "recordings": "Recordings" + }, + "regionGrid": { + "title": "Region Grid", + "desc": "The region grid is an optimization that learns where objects of different sizes typically appear in each camera's field of view. Frigate uses this data to efficiently size detection regions. The grid is automatically built over time from tracked object data.", + "clear": "Clear region grid", + "clearConfirmTitle": "Clear Region Grid", + "clearConfirmDesc": "Clearing the region grid is not recommended unless you have recently changed your detector model size or have changed your camera's physical position and are having object tracking issues. The grid will be automatically rebuilt over time as objects are tracked. A Frigate restart is required for changes to take effect.", + "clearSuccess": "Region grid cleared successfully", + "clearError": "Failed to clear region grid", + "restartRequired": "Restart required for region grid changes to take effect" } }, "configForm": { @@ -1392,6 +1403,7 @@ }, "toast": { "success": "Settings saved successfully", + "applied": "Settings applied successfully", "successRestartRequired": "Settings saved successfully. Restart Frigate to apply your changes.", "error": "Failed to save settings", "validationError": "Validation failed: {{message}}", diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json index 8ddbc03e1..faaff31c9 100644 --- a/web/public/locales/en/views/system.json +++ b/web/public/locales/en/views/system.json @@ -7,12 +7,39 @@ "logs": { "frigate": "Frigate Logs - Frigate", "go2rtc": "Go2RTC Logs - Frigate", - "nginx": "Nginx Logs - Frigate" + "nginx": "Nginx Logs - Frigate", + "websocket": "Messages Logs - Frigate" } }, "title": "System", "metrics": "System metrics", "logs": { + "websocket": { + "label": "Messages", + "pause": "Pause", + "resume": "Resume", + "clear": "Clear", + "filter": { + "all": "All topics", + "topics": "Topics", + "events": "Events", + "reviews": "Reviews", + "classification": "Classification", + "face_recognition": "Face Recognition", + "lpr": "LPR", + "camera_activity": "Camera activity", + "system": "System", + "camera": "Camera", + "all_cameras": "All cameras", + "cameras_count_one": "{{count}} Camera", + "cameras_count_other": "{{count}} Cameras" + }, + "empty": "No messages captured yet", + "count": "{{count}} messages", + "expanded": { + "payload": "Payload" + } + }, "download": { "label": "Download Logs" }, @@ -189,7 +216,8 @@ "cameraIsOffline": "{{camera}} is offline", "detectIsSlow": "{{detect}} is slow ({{speed}} ms)", "detectIsVerySlow": "{{detect}} is very slow ({{speed}} ms)", - "shmTooLow": "/dev/shm allocation ({{total}} MB) should be increased to at least {{min}} MB." + "shmTooLow": "/dev/shm allocation ({{total}} MB) should be increased to at least {{min}} MB.", + "debugReplayActive": "Debug replay session is active" }, "enrichments": { "title": "Enrichments", diff --git a/web/src/App.tsx b/web/src/App.tsx index 82ca2b1e0..21babc2b9 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -30,6 +30,7 @@ const Classification = lazy(() => import("@/pages/ClassificationModel")); const Chat = lazy(() => import("@/pages/Chat")); const Logs = lazy(() => import("@/pages/Logs")); const AccessDenied = lazy(() => import("@/pages/AccessDenied")); +const Replay = lazy(() => import("@/pages/Replay")); function App() { const { data: config } = useSWR("config", { @@ -108,7 +109,8 @@ function DefaultAppView() { } /> } /> } /> - } /> + } />{" "} + } />{" "} } /> } /> diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index f5b7b57a3..07d44d67a 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -1,5 +1,5 @@ import { baseUrl } from "./baseUrl"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import useWebSocket, { ReadyState } from "react-use-websocket"; import { EmbeddingsReindexProgressType, @@ -17,6 +17,13 @@ import { FrigateStats } from "@/types/stats"; import { createContainer } from "react-tracked"; import useDeepMemo from "@/hooks/use-deep-memo"; +export type WsFeedMessage = { + topic: string; + payload: unknown; + timestamp: number; + id: string; +}; + type Update = { topic: string; payload: unknown; @@ -29,6 +36,9 @@ type WsState = { type useValueReturn = [WsState, (update: Update) => void]; +const wsMessageSubscribers = new Set<(msg: WsFeedMessage) => void>(); +let wsMessageIdCounter = 0; + function useValue(): useValueReturn { const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`; @@ -43,8 +53,13 @@ function useValue(): useValueReturn { return; } - const cameraActivity: { [key: string]: FrigateCameraState } = - JSON.parse(activityValue); + let cameraActivity: { [key: string]: Partial }; + + try { + cameraActivity = JSON.parse(activityValue); + } catch { + return; + } if (Object.keys(cameraActivity).length === 0) { return; @@ -53,6 +68,12 @@ function useValue(): useValueReturn { const cameraStates: WsState = {}; Object.entries(cameraActivity).forEach(([name, state]) => { + const cameraConfig = state?.config; + + if (!cameraConfig) { + return; + } + const { record, detect, @@ -67,7 +88,7 @@ function useValue(): useValueReturn { detections, object_descriptions, review_descriptions, - } = state["config"]; + } = cameraConfig; cameraStates[`${name}/recordings/state`] = record ? "ON" : "OFF"; cameraStates[`${name}/enabled/state`] = enabled ? "ON" : "OFF"; cameraStates[`${name}/detect/state`] = detect ? "ON" : "OFF"; @@ -115,6 +136,17 @@ function useValue(): useValueReturn { ...prevState, [data.topic]: data.payload, })); + + // Notify feed subscribers + if (wsMessageSubscribers.size > 0) { + const feedMsg: WsFeedMessage = { + topic: data.topic, + payload: data.payload, + timestamp: Date.now(), + id: String(wsMessageIdCounter++), + }; + wsMessageSubscribers.forEach((cb) => cb(feedMsg)); + } } }, onOpen: () => { @@ -740,3 +772,16 @@ export function useJobStatus( return { payload: currentJob as Job | null }; } + +export function useWsMessageSubscribe(callback: (msg: WsFeedMessage) => void) { + const callbackRef = useRef(callback); + callbackRef.current = callback; + + useEffect(() => { + const handler = (msg: WsFeedMessage) => callbackRef.current(msg); + wsMessageSubscribers.add(handler); + return () => { + wsMessageSubscribers.delete(handler); + }; + }, []); +} diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx index 716e63f57..f0c05995e 100644 --- a/web/src/components/camera/CameraImage.tsx +++ b/web/src/components/camera/CameraImage.tsx @@ -26,7 +26,8 @@ export default function CameraImage({ const containerRef = useRef(null); const imgRef = useRef(null); - const { name } = config ? config.cameras[camera] : ""; + const cameraConfig = config?.cameras?.[camera]; + const { name } = cameraConfig ?? { name: camera }; const { payload: enabledState } = useEnabledState(camera); const enabled = enabledState ? enabledState === "ON" : true; @@ -34,15 +35,15 @@ export default function CameraImage({ useResizeObserver(containerRef); const requestHeight = useMemo(() => { - if (!config || containerHeight == 0) { + if (!cameraConfig || containerHeight == 0) { return 360; } return Math.min( - config.cameras[camera].detect.height, + cameraConfig.detect.height, Math.round(containerHeight * (isDesktop ? 1.1 : 1.25)), ); - }, [config, camera, containerHeight]); + }, [cameraConfig, containerHeight]); const [isPortraitImage, setIsPortraitImage] = useState(false); diff --git a/web/src/components/config-form/section-configs/motion.ts b/web/src/components/config-form/section-configs/motion.ts index 0acdc0d99..41b5738d3 100644 --- a/web/src/components/config-form/section-configs/motion.ts +++ b/web/src/components/config-form/section-configs/motion.ts @@ -44,6 +44,30 @@ const motion: SectionConfigOverrides = { camera: { restartRequired: ["frame_height"], }, + replay: { + restartRequired: [], + fieldOrder: [ + "threshold", + "contour_area", + "lightning_threshold", + "improve_contrast", + ], + fieldGroups: { + sensitivity: ["threshold", "contour_area"], + algorithm: ["improve_contrast"], + }, + hiddenFields: [ + "enabled", + "enabled_in_config", + "mask", + "raw_mask", + "mqtt_off_delay", + "delta_alpha", + "frame_alpha", + "frame_height", + ], + advancedFields: ["lightning_threshold"], + }, }; export default motion; diff --git a/web/src/components/config-form/section-configs/objects.ts b/web/src/components/config-form/section-configs/objects.ts index 1dfb31053..a70746c49 100644 --- a/web/src/components/config-form/section-configs/objects.ts +++ b/web/src/components/config-form/section-configs/objects.ts @@ -99,6 +99,28 @@ const objects: SectionConfigOverrides = { camera: { restartRequired: [], }, + replay: { + restartRequired: [], + fieldOrder: ["track", "filters"], + fieldGroups: { + tracking: ["track"], + filtering: ["filters"], + }, + hiddenFields: [ + "enabled_in_config", + "alert", + "detect", + "mask", + "raw_mask", + "genai", + "genai.enabled_in_config", + "filters.*.mask", + "filters.*.raw_mask", + "filters.mask", + "filters.raw_mask", + ], + advancedFields: [], + }, }; export default objects; diff --git a/web/src/components/config-form/section-configs/types.ts b/web/src/components/config-form/section-configs/types.ts index 600a3ca50..e2b308e08 100644 --- a/web/src/components/config-form/section-configs/types.ts +++ b/web/src/components/config-form/section-configs/types.ts @@ -4,4 +4,5 @@ export type SectionConfigOverrides = { base?: SectionConfig; global?: Partial; camera?: Partial; + replay?: Partial; }; diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 606919dcc..821857ae6 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -95,9 +95,9 @@ export interface SectionConfig { } export interface BaseSectionProps { - /** Whether this is at global or camera level */ - level: "global" | "camera"; - /** Camera name (required if level is "camera") */ + /** Whether this is at global, camera, or replay level */ + level: "global" | "camera" | "replay"; + /** Camera name (required if level is "camera" or "replay") */ cameraName?: string; /** Whether to show override indicator badge */ showOverrideIndicator?: boolean; @@ -117,6 +117,10 @@ export interface BaseSectionProps { defaultCollapsed?: boolean; /** Whether to show the section title (default: false for global, true for camera) */ showTitle?: boolean; + /** If true, apply config in-memory only without writing to YAML */ + skipSave?: boolean; + /** If true, buttons are not sticky at the bottom */ + noStickyButtons?: boolean; /** Callback when section status changes */ onStatusChange?: (status: { hasChanges: boolean; @@ -156,12 +160,16 @@ export function ConfigSection({ collapsible = false, defaultCollapsed = true, showTitle, + skipSave = false, + noStickyButtons = false, onStatusChange, pendingDataBySection, onPendingDataChange, }: ConfigSectionProps) { + // For replay level, treat as camera-level config access + const effectiveLevel = level === "replay" ? "camera" : level; const { t, i18n } = useTranslation([ - level === "camera" ? "config/cameras" : "config/global", + effectiveLevel === "camera" ? "config/cameras" : "config/global", "config/cameras", "views/settings", "common", @@ -174,10 +182,10 @@ export function ConfigSection({ // Create a key for this section's pending data const pendingDataKey = useMemo( () => - level === "camera" && cameraName + effectiveLevel === "camera" && cameraName ? `${cameraName}::${sectionPath}` : sectionPath, - [level, cameraName, sectionPath], + [effectiveLevel, cameraName, sectionPath], ); // Use pending data from parent if available, otherwise use local state @@ -222,20 +230,20 @@ export function ConfigSection({ const lastPendingDataKeyRef = useRef(null); const updateTopic = - level === "camera" && cameraName + effectiveLevel === "camera" && cameraName ? cameraUpdateTopicMap[sectionPath] ? `config/cameras/${cameraName}/${cameraUpdateTopicMap[sectionPath]}` : undefined : `config/${sectionPath}`; // Default: show title for camera level (since it might be collapsible), hide for global - const shouldShowTitle = showTitle ?? level === "camera"; + const shouldShowTitle = showTitle ?? effectiveLevel === "camera"; // Fetch config const { data: config, mutate: refreshConfig } = useSWR("config"); // Get section schema using cached hook - const sectionSchema = useSectionSchema(sectionPath, level); + const sectionSchema = useSectionSchema(sectionPath, effectiveLevel); // Apply special case handling for sections with problematic schema defaults const modifiedSchema = useMemo( @@ -247,7 +255,7 @@ export function ConfigSection({ // Get override status const { isOverridden, globalValue, cameraValue } = useConfigOverride({ config, - cameraName: level === "camera" ? cameraName : undefined, + cameraName: effectiveLevel === "camera" ? cameraName : undefined, sectionPath, compareFields: sectionConfig.overrideFields, }); @@ -256,12 +264,12 @@ export function ConfigSection({ const rawSectionValue = useMemo(() => { if (!config) return undefined; - if (level === "camera" && cameraName) { + if (effectiveLevel === "camera" && cameraName) { return get(config.cameras?.[cameraName], sectionPath); } return get(config, sectionPath); - }, [config, level, cameraName, sectionPath]); + }, [config, cameraName, sectionPath, effectiveLevel]); const rawFormData = useMemo(() => { if (!config) return {}; @@ -328,9 +336,10 @@ export function ConfigSection({ [rawFormData, sanitizeSectionData], ); - // Clear pendingData whenever formData changes (e.g., from server refresh) - // This prevents RJSF's initial onChange call from being treated as a user edit - // Only clear if pendingData is managed locally (not by parent) + // Clear pendingData whenever the section/camera key changes (e.g., switching + // cameras) or when there is no pending data yet (initialization). + // This prevents RJSF's initial onChange call from being treated as a user edit. + // Only clear if pendingData is managed locally (not by parent). useEffect(() => { const pendingKeyChanged = lastPendingDataKeyRef.current !== pendingDataKey; @@ -339,15 +348,16 @@ export function ConfigSection({ isInitializingRef.current = true; setPendingOverrides(undefined); setDirtyOverrides(undefined); + + // Reset local pending data when switching sections/cameras + if (onPendingDataChange === undefined) { + setPendingData(null); + } } else if (!pendingData) { isInitializingRef.current = true; setPendingOverrides(undefined); setDirtyOverrides(undefined); } - - if (onPendingDataChange === undefined) { - setPendingData(null); - } }, [ onPendingDataChange, pendingData, @@ -484,7 +494,7 @@ export function ConfigSection({ setIsSaving(true); try { const basePath = - level === "camera" && cameraName + effectiveLevel === "camera" && cameraName ? `cameras.${cameraName}.${sectionPath}` : sectionPath; const rawData = sanitizeSectionData(rawFormData); @@ -495,7 +505,7 @@ export function ConfigSection({ ); const sanitizedOverrides = sanitizeOverridesForSection( sectionPath, - level, + effectiveLevel, overrides, ); @@ -508,16 +518,26 @@ export function ConfigSection({ return; } - const needsRestart = requiresRestartForOverrides(sanitizedOverrides); + const needsRestart = skipSave + ? false + : requiresRestartForOverrides(sanitizedOverrides); const configData = buildConfigDataForPath(basePath, sanitizedOverrides); await axios.put("config/set", { requires_restart: needsRestart ? 1 : 0, update_topic: updateTopic, config_data: configData, + ...(skipSave ? { skip_save: true } : {}), }); - if (needsRestart) { + if (skipSave) { + toast.success( + t("toast.applied", { + ns: "views/settings", + defaultValue: "Settings applied successfully", + }), + ); + } else if (needsRestart) { statusBar?.addMessage( "config_restart_required", t("configForm.restartRequiredFooter", { @@ -596,7 +616,7 @@ export function ConfigSection({ }, [ sectionPath, pendingData, - level, + effectiveLevel, cameraName, t, refreshConfig, @@ -608,15 +628,16 @@ export function ConfigSection({ updateTopic, setPendingData, requiresRestartForOverrides, + skipSave, ]); // Handle reset to global/defaults - removes camera-level override or resets global to defaults const handleResetToGlobal = useCallback(async () => { - if (level === "camera" && !cameraName) return; + if (effectiveLevel === "camera" && !cameraName) return; try { const basePath = - level === "camera" && cameraName + effectiveLevel === "camera" && cameraName ? `cameras.${cameraName}.${sectionPath}` : sectionPath; @@ -632,7 +653,7 @@ export function ConfigSection({ t("toast.resetSuccess", { ns: "views/settings", defaultValue: - level === "global" + effectiveLevel === "global" ? "Reset to defaults" : "Reset to global defaults", }), @@ -651,7 +672,7 @@ export function ConfigSection({ } }, [ sectionPath, - level, + effectiveLevel, cameraName, requiresRestart, t, @@ -661,8 +682,8 @@ export function ConfigSection({ ]); const sectionValidation = useMemo( - () => getSectionValidation({ sectionPath, level, t }), - [sectionPath, level, t], + () => getSectionValidation({ sectionPath, level: effectiveLevel, t }), + [sectionPath, effectiveLevel, t], ); const customValidate = useMemo(() => { @@ -733,7 +754,7 @@ export function ConfigSection({ // nested under the section name (e.g., `audio.label`). For global-level // sections, keys are nested under the section name in `config/global`. const configNamespace = - level === "camera" ? "config/cameras" : "config/global"; + effectiveLevel === "camera" ? "config/cameras" : "config/global"; const title = t(`${sectionPath}.label`, { ns: configNamespace, defaultValue: defaultTitle, @@ -769,7 +790,7 @@ export function ConfigSection({ i18nNamespace={configNamespace} customValidate={customValidate} formContext={{ - level, + level: effectiveLevel, cameraName, globalValue, cameraValue, @@ -784,7 +805,7 @@ export function ConfigSection({ onFormDataChange: (data: ConfigSectionData) => handleChange(data), // For widgets that need access to full camera config (e.g., zone names) fullCameraConfig: - level === "camera" && cameraName + effectiveLevel === "camera" && cameraName ? config?.cameras?.[cameraName] : undefined, fullConfig: config, @@ -804,7 +825,12 @@ export function ConfigSection({ }} /> -
+
)}
- {((level === "camera" && isOverridden) || level === "global") && - !hasChanges && ( + {((effectiveLevel === "camera" && isOverridden) || + effectiveLevel === "global") && + !hasChanges && + !skipSave && ( + + ), + }, + ); + } else { + toast.error(t("dialog.toast.error", { error: errorMessage }), { + position: "top-center", + }); + } + }) + .finally(() => { + setIsStarting(false); + }); + }, + [navigate, t], + ); + const MenuItem = isContextMenu ? ContextMenuItem : DropdownMenuItem; const menuItems = ( @@ -149,6 +205,20 @@ export default function SearchResultActions({ {t("itemMenu.addTrigger.label")} )} + {searchResult.has_clip && ( + { + handleDebugReplay(searchResult); + }} + > + {isStarting + ? t("dialog.starting", { ns: "views/replay" }) + : t("itemMenu.debugReplay.label")} + + )} {isAdmin && ( + {children} - + ); } @@ -102,7 +102,7 @@ export function MobilePagePortal({ type MobilePageContentProps = { children: React.ReactNode; className?: string; - scrollerRef?: React.RefObject; + scrollerRef?: React.RefObject; }; export function MobilePageContent({ diff --git a/web/src/components/overlay/ActionsDropdown.tsx b/web/src/components/overlay/ActionsDropdown.tsx new file mode 100644 index 000000000..d73ae71e3 --- /dev/null +++ b/web/src/components/overlay/ActionsDropdown.tsx @@ -0,0 +1,46 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { Button } from "../ui/button"; +import { useTranslation } from "react-i18next"; +import { FaFilm } from "react-icons/fa6"; + +type ActionsDropdownProps = { + onDebugReplayClick: () => void; + onExportClick: () => void; +}; + +export default function ActionsDropdown({ + onDebugReplayClick, + onExportClick, +}: ActionsDropdownProps) { + const { t } = useTranslation(["components/dialog", "views/replay", "common"]); + + return ( + + + + + + + {t("menu.export", { ns: "common" })} + + + {t("title", { ns: "views/replay" })} + + + + ); +} diff --git a/web/src/components/overlay/CustomTimeSelector.tsx b/web/src/components/overlay/CustomTimeSelector.tsx new file mode 100644 index 000000000..0d9a4d052 --- /dev/null +++ b/web/src/components/overlay/CustomTimeSelector.tsx @@ -0,0 +1,240 @@ +import { useMemo, useState } from "react"; +import { Button } from "../ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { SelectSeparator } from "../ui/select"; +import { TimeRange } from "@/types/timeline"; +import { useFormattedTimestamp } from "@/hooks/use-date-utils"; +import { getUTCOffset } from "@/utils/dateUtil"; +import { TimezoneAwareCalendar } from "./ReviewActivityCalendar"; +import { FaArrowRight, FaCalendarAlt } from "react-icons/fa"; +import { isDesktop, isIOS } from "react-device-detect"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useTranslation } from "react-i18next"; + +type CustomTimeSelectorProps = { + latestTime: number; + range?: TimeRange; + setRange: (range: TimeRange | undefined) => void; + startLabel: string; + endLabel: string; +}; + +export function CustomTimeSelector({ + latestTime, + range, + setRange, + startLabel, + endLabel, +}: CustomTimeSelectorProps) { + const { t } = useTranslation(["common"]); + const { data: config } = useSWR("config"); + + // times + const timezoneOffset = useMemo( + () => + config?.ui.timezone + ? Math.round(getUTCOffset(new Date(), config.ui.timezone)) + : undefined, + [config?.ui.timezone], + ); + const localTimeOffset = useMemo( + () => + Math.round( + getUTCOffset( + new Date(), + Intl.DateTimeFormat().resolvedOptions().timeZone, + ), + ), + [], + ); + + const startTime = useMemo(() => { + let time = range?.after || latestTime - 3600; + + if (timezoneOffset) { + time = time + (timezoneOffset - localTimeOffset) * 60; + } + + return time; + }, [range, latestTime, timezoneOffset, localTimeOffset]); + + const endTime = useMemo(() => { + let time = range?.before || latestTime; + + if (timezoneOffset) { + time = time + (timezoneOffset - localTimeOffset) * 60; + } + + return time; + }, [range, latestTime, timezoneOffset, localTimeOffset]); + + const formattedStart = useFormattedTimestamp( + startTime, + config?.ui.time_format == "24hour" + ? t("time.formattedTimestamp.24hour") + : t("time.formattedTimestamp.12hour"), + ); + + const formattedEnd = useFormattedTimestamp( + endTime, + config?.ui.time_format == "24hour" + ? t("time.formattedTimestamp.24hour") + : t("time.formattedTimestamp.12hour"), + ); + + const startClock = useMemo(() => { + const date = new Date(startTime * 1000); + return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; + }, [startTime]); + + const endClock = useMemo(() => { + const date = new Date(endTime * 1000); + return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; + }, [endTime]); + + // calendars + const [startOpen, setStartOpen] = useState(false); + const [endOpen, setEndOpen] = useState(false); + + return ( +
+ +
+ { + if (!open) { + setStartOpen(false); + } + }} + > + + + + + { + if (!day) { + return; + } + + setRange({ + before: endTime, + after: day.getTime() / 1000 + 1, + }); + }} + /> + + { + const clock = e.target.value; + const [hour, minute, second] = isIOS + ? [...clock.split(":"), "00"] + : clock.split(":"); + + const start = new Date(startTime * 1000); + start.setHours( + parseInt(hour), + parseInt(minute), + parseInt(second ?? 0), + 0, + ); + setRange({ + before: endTime, + after: start.getTime() / 1000, + }); + }} + /> + + + + { + if (!open) { + setEndOpen(false); + } + }} + > + + + + + { + if (!day) { + return; + } + + setRange({ + after: startTime, + before: day.getTime() / 1000, + }); + }} + /> + + { + const clock = e.target.value; + const [hour, minute, second] = isIOS + ? [...clock.split(":"), "00"] + : clock.split(":"); + + const end = new Date(endTime * 1000); + end.setHours( + parseInt(hour), + parseInt(minute), + parseInt(second ?? 0), + 0, + ); + setRange({ + before: end.getTime() / 1000, + after: startTime, + }); + }} + /> + + +
+
+ ); +} diff --git a/web/src/components/overlay/DebugDrawingLayer.tsx b/web/src/components/overlay/DebugDrawingLayer.tsx index b45ef3f81..7cc52bc93 100644 --- a/web/src/components/overlay/DebugDrawingLayer.tsx +++ b/web/src/components/overlay/DebugDrawingLayer.tsx @@ -10,7 +10,7 @@ import Konva from "konva"; import { useResizeObserver } from "@/hooks/resize-observer"; type DebugDrawingLayerProps = { - containerRef: React.RefObject; + containerRef: React.RefObject; cameraWidth: number; cameraHeight: number; }; diff --git a/web/src/components/overlay/DebugReplayDialog.tsx b/web/src/components/overlay/DebugReplayDialog.tsx new file mode 100644 index 000000000..9c3efb4f5 --- /dev/null +++ b/web/src/components/overlay/DebugReplayDialog.tsx @@ -0,0 +1,367 @@ +import { useCallback, useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../ui/dialog"; +import { Label } from "../ui/label"; +import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; +import { Button } from "../ui/button"; +import axios from "axios"; +import { toast } from "sonner"; +import { isDesktop } from "react-device-detect"; +import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { SelectSeparator } from "../ui/select"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { LuBug, LuPlay, LuX } from "react-icons/lu"; +import { ExportMode } from "@/types/filter"; +import { TimeRange } from "@/types/timeline"; +import { cn } from "@/lib/utils"; +import { CustomTimeSelector } from "./CustomTimeSelector"; + +const REPLAY_TIME_OPTIONS = ["1", "5", "timeline", "custom"] as const; +type ReplayTimeOption = (typeof REPLAY_TIME_OPTIONS)[number]; + +type DebugReplayContentProps = { + currentTime: number; + latestTime: number; + range?: TimeRange; + selectedOption: ReplayTimeOption; + isStarting: boolean; + onSelectedOptionChange: (option: ReplayTimeOption) => void; + onStart: () => void; + onCancel: () => void; + setRange: (range: TimeRange | undefined) => void; + setMode: (mode: ExportMode) => void; +}; + +export function DebugReplayContent({ + currentTime, + latestTime, + range, + selectedOption, + isStarting, + onSelectedOptionChange, + onStart, + onCancel, + setRange, + setMode, +}: DebugReplayContentProps) { + const { t } = useTranslation(["views/replay"]); + + return ( +
+ {isDesktop && ( + <> + + {t("dialog.title")} + {t("dialog.description")} + + + + )} + + {/* Time range */} +
+ + onSelectedOptionChange(value as ReplayTimeOption) + } + > + {REPLAY_TIME_OPTIONS.map((opt) => ( +
+ + +
+ ))} +
+
+ + {/* Custom time inputs */} + {selectedOption === "custom" && ( + + )} + + {isDesktop && } + + +
+ {t("button.cancel", { ns: "common" })} +
+ +
+
+ ); +} + +type DebugReplayDialogProps = { + camera: string; + currentTime: number; + latestTime: number; + range?: TimeRange; + mode: ExportMode; + setRange: (range: TimeRange | undefined) => void; + setMode: (mode: ExportMode) => void; +}; + +export default function DebugReplayDialog({ + camera, + currentTime, + latestTime, + range, + mode, + setRange, + setMode, +}: DebugReplayDialogProps) { + const { t } = useTranslation(["views/replay"]); + const navigate = useNavigate(); + + const [selectedOption, setSelectedOption] = useState("1"); + const [isStarting, setIsStarting] = useState(false); + + const handleTimeOptionChange = useCallback( + (option: ReplayTimeOption) => { + setSelectedOption(option); + + if (option === "custom" || option === "timeline") { + return; + } + + const minutes = parseInt(option, 10); + const end = latestTime; + setRange({ after: end - minutes * 60, before: end }); + }, + [latestTime, setRange], + ); + + const handleStart = useCallback(() => { + if (!range || range.before <= range.after) { + toast.error( + t("dialog.toast.error", { error: "End time must be after start time" }), + { position: "top-center" }, + ); + return; + } + + setIsStarting(true); + + axios + .post("debug_replay/start", { + camera: camera, + start_time: range.after, + end_time: range.before, + }) + .then((response) => { + if (response.status === 200) { + toast.success(t("dialog.toast.success"), { + position: "top-center", + }); + setMode("none"); + setRange(undefined); + navigate("/replay"); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + + if (error.response?.status === 409) { + toast.error(t("dialog.toast.alreadyActive"), { + position: "top-center", + closeButton: true, + dismissible: false, + action: ( + + + + ), + }); + } else { + toast.error(t("dialog.toast.error", { error: errorMessage }), { + position: "top-center", + }); + } + }) + .finally(() => { + setIsStarting(false); + }); + }, [camera, range, navigate, setMode, setRange, t]); + + const handleCancel = useCallback(() => { + setMode("none"); + setRange(undefined); + }, [setMode, setRange]); + + const Overlay = isDesktop ? Dialog : Drawer; + const Trigger = isDesktop ? DialogTrigger : DrawerTrigger; + const Content = isDesktop ? DialogContent : DrawerContent; + + return ( + <> + + { + if (!open) { + setMode("none"); + } + }} + > + {!isDesktop && ( + + + + )} + + + + + + ); +} + +type SaveDebugReplayOverlayProps = { + className: string; + show: boolean; + isStarting: boolean; + onSave: () => void; + onCancel: () => void; +}; + +export function SaveDebugReplayOverlay({ + className, + show, + isStarting, + onSave, + onCancel, +}: SaveDebugReplayOverlayProps) { + const { t } = useTranslation(["views/replay"]); + + return ( +
+
+ + +
+
+ ); +} diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index 738aa689e..6912ebf46 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import { Dialog, DialogContent, @@ -12,16 +12,12 @@ import { Label } from "../ui/label"; import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import { Button } from "../ui/button"; import { ExportMode } from "@/types/filter"; -import { FaArrowDown, FaArrowRight, FaCalendarAlt } from "react-icons/fa"; +import { FaArrowDown } from "react-icons/fa"; import axios from "axios"; import { toast } from "sonner"; import { Input } from "../ui/input"; import { TimeRange } from "@/types/timeline"; -import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import useSWR from "swr"; -import { FrigateConfig } from "@/types/frigateConfig"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -import { TimezoneAwareCalendar } from "./ReviewActivityCalendar"; import { Select, SelectContent, @@ -30,15 +26,15 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; -import { isDesktop, isIOS, isMobile } from "react-device-detect"; +import { isDesktop, isMobile } from "react-device-detect"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import SaveExportOverlay from "./SaveExportOverlay"; -import { getUTCOffset } from "@/utils/dateUtil"; import { baseUrl } from "@/api/baseUrl"; import { cn } from "@/lib/utils"; import { GenericVideoPlayer } from "../player/GenericVideoPlayer"; import { useTranslation } from "react-i18next"; import { ExportCase } from "@/types/export"; +import { CustomTimeSelector } from "./CustomTimeSelector"; const EXPORT_OPTIONS = [ "1", @@ -167,31 +163,33 @@ export default function ExportDialog({ } }} > - - - + {!isDesktop && ( + + + + )} )} void; -}; -function CustomTimeSelector({ - latestTime, - range, - setRange, -}: CustomTimeSelectorProps) { - const { t } = useTranslation(["components/dialog"]); - const { data: config } = useSWR("config"); - - // times - - const timezoneOffset = useMemo( - () => - config?.ui.timezone - ? Math.round(getUTCOffset(new Date(), config.ui.timezone)) - : undefined, - [config?.ui.timezone], - ); - const localTimeOffset = useMemo( - () => - Math.round( - getUTCOffset( - new Date(), - Intl.DateTimeFormat().resolvedOptions().timeZone, - ), - ), - [], - ); - - const startTime = useMemo(() => { - let time = range?.after || latestTime - 3600; - - if (timezoneOffset) { - time = time + (timezoneOffset - localTimeOffset) * 60; - } - - return time; - }, [range, latestTime, timezoneOffset, localTimeOffset]); - const endTime = useMemo(() => { - let time = range?.before || latestTime; - - if (timezoneOffset) { - time = time + (timezoneOffset - localTimeOffset) * 60; - } - - return time; - }, [range, latestTime, timezoneOffset, localTimeOffset]); - const formattedStart = useFormattedTimestamp( - startTime, - config?.ui.time_format == "24hour" - ? t("time.formattedTimestamp.24hour", { ns: "common" }) - : t("time.formattedTimestamp.12hour", { ns: "common" }), - ); - const formattedEnd = useFormattedTimestamp( - endTime, - config?.ui.time_format == "24hour" - ? t("time.formattedTimestamp.24hour", { ns: "common" }) - : t("time.formattedTimestamp.12hour", { ns: "common" }), - ); - - const startClock = useMemo(() => { - const date = new Date(startTime * 1000); - return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; - }, [startTime]); - const endClock = useMemo(() => { - const date = new Date(endTime * 1000); - return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; - }, [endTime]); - - // calendars - - const [startOpen, setStartOpen] = useState(false); - const [endOpen, setEndOpen] = useState(false); - - return ( -
- -
- { - if (!open) { - setStartOpen(false); - } - }} - > - - - - - { - if (!day) { - return; - } - - setRange({ - before: endTime, - after: day.getTime() / 1000 + 1, - }); - }} - /> - - { - const clock = e.target.value; - const [hour, minute, second] = isIOS - ? [...clock.split(":"), "00"] - : clock.split(":"); - - const start = new Date(startTime * 1000); - start.setHours( - parseInt(hour), - parseInt(minute), - parseInt(second ?? 0), - 0, - ); - setRange({ - before: endTime, - after: start.getTime() / 1000, - }); - }} - /> - - - - { - if (!open) { - setEndOpen(false); - } - }} - > - - - - - { - if (!day) { - return; - } - - setRange({ - after: startTime, - before: day.getTime() / 1000, - }); - }} - /> - - { - const clock = e.target.value; - const [hour, minute, second] = isIOS - ? [...clock.split(":"), "00"] - : clock.split(":"); - - const end = new Date(endTime * 1000); - end.setHours( - parseInt(hour), - parseInt(minute), - parseInt(second ?? 0), - 0, - ); - setRange({ - before: end.getTime() / 1000, - after: startTime, - }); - }} - /> - - -
-
- ); -} - type ExportPreviewDialogProps = { camera: string; range?: TimeRange; diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index 84f9051ea..77cb8e3f4 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -2,8 +2,13 @@ import { useCallback, useState } from "react"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Button } from "../ui/button"; import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa"; +import { LuBug } from "react-icons/lu"; import { TimeRange } from "@/types/timeline"; import { ExportContent, ExportPreviewDialog } from "./ExportDialog"; +import { + DebugReplayContent, + SaveDebugReplayOverlay, +} from "./DebugReplayDialog"; import { ExportMode, GeneralFilter } from "@/types/filter"; import ReviewActivityCalendar from "./ReviewActivityCalendar"; import { SelectSeparator } from "../ui/select"; @@ -16,19 +21,32 @@ import { import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import { GeneralFilterContent } from "../filter/ReviewFilterGroup"; import { toast } from "sonner"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; import SaveExportOverlay from "./SaveExportOverlay"; import { isIOS, isMobile } from "react-device-detect"; import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; -type DrawerMode = "none" | "select" | "export" | "calendar" | "filter"; +type DrawerMode = + | "none" + | "select" + | "export" + | "calendar" + | "filter" + | "debug-replay"; -const DRAWER_FEATURES = ["export", "calendar", "filter"] as const; +const DRAWER_FEATURES = [ + "export", + "calendar", + "filter", + "debug-replay", +] as const; export type DrawerFeatures = (typeof DRAWER_FEATURES)[number]; const DEFAULT_DRAWER_FEATURES: DrawerFeatures[] = [ "export", "calendar", "filter", + "debug-replay", ]; type MobileReviewSettingsDrawerProps = { @@ -45,6 +63,10 @@ type MobileReviewSettingsDrawerProps = { recordingsSummary?: RecordingsSummary; allLabels: string[]; allZones: string[]; + debugReplayMode?: ExportMode; + debugReplayRange?: TimeRange; + setDebugReplayMode?: (mode: ExportMode) => void; + setDebugReplayRange?: (range: TimeRange | undefined) => void; onUpdateFilter: (filter: ReviewFilter) => void; setRange: (range: TimeRange | undefined) => void; setMode: (mode: ExportMode) => void; @@ -64,13 +86,26 @@ export default function MobileReviewSettingsDrawer({ recordingsSummary, allLabels, allZones, + debugReplayMode = "none", + debugReplayRange, + setDebugReplayMode = () => {}, + setDebugReplayRange = () => {}, onUpdateFilter, setRange, setMode, setShowExportPreview, }: MobileReviewSettingsDrawerProps) { - const { t } = useTranslation(["views/recording", "components/dialog"]); + const { t } = useTranslation([ + "views/recording", + "components/dialog", + "views/replay", + ]); + const navigate = useNavigate(); const [drawerMode, setDrawerMode] = useState("none"); + const [selectedReplayOption, setSelectedReplayOption] = useState< + "1" | "5" | "custom" | "timeline" + >("1"); + const [isDebugReplayStarting, setIsDebugReplayStarting] = useState(false); // exports @@ -140,6 +175,76 @@ export default function MobileReviewSettingsDrawer({ }); }, [camera, name, range, selectedCaseId, setRange, setName, setMode, t]); + const onStartDebugReplay = useCallback(async () => { + if ( + !debugReplayRange || + debugReplayRange.before <= debugReplayRange.after + ) { + toast.error( + t("dialog.toast.error", { + error: "End time must be after start time", + ns: "views/replay", + }), + { position: "top-center" }, + ); + return; + } + + setIsDebugReplayStarting(true); + + try { + const response = await axios.post("debug_replay/start", { + camera: camera, + start_time: debugReplayRange.after, + end_time: debugReplayRange.before, + }); + + if (response.status === 200) { + toast.success(t("dialog.toast.success", { ns: "views/replay" }), { + position: "top-center", + }); + setDebugReplayMode("none"); + setDebugReplayRange(undefined); + setDrawerMode("none"); + navigate("/replay"); + } + } catch (error) { + const axiosError = error as AxiosError<{ + message?: string; + detail?: string; + }>; + const errorMessage = + axiosError.response?.data?.message || + axiosError.response?.data?.detail || + "Unknown error"; + + if (axiosError.response?.status === 409) { + toast.error(t("dialog.toast.alreadyActive", { ns: "views/replay" }), { + position: "top-center", + }); + } else { + toast.error( + t("dialog.toast.error", { + error: errorMessage, + ns: "views/replay", + }), + { + position: "top-center", + }, + ); + } + } finally { + setIsDebugReplayStarting(false); + } + }, [ + camera, + debugReplayRange, + navigate, + setDebugReplayMode, + setDebugReplayRange, + t, + ]); + // filters const [currentFilter, setCurrentFilter] = useState({ @@ -196,6 +301,26 @@ export default function MobileReviewSettingsDrawer({ {t("filter")} )} + {features.includes("debug-replay") && ( + + )}
); } else if (drawerMode == "export") { @@ -311,6 +436,47 @@ export default function MobileReviewSettingsDrawer({ />
); + } else if (drawerMode == "debug-replay") { + const handleTimeOptionChange = ( + option: "1" | "5" | "custom" | "timeline", + ) => { + setSelectedReplayOption(option); + + if (option === "custom" || option === "timeline") { + return; + } + + const hours = parseInt(option); + const end = latestTime; + const now = new Date(end * 1000); + now.setHours(now.getHours() - hours); + setDebugReplayRange({ after: now.getTime() / 1000, before: end }); + }; + + content = ( + { + setDebugReplayMode("none"); + setDebugReplayRange(undefined); + setDrawerMode("select"); + }} + setRange={setDebugReplayRange} + setMode={(mode) => { + setDebugReplayMode(mode); + + if (mode == "timeline") { + setDrawerMode("none"); + } + }} + /> + ); } return ( @@ -322,6 +488,16 @@ export default function MobileReviewSettingsDrawer({ onCancel={() => setMode("none")} onPreview={() => setShowExportPreview(true)} /> + { + setDebugReplayMode("none"); + setDebugReplayRange(undefined); + }} + /> - + {content} diff --git a/web/src/components/overlay/detail/ObjectPath.tsx b/web/src/components/overlay/detail/ObjectPath.tsx index 201a4d9b3..4af68a0d4 100644 --- a/web/src/components/overlay/detail/ObjectPath.tsx +++ b/web/src/components/overlay/detail/ObjectPath.tsx @@ -17,7 +17,7 @@ type ObjectPathProps = { color?: number[]; width?: number; pointRadius?: number; - imgRef: React.RefObject; + imgRef: React.RefObject; onPointClick?: (index: number) => void; visible?: boolean; }; diff --git a/web/src/components/overlay/detail/TrackingDetails.tsx b/web/src/components/overlay/detail/TrackingDetails.tsx index 03b6dd6c4..3dad95621 100644 --- a/web/src/components/overlay/detail/TrackingDetails.tsx +++ b/web/src/components/overlay/detail/TrackingDetails.tsx @@ -1,6 +1,7 @@ import useSWR from "swr"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useResizeObserver } from "@/hooks/resize-observer"; +import { useFullscreen } from "@/hooks/use-fullscreen"; import { Event } from "@/types/event"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { TrackingDetailsSequence } from "@/types/timeline"; @@ -243,6 +244,8 @@ export function TrackingDetails({ }, [manualOverride, currentTime, annotationOffset]); const containerRef = useRef(null); + const { fullscreen, toggleFullscreen, supportsFullScreen } = + useFullscreen(containerRef); const timelineContainerRef = useRef(null); const rowRefs = useRef<(HTMLDivElement | null)[]>([]); const [_selectedZone, setSelectedZone] = useState(""); @@ -559,14 +562,15 @@ export function TrackingDetails({ visible={true} currentSource={videoSource} hotKeys={false} - supportsFullscreen={false} - fullscreen={false} + supportsFullscreen={supportsFullScreen} + fullscreen={fullscreen} frigateControls={true} onTimeUpdate={handleTimeUpdate} onSeekToTime={handleSeekToTime} onUploadFrame={onUploadFrameToPlus} onPlaying={() => setIsVideoLoading(false)} setFullResolution={setFullResolution} + toggleFullscreen={toggleFullscreen} isDetailMode={true} camera={event.camera} currentTimeOverride={currentTime} diff --git a/web/src/components/overlay/dialog/PlatformAwareDialog.tsx b/web/src/components/overlay/dialog/PlatformAwareDialog.tsx index 5782ba149..963322b9f 100644 --- a/web/src/components/overlay/dialog/PlatformAwareDialog.tsx +++ b/web/src/components/overlay/dialog/PlatformAwareDialog.tsx @@ -22,6 +22,7 @@ import { } from "@/components/ui/sheet"; import { cn } from "@/lib/utils"; import { isMobile } from "react-device-detect"; +import type { JSX } from "react"; import { useRef } from "react"; type PlatformAwareDialogProps = { diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 421c036d1..b91efd84b 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -94,7 +94,7 @@ export default function HlsVideoPlayer({ // playback - const hlsRef = useRef(); + const hlsRef = useRef(undefined); const [useHlsCompat, setUseHlsCompat] = useState(false); const [loadedMetadata, setLoadedMetadata] = useState(false); const [bufferTimeout, setBufferTimeout] = useState(); diff --git a/web/src/components/player/WebRTCPlayer.tsx b/web/src/components/player/WebRTCPlayer.tsx index 37770acc0..147af43ea 100644 --- a/web/src/components/player/WebRTCPlayer.tsx +++ b/web/src/components/player/WebRTCPlayer.tsx @@ -51,10 +51,10 @@ export default function WebRtcPlayer({ // camera states - const pcRef = useRef(); + const pcRef = useRef(undefined); const videoRef = useRef(null); const [bufferTimeout, setBufferTimeout] = useState(); - const videoLoadTimeoutRef = useRef(); + const videoLoadTimeoutRef = useRef(undefined); const PeerConnection = useCallback( async (media: string) => { diff --git a/web/src/components/settings/PolygonCanvas.tsx b/web/src/components/settings/PolygonCanvas.tsx index 30b203bd3..b2241ab84 100644 --- a/web/src/components/settings/PolygonCanvas.tsx +++ b/web/src/components/settings/PolygonCanvas.tsx @@ -10,7 +10,7 @@ import { snapPointToLines } from "@/utils/canvasUtil"; import { usePolygonStates } from "@/hooks/use-polygon-states"; type PolygonCanvasProps = { - containerRef: RefObject; + containerRef: RefObject; camera: string; width: number; height: number; @@ -258,7 +258,7 @@ export function PolygonCanvas({ const updatedPolygons = [...polygons]; const activePolygon = updatedPolygons[activePolygonIndex]; - if (containerRef.current && !activePolygon.isFinished) { + if (containerRef.current && activePolygon && !activePolygon.isFinished) { containerRef.current.style.cursor = "crosshair"; } }; diff --git a/web/src/components/settings/PolygonDrawer.tsx b/web/src/components/settings/PolygonDrawer.tsx index 6b29b5e85..21929e3c5 100644 --- a/web/src/components/settings/PolygonDrawer.tsx +++ b/web/src/components/settings/PolygonDrawer.tsx @@ -18,7 +18,7 @@ import Konva from "konva"; import { Vector2d } from "konva/lib/types"; type PolygonDrawerProps = { - stageRef: RefObject; + stageRef: RefObject; points: number[][]; distances: number[]; isActive: boolean; diff --git a/web/src/components/settings/PolygonItem.tsx b/web/src/components/settings/PolygonItem.tsx index 8fc6b639e..13522f9fc 100644 --- a/web/src/components/settings/PolygonItem.tsx +++ b/web/src/components/settings/PolygonItem.tsx @@ -329,7 +329,7 @@ export default function PolygonItem({
setHoveredPolygonIndex(index)} onMouseLeave={() => setHoveredPolygonIndex(null)} @@ -341,7 +341,7 @@ export default function PolygonItem({ }} >
)} {!isMobile && hoveredPolygonIndex === index && ( -
+
{ if (isSelected) { @@ -46,6 +50,59 @@ export default function EventMenu({ } }; + const handleDebugReplay = useCallback( + (event: Event) => { + setIsStarting(true); + + axios + .post("debug_replay/start", { + camera: event.camera, + start_time: event.start_time, + end_time: event.end_time, + }) + .then((response) => { + if (response.status === 200) { + toast.success(t("dialog.toast.success", { ns: "views/replay" }), { + position: "top-center", + }); + navigate("/replay"); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + + if (error.response?.status === 409) { + toast.error( + t("dialog.toast.alreadyActive", { ns: "views/replay" }), + { + position: "top-center", + closeButton: true, + dismissible: false, + action: ( + + + + ), + }, + ); + } else { + toast.error(t("dialog.toast.error", { error: errorMessage }), { + position: "top-center", + }); + } + }) + .finally(() => { + setIsStarting(false); + }); + }, + [navigate, t], + ); + return ( <> @@ -117,6 +174,19 @@ export default function EventMenu({ {t("itemMenu.findSimilar.label")} )} + {event.has_clip && ( + { + handleDebugReplay(event); + }} + > + {isStarting + ? t("dialog.starting", { ns: "views/replay" }) + : t("itemMenu.debugReplay.label")} + + )} diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx index 89e73c21d..273576d18 100644 --- a/web/src/components/timeline/EventReviewTimeline.tsx +++ b/web/src/components/timeline/EventReviewTimeline.tsx @@ -37,8 +37,8 @@ export type EventReviewTimelineProps = { events: ReviewSegment[]; visibleTimestamps?: number[]; severityType: ReviewSeverity; - timelineRef?: RefObject; - contentRef: RefObject; + timelineRef?: RefObject; + contentRef: RefObject; onHandlebarDraggingChange?: (isDragging: boolean) => void; isZooming: boolean; zoomDirection: TimelineZoomDirection; diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx index 368e0fad5..0a09e2e21 100644 --- a/web/src/components/timeline/EventSegment.tsx +++ b/web/src/components/timeline/EventSegment.tsx @@ -28,7 +28,7 @@ type EventSegmentProps = { minimapStartTime?: number; minimapEndTime?: number; severityType: ReviewSeverity; - contentRef: RefObject; + contentRef: RefObject; setHandlebarTime?: React.Dispatch>; scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void; dense: boolean; diff --git a/web/src/components/timeline/MotionReviewTimeline.tsx b/web/src/components/timeline/MotionReviewTimeline.tsx index 82bde68ed..2796bc968 100644 --- a/web/src/components/timeline/MotionReviewTimeline.tsx +++ b/web/src/components/timeline/MotionReviewTimeline.tsx @@ -42,8 +42,8 @@ export type MotionReviewTimelineProps = { events: ReviewSegment[]; motion_events: MotionData[]; noRecordingRanges?: RecordingSegment[]; - contentRef: RefObject; - timelineRef?: RefObject; + contentRef: RefObject; + timelineRef?: RefObject; onHandlebarDraggingChange?: (isDragging: boolean) => void; dense?: boolean; isZooming: boolean; diff --git a/web/src/components/timeline/ReviewTimeline.tsx b/web/src/components/timeline/ReviewTimeline.tsx index 10fe6f082..21925fa00 100644 --- a/web/src/components/timeline/ReviewTimeline.tsx +++ b/web/src/components/timeline/ReviewTimeline.tsx @@ -20,8 +20,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip"; export type ReviewTimelineProps = { - timelineRef: RefObject; - contentRef: RefObject; + timelineRef: RefObject; + contentRef: RefObject; segmentDuration: number; timelineDuration: number; timelineStartAligned: number; diff --git a/web/src/components/timeline/SummaryTimeline.tsx b/web/src/components/timeline/SummaryTimeline.tsx index d17709bdf..91d3104de 100644 --- a/web/src/components/timeline/SummaryTimeline.tsx +++ b/web/src/components/timeline/SummaryTimeline.tsx @@ -14,7 +14,7 @@ import { import { useTimelineUtils } from "@/hooks/use-timeline-utils"; export type SummaryTimelineProps = { - reviewTimelineRef: React.RefObject; + reviewTimelineRef: React.RefObject; timelineStart: number; timelineEnd: number; segmentDuration: number; diff --git a/web/src/components/timeline/VirtualizedEventSegments.tsx b/web/src/components/timeline/VirtualizedEventSegments.tsx index 5b8b73633..23ad49ffc 100644 --- a/web/src/components/timeline/VirtualizedEventSegments.tsx +++ b/web/src/components/timeline/VirtualizedEventSegments.tsx @@ -10,7 +10,7 @@ import { EventSegment } from "./EventSegment"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; type VirtualizedEventSegmentsProps = { - timelineRef: React.RefObject; + timelineRef: React.RefObject; segments: number[]; events: ReviewSegment[]; segmentDuration: number; @@ -19,7 +19,7 @@ type VirtualizedEventSegmentsProps = { minimapStartTime?: number; minimapEndTime?: number; severityType: ReviewSeverity; - contentRef: React.RefObject; + contentRef: React.RefObject; setHandlebarTime?: React.Dispatch>; dense: boolean; alignStartDateToTimeline: (timestamp: number) => number; diff --git a/web/src/components/timeline/VirtualizedMotionSegments.tsx b/web/src/components/timeline/VirtualizedMotionSegments.tsx index f868e158f..240192911 100644 --- a/web/src/components/timeline/VirtualizedMotionSegments.tsx +++ b/web/src/components/timeline/VirtualizedMotionSegments.tsx @@ -10,7 +10,7 @@ import MotionSegment from "./MotionSegment"; import { ReviewSegment, MotionData } from "@/types/review"; type VirtualizedMotionSegmentsProps = { - timelineRef: React.RefObject; + timelineRef: React.RefObject; segments: number[]; events: ReviewSegment[]; motion_events: MotionData[]; @@ -19,7 +19,7 @@ type VirtualizedMotionSegmentsProps = { showMinimap: boolean; minimapStartTime?: number; minimapEndTime?: number; - contentRef: React.RefObject; + contentRef: React.RefObject; setHandlebarTime?: React.Dispatch>; dense: boolean; motionOnly: boolean; diff --git a/web/src/components/ui/calendar-range.tsx b/web/src/components/ui/calendar-range.tsx index 09641926a..f8ed6c886 100644 --- a/web/src/components/ui/calendar-range.tsx +++ b/web/src/components/ui/calendar-range.tsx @@ -1,3 +1,4 @@ +import type { JSX } from "react"; import { useState, useEffect, useRef } from "react"; import { Button } from "./button"; import { Calendar } from "./calendar"; @@ -124,8 +125,8 @@ export function DateRangePicker({ ); // Refs to store the values of range and rangeCompare when the date picker is opened - const openedRangeRef = useRef(); - const openedRangeCompareRef = useRef(); + const openedRangeRef = useRef(undefined); + const openedRangeCompareRef = useRef(undefined); const [selectedPreset, setSelectedPreset] = useState( undefined, diff --git a/web/src/components/ui/carousel.tsx b/web/src/components/ui/carousel.tsx deleted file mode 100644 index 98b7d6cbd..000000000 --- a/web/src/components/ui/carousel.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import * as React from "react"; -import useEmblaCarousel, { - type UseEmblaCarouselType, -} from "embla-carousel-react"; -import { ArrowLeft, ArrowRight } from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { useTranslation } from "react-i18next"; - -type CarouselApi = UseEmblaCarouselType[1]; -type UseCarouselParameters = Parameters; -type CarouselOptions = UseCarouselParameters[0]; -type CarouselPlugin = UseCarouselParameters[1]; - -type CarouselProps = { - opts?: CarouselOptions; - plugins?: CarouselPlugin; - orientation?: "horizontal" | "vertical"; - setApi?: (api: CarouselApi) => void; -}; - -type CarouselContextProps = { - carouselRef: ReturnType[0]; - api: ReturnType[1]; - scrollPrev: () => void; - scrollNext: () => void; - canScrollPrev: boolean; - canScrollNext: boolean; -} & CarouselProps; - -const CarouselContext = React.createContext(null); - -function useCarousel() { - const context = React.useContext(CarouselContext); - - if (!context) { - throw new Error("useCarousel must be used within a "); - } - - return context; -} - -const Carousel = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & CarouselProps ->( - ( - { - orientation = "horizontal", - opts, - setApi, - plugins, - className, - children, - ...props - }, - ref, - ) => { - const [carouselRef, api] = useEmblaCarousel( - { - ...opts, - axis: orientation === "horizontal" ? "x" : "y", - }, - plugins, - ); - const [canScrollPrev, setCanScrollPrev] = React.useState(false); - const [canScrollNext, setCanScrollNext] = React.useState(false); - - const onSelect = React.useCallback((api: CarouselApi) => { - if (!api) { - return; - } - - setCanScrollPrev(api.canScrollPrev()); - setCanScrollNext(api.canScrollNext()); - }, []); - - const scrollPrev = React.useCallback(() => { - api?.scrollPrev(); - }, [api]); - - const scrollNext = React.useCallback(() => { - api?.scrollNext(); - }, [api]); - - const handleKeyDown = React.useCallback( - (event: React.KeyboardEvent) => { - if (event.key === "ArrowLeft") { - event.preventDefault(); - scrollPrev(); - } else if (event.key === "ArrowRight") { - event.preventDefault(); - scrollNext(); - } - }, - [scrollPrev, scrollNext], - ); - - React.useEffect(() => { - if (!api || !setApi) { - return; - } - - setApi(api); - }, [api, setApi]); - - React.useEffect(() => { - if (!api) { - return; - } - - onSelect(api); - api.on("reInit", onSelect); - api.on("select", onSelect); - - return () => { - api?.off("select", onSelect); - }; - }, [api, onSelect]); - - return ( - -
- {children} -
-
- ); - }, -); -Carousel.displayName = "Carousel"; - -const CarouselContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const { carouselRef, orientation } = useCarousel(); - - return ( -
-
-
- ); -}); -CarouselContent.displayName = "CarouselContent"; - -const CarouselItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const { orientation } = useCarousel(); - - return ( -
- ); -}); -CarouselItem.displayName = "CarouselItem"; - -const CarouselPrevious = React.forwardRef< - HTMLButtonElement, - React.ComponentProps ->(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { t } = useTranslation(["views/explore"]); - const { orientation, scrollPrev, canScrollPrev } = useCarousel(); - - return ( - - ); -}); -CarouselPrevious.displayName = "CarouselPrevious"; - -const CarouselNext = React.forwardRef< - HTMLButtonElement, - React.ComponentProps ->(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { t } = useTranslation(["views/explore"]); - const { orientation, scrollNext, canScrollNext } = useCarousel(); - - return ( - - ); -}); -CarouselNext.displayName = "CarouselNext"; - -export { - type CarouselApi, - Carousel, - CarouselContent, - CarouselItem, - CarouselPrevious, - CarouselNext, -}; diff --git a/web/src/components/ui/form.tsx b/web/src/components/ui/form.tsx index dc6102ea2..aa4db1f36 100644 --- a/web/src/components/ui/form.tsx +++ b/web/src/components/ui/form.tsx @@ -33,9 +33,9 @@ const FormField = < ...props }: ControllerProps) => { return ( - + - + ); }; @@ -77,9 +77,9 @@ const FormItem = React.forwardRef< const id = React.useId(); return ( - +
- + ); }); FormItem.displayName = "FormItem"; diff --git a/web/src/components/ui/icon-wrapper.tsx b/web/src/components/ui/icon-wrapper.tsx index f87d18c62..f9504e0a1 100644 --- a/web/src/components/ui/icon-wrapper.tsx +++ b/web/src/components/ui/icon-wrapper.tsx @@ -1,10 +1,10 @@ import { ForwardedRef, forwardRef } from "react"; import { IconType } from "react-icons"; -interface IconWrapperProps { +interface IconWrapperProps extends React.HTMLAttributes { icon: IconType; className?: string; - [key: string]: any; + disabled?: boolean; } const IconWrapper = forwardRef( diff --git a/web/src/components/ui/sidebar.tsx b/web/src/components/ui/sidebar.tsx index 5a21a6cc1..e7dc78de7 100644 --- a/web/src/components/ui/sidebar.tsx +++ b/web/src/components/ui/sidebar.tsx @@ -141,7 +141,7 @@ const SidebarProvider = React.forwardRef< ); return ( - +
- + ); }, ); diff --git a/web/src/components/ui/toggle-group.tsx b/web/src/components/ui/toggle-group.tsx index 19505f9a4..85710b4a3 100644 --- a/web/src/components/ui/toggle-group.tsx +++ b/web/src/components/ui/toggle-group.tsx @@ -22,9 +22,9 @@ const ToggleGroup = React.forwardRef< className={cn("flex items-center justify-center gap-1", className)} {...props} > - + {children} - + )) diff --git a/web/src/components/ws/WsMessageFeed.tsx b/web/src/components/ws/WsMessageFeed.tsx new file mode 100644 index 000000000..0da86a108 --- /dev/null +++ b/web/src/components/ws/WsMessageFeed.tsx @@ -0,0 +1,608 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { WsFeedMessage } from "@/api/ws"; +import { useWsMessageBuffer } from "@/hooks/use-ws-message-buffer"; +import WsMessageRow from "./WsMessageRow"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { FaEraser, FaFilter, FaPause, FaPlay, FaVideo } from "react-icons/fa"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; +import FilterSwitch from "@/components/filter/FilterSwitch"; +import { isMobile } from "react-device-detect"; +import { isReplayCamera } from "@/utils/cameraUtil"; + +type TopicCategory = + | "events" + | "camera_activity" + | "system" + | "reviews" + | "classification" + | "face_recognition" + | "lpr"; + +const ALL_TOPIC_CATEGORIES: TopicCategory[] = [ + "events", + "reviews", + "classification", + "face_recognition", + "lpr", + "camera_activity", + "system", +]; + +const PRESET_TOPICS: Record> = { + events: new Set(["events", "triggers"]), + reviews: new Set(["reviews"]), + classification: new Set(["tracked_object_update"]), + face_recognition: new Set(["tracked_object_update"]), + lpr: new Set(["tracked_object_update"]), + camera_activity: new Set(["camera_activity", "audio_detections"]), + system: new Set([ + "stats", + "model_state", + "job_state", + "embeddings_reindex_progress", + "audio_transcription_state", + "birdseye_layout", + ]), +}; + +// Maps tracked_object_update payload type to TopicCategory +const TRACKED_UPDATE_TYPE_MAP: Record = { + classification: "classification", + face: "face_recognition", + lpr: "lpr", +}; + +// camera_activity preset also matches topics with camera prefix patterns +const CAMERA_ACTIVITY_TOPIC_PATTERNS = [ + "/motion", + "/audio", + "/detect", + "/recordings", + "/enabled", + "/snapshots", + "/ptz", +]; + +function matchesCategories( + msg: WsFeedMessage, + categories: TopicCategory[] | undefined, +): boolean { + // undefined means all topics + if (!categories) return true; + + const { topic, payload } = msg; + + // Handle tracked_object_update with payload-based sub-categories + if (topic === "tracked_object_update") { + // payload might be a JSON string or a parsed object + let data: unknown = payload; + if (typeof data === "string") { + try { + data = JSON.parse(data); + } catch { + // not valid JSON, fall through + } + } + + const updateType = + data && typeof data === "object" && "type" in data + ? (data as { type: string }).type + : undefined; + + if (updateType && updateType in TRACKED_UPDATE_TYPE_MAP) { + const mappedCategory = TRACKED_UPDATE_TYPE_MAP[updateType]; + return categories.includes(mappedCategory); + } + + // tracked_object_update with other types (e.g. "description") falls under "events" + return categories.includes("events"); + } + + for (const cat of categories) { + const topicSet = PRESET_TOPICS[cat]; + if (topicSet.has(topic)) return true; + + if (cat === "camera_activity") { + if ( + CAMERA_ACTIVITY_TOPIC_PATTERNS.some((pattern) => + topic.includes(pattern), + ) + ) { + return true; + } + } + } + + return false; +} + +type WsMessageFeedProps = { + maxSize?: number; + defaultCamera?: string; + lockedCamera?: string; + showCameraBadge?: boolean; +}; + +export default function WsMessageFeed({ + maxSize = 500, + defaultCamera, + lockedCamera, + showCameraBadge = true, +}: WsMessageFeedProps) { + const { t } = useTranslation(["views/system"]); + const [paused, setPaused] = useState(false); + // undefined = all topics + const [selectedTopics, setSelectedTopics] = useState< + TopicCategory[] | undefined + >(undefined); + // undefined = all cameras + const [selectedCameras, setSelectedCameras] = useState( + () => { + if (lockedCamera) return [lockedCamera]; + if (defaultCamera) return [defaultCamera]; + return undefined; + }, + ); + + const { messages, clear } = useWsMessageBuffer(maxSize, paused, { + cameraFilter: selectedCameras, + }); + + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const availableCameras = useMemo(() => { + if (!config?.cameras) return []; + return Object.keys(config.cameras) + .filter((name) => { + const cam = config.cameras[name]; + return !isReplayCamera(name) && cam.enabled_in_config; + }) + .sort(); + }, [config]); + + const filteredMessages = useMemo(() => { + return messages.filter((msg: WsFeedMessage) => { + if (!matchesCategories(msg, selectedTopics)) return false; + return true; + }); + }, [messages, selectedTopics]); + + // Auto-scroll logic + const scrollContainerRef = useRef(null); + const autoScrollRef = useRef(true); + + const handleScroll = useCallback(() => { + const el = scrollContainerRef.current; + if (!el) return; + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40; + autoScrollRef.current = atBottom; + }, []); + + useEffect(() => { + const el = scrollContainerRef.current; + if (!el || !autoScrollRef.current) return; + el.scrollTop = el.scrollHeight; + }, [filteredMessages.length]); + + return ( +
+ {/* Toolbar */} +
+
+ + + {!lockedCamera && ( + + )} +
+ +
+ + {t("logs.websocket.count", { + count: filteredMessages.length, + })} + + +
+ + + +
+
+
+ + {/* Feed area */} +
+ {filteredMessages.length === 0 ? ( +
+ {t("logs.websocket.empty")} +
+ ) : ( + filteredMessages.map((msg: WsFeedMessage) => ( + + )) + )} +
+
+ ); +} + +// Topic Filter Button + +type TopicFilterButtonProps = { + selectedTopics: TopicCategory[] | undefined; + updateTopicFilter: (topics: TopicCategory[] | undefined) => void; +}; + +function TopicFilterButton({ + selectedTopics, + updateTopicFilter, +}: TopicFilterButtonProps) { + const { t } = useTranslation(["views/system"]); + const [open, setOpen] = useState(false); + const [currentTopics, setCurrentTopics] = useState< + TopicCategory[] | undefined + >(selectedTopics); + + useEffect(() => { + setCurrentTopics(selectedTopics); + }, [selectedTopics]); + + const isFiltered = selectedTopics !== undefined; + + const trigger = ( + + ); + + const content = ( + { + updateTopicFilter(currentTopics); + setOpen(false); + }} + onReset={() => { + setCurrentTopics(undefined); + updateTopicFilter(undefined); + }} + /> + ); + + if (isMobile) { + return ( + { + if (!open) setCurrentTopics(selectedTopics); + setOpen(open); + }} + > + {trigger} + + {content} + + + ); + } + + return ( + { + if (!open) setCurrentTopics(selectedTopics); + setOpen(open); + }} + > + {trigger} + {content} + + ); +} + +type TopicFilterContentProps = { + currentTopics: TopicCategory[] | undefined; + setCurrentTopics: (topics: TopicCategory[] | undefined) => void; + onApply: () => void; + onReset: () => void; +}; + +function TopicFilterContent({ + currentTopics, + setCurrentTopics, + onApply, + onReset, +}: TopicFilterContentProps) { + const { t } = useTranslation(["views/system", "common"]); + + return ( + <> +
+ { + if (isChecked) { + setCurrentTopics(undefined); + } + }} + /> + + {ALL_TOPIC_CATEGORIES.map((cat) => ( + { + if (isChecked) { + const updated = currentTopics ? [...currentTopics, cat] : [cat]; + setCurrentTopics(updated); + } else { + const updated = currentTopics + ? currentTopics.filter((c) => c !== cat) + : []; + if (updated.length === 0) { + setCurrentTopics(undefined); + } else { + setCurrentTopics(updated); + } + } + }} + /> + ))} +
+ +
+ + +
+ + ); +} + +// Camera Filter Button + +type WsCamerasFilterButtonProps = { + allCameras: string[]; + selectedCameras: string[] | undefined; + updateCameraFilter: (cameras: string[] | undefined) => void; +}; + +function WsCamerasFilterButton({ + allCameras, + selectedCameras, + updateCameraFilter, +}: WsCamerasFilterButtonProps) { + const { t } = useTranslation(["views/system", "common"]); + const [open, setOpen] = useState(false); + const [currentCameras, setCurrentCameras] = useState( + selectedCameras, + ); + + useEffect(() => { + setCurrentCameras(selectedCameras); + }, [selectedCameras]); + + const isFiltered = selectedCameras !== undefined; + + const trigger = ( + + ); + + const content = ( + { + updateCameraFilter(currentCameras); + setOpen(false); + }} + onReset={() => { + setCurrentCameras(undefined); + updateCameraFilter(undefined); + }} + /> + ); + + if (isMobile) { + return ( + { + if (!open) setCurrentCameras(selectedCameras); + setOpen(open); + }} + > + {trigger} + + {content} + + + ); + } + + return ( + { + if (!open) setCurrentCameras(selectedCameras); + setOpen(open); + }} + > + {trigger} + {content} + + ); +} + +type WsCamerasFilterContentProps = { + allCameras: string[]; + currentCameras: string[] | undefined; + setCurrentCameras: (cameras: string[] | undefined) => void; + onApply: () => void; + onReset: () => void; +}; + +function WsCamerasFilterContent({ + allCameras, + currentCameras, + setCurrentCameras, + onApply, + onReset, +}: WsCamerasFilterContentProps) { + const { t } = useTranslation(["views/system", "common"]); + + return ( + <> +
+ { + if (isChecked) { + setCurrentCameras(undefined); + } + }} + /> + + {allCameras.map((cam) => ( + { + if (isChecked) { + const updated = currentCameras ? [...currentCameras] : []; + if (!updated.includes(cam)) { + updated.push(cam); + } + setCurrentCameras(updated); + } else { + const updated = currentCameras ? [...currentCameras] : []; + if (updated.length > 1) { + updated.splice(updated.indexOf(cam), 1); + setCurrentCameras(updated); + } + } + }} + /> + ))} +
+ +
+ + +
+ + ); +} diff --git a/web/src/components/ws/WsMessageRow.tsx b/web/src/components/ws/WsMessageRow.tsx new file mode 100644 index 000000000..aa7c89522 --- /dev/null +++ b/web/src/components/ws/WsMessageRow.tsx @@ -0,0 +1,433 @@ +import { memo, useCallback, useState } from "react"; +import { WsFeedMessage } from "@/api/ws"; +import { cn } from "@/lib/utils"; +import { ChevronRight } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { extractCameraName } from "@/utils/wsUtil"; +import { getIconForLabel } from "@/utils/iconUtil"; +import { LuCheck, LuCopy } from "react-icons/lu"; + +type TopicCategory = "events" | "camera_activity" | "system" | "other"; + +const TOPIC_CATEGORY_COLORS: Record = { + events: "bg-blue-500/20 text-blue-700 dark:text-blue-300 border-blue-500/30", + camera_activity: + "bg-green-500/20 text-green-700 dark:text-green-300 border-green-500/30", + system: + "bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-500/30", + other: "bg-gray-500/20 text-gray-700 dark:text-gray-300 border-gray-500/30", +}; + +const EVENT_TYPE_COLORS: Record = { + start: + "bg-green-500/20 text-green-700 dark:text-green-300 border-green-500/30", + update: "bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 border-cyan-500/30", + end: "bg-red-500/20 text-red-700 dark:text-red-300 border-red-500/30", +}; + +const TRACKED_OBJECT_UPDATE_COLORS: Record = { + description: + "bg-amber-500/20 text-amber-700 dark:text-amber-300 border-amber-500/30", + face: "bg-pink-500/20 text-pink-700 dark:text-pink-300 border-pink-500/30", + lpr: "bg-yellow-500/20 text-yellow-700 dark:text-yellow-300 border-yellow-500/30", + classification: + "bg-violet-500/20 text-violet-700 dark:text-violet-300 border-violet-500/30", +}; + +function getEventTypeColor(eventType: string): string { + return ( + EVENT_TYPE_COLORS[eventType] || + "bg-orange-500/20 text-orange-700 dark:text-orange-300 border-orange-500/30" + ); +} + +function getTrackedObjectTypeColor(objectType: string): string { + return ( + TRACKED_OBJECT_UPDATE_COLORS[objectType] || + "bg-orange-500/20 text-orange-700 dark:text-orange-300 border-orange-500/30" + ); +} + +const EVENT_TOPICS = new Set([ + "events", + "reviews", + "tracked_object_update", + "triggers", +]); + +const SYSTEM_TOPICS = new Set([ + "stats", + "model_state", + "job_state", + "embeddings_reindex_progress", + "audio_transcription_state", + "birdseye_layout", +]); + +function getTopicCategory(topic: string): TopicCategory { + if (EVENT_TOPICS.has(topic)) return "events"; + if (SYSTEM_TOPICS.has(topic)) return "system"; + if ( + topic === "camera_activity" || + topic === "audio_detections" || + topic.includes("/motion") || + topic.includes("/audio") || + topic.includes("/detect") || + topic.includes("/recordings") || + topic.includes("/enabled") || + topic.includes("/snapshots") || + topic.includes("/ptz") + ) { + return "camera_activity"; + } + return "other"; +} + +function formatTimestamp(ts: number): string { + const d = new Date(ts); + const hh = String(d.getHours()).padStart(2, "0"); + const mm = String(d.getMinutes()).padStart(2, "0"); + const ss = String(d.getSeconds()).padStart(2, "0"); + const ms = String(d.getMilliseconds()).padStart(3, "0"); + return `${hh}:${mm}:${ss}.${ms}`; +} + +function getPayloadSummary( + topic: string, + payload: unknown, + hideType: boolean = false, +): string { + if (payload === null || payload === undefined) return ""; + + try { + const data = typeof payload === "string" ? JSON.parse(payload) : payload; + + if (typeof data === "object" && data !== null) { + // Topic-specific summary handlers + if (topic === "tracked_object_update") { + return getTrackedObjectUpdateSummary(data); + } + + if ("type" in data && "label" in (data.after || data)) { + const after = data.after || data; + const parts: string[] = []; + + if (!hideType) { + parts.push(`type: ${data.type}`); + } + parts.push(`label: ${after.label || "?"}`); + + // Add sub_label for events topic if present + if (topic === "events" && after.sub_label) { + parts.push(`sub_label: ${after.sub_label}`); + } + + return parts.join(", "); + } + if ("type" in data && "camera" in data) { + if (hideType) { + return `camera: ${data.camera}`; + } + return `type: ${data.type}, camera: ${data.camera}`; + } + const keys = Object.keys(data); + if (keys.length <= 3) { + return keys + .map((k) => { + const v = data[k]; + if (typeof v === "string" || typeof v === "number") { + return `${k}: ${v}`; + } + return k; + }) + .join(", "); + } + return `{${keys.length} keys}`; + } + + const str = String(data); + return str.length > 80 ? str.slice(0, 80) + "…" : str; + } catch { + const str = String(payload); + return str.length > 80 ? str.slice(0, 80) + "…" : str; + } +} + +function getTrackedObjectUpdateSummary(data: unknown): string { + if (typeof data !== "object" || data === null) return ""; + + const obj = data as Record; + const type = obj.type as string; + + switch (type) { + case "description": + return obj.description ? `${obj.description}` : "no description"; + + case "face": { + const name = obj.name as string | undefined; + return name || "unknown"; + } + + case "lpr": { + const name = obj.name as string | undefined; + const plate = obj.plate as string | undefined; + return name || plate || "unknown"; + } + + case "classification": { + const parts: string[] = []; + const model = obj.model as string | undefined; + const subLabel = obj.sub_label as string | undefined; + const attribute = obj.attribute as string | undefined; + + if (model) parts.push(`model: ${model}`); + if (subLabel) parts.push(`sub_label: ${subLabel}`); + if (attribute) parts.push(`attribute: ${attribute}`); + + return parts.length > 0 ? parts.join(", ") : "classification"; + } + + default: + return type || "unknown"; + } +} + +function extractTypeForBadge(payload: unknown): string | null { + if (payload === null || payload === undefined) return null; + + try { + const data = typeof payload === "string" ? JSON.parse(payload) : payload; + + if (typeof data === "object" && data !== null && "type" in data) { + return data.type as string; + } + } catch { + // ignore + } + return null; +} + +function shouldShowTypeBadge(type: string | null): boolean { + if (!type) return false; + return true; +} + +function shouldShowSummary(topic: string): boolean { + // Hide summary for reviews topic + return topic !== "reviews"; +} + +function escapeHtml(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +function highlightJson(value: unknown): string { + // Try to auto-parse JSON strings + if (typeof value === "string") { + try { + const parsed = JSON.parse(value); + if (typeof parsed === "object" && parsed !== null) { + value = parsed; + } + } catch { + // not JSON + } + } + + const raw = JSON.stringify(value, null, 2) ?? String(value); + + // Single regex pass to colorize JSON tokens + return raw.replace( + /("(?:[^"\\]|\\.)*")\s*:|("(?:[^"\\]|\\.)*")|(true|false|null)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, + (match, key: string, str: string, keyword: string, num: string) => { + if (key) { + return `${escapeHtml(key)}:`; + } + if (str) { + const content = escapeHtml(str); + return `${content}`; + } + if (keyword) { + return `${keyword}`; + } + if (num) { + return `${num}`; + } + return match; + }, + ); +} + +function CopyJsonButton({ payload }: { payload: unknown }) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + const text = + typeof payload === "string" + ? payload + : JSON.stringify(payload, null, 2); + navigator.clipboard.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, + [payload], + ); + + return ( + + ); +} + +type WsMessageRowProps = { + message: WsFeedMessage; + showCameraBadge?: boolean; +}; + +const WsMessageRow = memo(function WsMessageRow({ + message, + showCameraBadge = true, +}: WsMessageRowProps) { + const { t } = useTranslation(["views/system"]); + const [expanded, setExpanded] = useState(false); + const category = getTopicCategory(message.topic); + + const cameraName = extractCameraName(message); + + const messageType = extractTypeForBadge(message.payload); + const showTypeBadge = shouldShowTypeBadge(messageType); + + const summary = getPayloadSummary(message.topic, message.payload); + + const eventLabel = (() => { + try { + const data = + typeof message.payload === "string" + ? JSON.parse(message.payload) + : message.payload; + if (typeof data === "object" && data !== null) { + return (data.after?.label as string) || (data.label as string) || null; + } + } catch { + // ignore + } + return null; + })(); + + const parsedPayload = (() => { + try { + return typeof message.payload === "string" + ? JSON.parse(message.payload) + : message.payload; + } catch { + return message.payload; + } + })(); + + const handleToggle = useCallback(() => { + setExpanded((prev) => !prev); + }, []); + + // Determine which color function to use based on topic + const getTypeBadgeColor = (type: string | null) => { + if (!type) return ""; + if (message.topic === "tracked_object_update") { + return getTrackedObjectTypeColor(type); + } + return getEventTypeColor(type); + }; + + return ( +
+
+ + + + {formatTimestamp(message.timestamp)} + + + + {message.topic} + + + {showTypeBadge && messageType && ( + + {messageType} + + )} + + {showCameraBadge && cameraName && ( + + {cameraName} + + )} + + {eventLabel && ( + + {getIconForLabel( + eventLabel, + "object", + "size-3.5 text-primary-variant", + )} + + )} + + {shouldShowSummary(message.topic) && ( + + {summary} + + )} +
+ + {expanded && ( +
+
+ + {t("logs.websocket.expanded.payload")} + + +
+
+        
+ )} +
+ ); +}); + +export default WsMessageRow; diff --git a/web/src/context/auth-context.tsx b/web/src/context/auth-context.tsx index f863c8c06..eae1a8d4a 100644 --- a/web/src/context/auth-context.tsx +++ b/web/src/context/auth-context.tsx @@ -102,9 +102,5 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { axios.get("/logout", { withCredentials: true }); }; - return ( - - {children} - - ); + return {children}; } diff --git a/web/src/context/detail-stream-context.tsx b/web/src/context/detail-stream-context.tsx index 67c06f981..a672a0365 100644 --- a/web/src/context/detail-stream-context.tsx +++ b/web/src/context/detail-stream-context.tsx @@ -125,11 +125,7 @@ export function DetailStreamProvider({ isDetailMode, }; - return ( - - {children} - - ); + return {children}; } // eslint-disable-next-line react-refresh/only-export-components diff --git a/web/src/context/language-provider.tsx b/web/src/context/language-provider.tsx index 88ade5032..5c01052a6 100644 --- a/web/src/context/language-provider.tsx +++ b/web/src/context/language-provider.tsx @@ -77,9 +77,9 @@ export function LanguageProvider({ }; return ( - + {children} - + ); } diff --git a/web/src/context/providers.tsx b/web/src/context/providers.tsx index 310ab1724..b3f4b3e08 100644 --- a/web/src/context/providers.tsx +++ b/web/src/context/providers.tsx @@ -1,6 +1,5 @@ import { ReactNode } from "react"; import { ThemeProvider } from "@/context/theme-provider"; -import { RecoilRoot } from "recoil"; import { ApiProvider } from "@/api"; import { IconContext } from "react-icons"; import { TooltipProvider } from "@/components/ui/tooltip"; @@ -15,25 +14,23 @@ type TProvidersProps = { function providers({ children }: TProvidersProps) { return ( - - - - - - - - - - {children} - - - - - - - - - + + + + + + + + + {children} + + + + + + + + ); } diff --git a/web/src/context/statusbar-provider.tsx b/web/src/context/statusbar-provider.tsx index 03c4752dc..009cb59a1 100644 --- a/web/src/context/statusbar-provider.tsx +++ b/web/src/context/statusbar-provider.tsx @@ -107,10 +107,10 @@ export function StatusBarMessagesProvider({ }, []); return ( - {children} - + ); } diff --git a/web/src/context/streaming-settings-provider.tsx b/web/src/context/streaming-settings-provider.tsx index bcf6da9c5..affa2e4c9 100644 --- a/web/src/context/streaming-settings-provider.tsx +++ b/web/src/context/streaming-settings-provider.tsx @@ -44,7 +44,7 @@ export function StreamingSettingsProvider({ }, [allGroupsStreamingSettings, setPersistedGroupStreamingSettings]); return ( - {children} - + ); } diff --git a/web/src/context/theme-provider.tsx b/web/src/context/theme-provider.tsx index d2be5e7ee..1112bf925 100644 --- a/web/src/context/theme-provider.tsx +++ b/web/src/context/theme-provider.tsx @@ -124,9 +124,9 @@ export function ThemeProvider({ }; return ( - + {children} - + ); } diff --git a/web/src/hooks/resize-observer.ts b/web/src/hooks/resize-observer.ts index 1e174af7e..f5a3e1ec6 100644 --- a/web/src/hooks/resize-observer.ts +++ b/web/src/hooks/resize-observer.ts @@ -31,25 +31,24 @@ export function useResizeObserver(...refs: RefType[]) { [], ); + // Resolve refs to actual DOM elements for use as stable effect dependencies. + // Rest params create a new array each call, but the underlying elements are + // stable DOM nodes, so spreading them into the dep array avoids re-running + // the effect on every render. + const elements = refs.map((ref) => + ref instanceof Window ? document.body : ref.current, + ); + useEffect(() => { - refs.forEach((ref) => { - if (ref instanceof Window) { - resizeObserver.observe(document.body); - } else if (ref.current) { - resizeObserver.observe(ref.current); - } + elements.forEach((el) => { + if (el) resizeObserver.observe(el); }); return () => { - refs.forEach((ref) => { - if (ref instanceof Window) { - resizeObserver.unobserve(document.body); - } else if (ref.current) { - resizeObserver.unobserve(ref.current); - } - }); + resizeObserver.disconnect(); }; - }, [refs, resizeObserver]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...elements, resizeObserver]); if (dimensions.length == refs.length) { return dimensions; diff --git a/web/src/hooks/use-allowed-cameras.ts b/web/src/hooks/use-allowed-cameras.ts index 05941922a..add52fed7 100644 --- a/web/src/hooks/use-allowed-cameras.ts +++ b/web/src/hooks/use-allowed-cameras.ts @@ -2,6 +2,7 @@ import { useContext } from "react"; import { AuthContext } from "@/context/auth-context"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; +import { isReplayCamera } from "@/utils/cameraUtil"; export function useAllowedCameras() { const { auth } = useContext(AuthContext); @@ -14,9 +15,11 @@ export function useAllowedCameras() { auth.user?.role === "admin" || !auth.isAuthenticated // anonymous internal port ) { - // return all cameras - return config?.cameras ? Object.keys(config.cameras) : []; + // return all cameras, excluding replay cameras + return config?.cameras + ? Object.keys(config.cameras).filter((name) => !isReplayCamera(name)) + : []; } - return auth.allowedCameras || []; + return (auth.allowedCameras || []).filter((name) => !isReplayCamera(name)); } diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index b56b39d06..71507c2af 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -31,7 +31,7 @@ type useCameraActivityReturn = { }; export function useCameraActivity( - camera: CameraConfig, + camera: CameraConfig | undefined, revalidateOnFocus: boolean = true, ): useCameraActivityReturn { const { data: config } = useSWR("config", { @@ -52,7 +52,7 @@ export function useCameraActivity( // init camera activity const { payload: updatedCameraState } = useInitialCameraState( - camera.name, + camera?.name ?? "", revalidateOnFocus, ); useEffect(() => { @@ -65,7 +65,7 @@ export function useCameraActivity( const memoizedAudioState = useDeepMemo(updatedAudioState); useEffect(() => { - if (memoizedAudioState) { + if (memoizedAudioState && camera?.name) { setAudioDetections(memoizedAudioState[camera.name]); } }, [memoizedAudioState, camera]); @@ -77,8 +77,8 @@ export function useCameraActivity( [objects], ); - const { payload: cameraEnabled } = useEnabledState(camera.name); - const { payload: detectingMotion } = useMotionActivity(camera.name); + const { payload: cameraEnabled } = useEnabledState(camera?.name ?? ""); + const { payload: detectingMotion } = useMotionActivity(camera?.name ?? ""); const { payload: event } = useFrigateEvents(); const updatedEvent = useDeepMemo(event); @@ -96,7 +96,7 @@ export function useCameraActivity( return; } - if (updatedEvent.after.camera !== camera.name) { + if (!camera?.name || updatedEvent.after.camera !== camera.name) { return; } @@ -163,6 +163,10 @@ export function useCameraActivity( return false; } + if (!camera?.name) { + return false; + } + return ( cameras[camera.name]?.camera_fps == 0 && stats["service"].uptime > 60 ); diff --git a/web/src/hooks/use-deferred-stream-metadata.ts b/web/src/hooks/use-deferred-stream-metadata.ts index 8e68b6a6a..44bd1bb0c 100644 --- a/web/src/hooks/use-deferred-stream-metadata.ts +++ b/web/src/hooks/use-deferred-stream-metadata.ts @@ -5,6 +5,7 @@ import { LiveStreamMetadata } from "@/types/live"; const FETCH_TIMEOUT_MS = 10000; const DEFER_DELAY_MS = 2000; +const EMPTY_METADATA: { [key: string]: LiveStreamMetadata } = {}; /** * Hook that fetches go2rtc stream metadata with deferred loading. @@ -77,7 +78,7 @@ export default function useDeferredStreamMetadata(streamNames: string[]) { return metadata; }, []); - const { data: metadata = {} } = useSWR<{ + const { data: metadata = EMPTY_METADATA } = useSWR<{ [key: string]: LiveStreamMetadata; }>(swrKey, fetcher, { revalidateOnFocus: false, diff --git a/web/src/hooks/use-draggable-element.ts b/web/src/hooks/use-draggable-element.ts index e7f77a6f5..9103a8a37 100644 --- a/web/src/hooks/use-draggable-element.ts +++ b/web/src/hooks/use-draggable-element.ts @@ -8,10 +8,10 @@ import { useTranslation } from "react-i18next"; import useUserInteraction from "./use-user-interaction"; type DraggableElementProps = { - contentRef: React.RefObject; - timelineRef: React.RefObject; - segmentsRef: React.RefObject; - draggableElementRef: React.RefObject; + contentRef: React.RefObject; + timelineRef: React.RefObject; + segmentsRef: React.RefObject; + draggableElementRef: React.RefObject; segmentDuration: number; showDraggableElement: boolean; draggableElementTime?: number; diff --git a/web/src/hooks/use-fullscreen.ts b/web/src/hooks/use-fullscreen.ts index c7082ab5c..2c07fccaf 100644 --- a/web/src/hooks/use-fullscreen.ts +++ b/web/src/hooks/use-fullscreen.ts @@ -78,7 +78,7 @@ function removeEventListeners( } export function useFullscreen( - elementRef: RefObject, + elementRef: RefObject, ) { const [fullscreen, setFullscreen] = useState(false); const [error, setError] = useState(null); diff --git a/web/src/hooks/use-image-loaded.ts b/web/src/hooks/use-image-loaded.ts index b64258c88..df030550a 100644 --- a/web/src/hooks/use-image-loaded.ts +++ b/web/src/hooks/use-image-loaded.ts @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; const useImageLoaded = (): [ - React.RefObject, + React.RefObject, boolean, () => void, ] => { diff --git a/web/src/hooks/use-stats.ts b/web/src/hooks/use-stats.ts index 040dd7b1a..5bddb75ac 100644 --- a/web/src/hooks/use-stats.ts +++ b/web/src/hooks/use-stats.ts @@ -9,6 +9,7 @@ import { useMemo } from "react"; import useSWR from "swr"; import useDeepMemo from "./use-deep-memo"; import { capitalizeAll, capitalizeFirstLetter } from "@/utils/stringUtil"; +import { isReplayCamera } from "@/utils/cameraUtil"; import { useFrigateStats } from "@/api/ws"; import { useTranslation } from "react-i18next"; @@ -16,6 +17,9 @@ import { useTranslation } from "react-i18next"; export default function useStats(stats: FrigateStats | undefined) { const { t } = useTranslation(["views/system"]); const { data: config } = useSWR("config"); + const { data: debugReplayStatus } = useSWR("debug_replay/status", { + revalidateOnFocus: false, + }); const memoizedStats = useDeepMemo(stats); @@ -74,6 +78,11 @@ export default function useStats(stats: FrigateStats | undefined) { return; } + // Skip replay cameras + if (isReplayCamera(name)) { + return; + } + const cameraName = config.cameras?.[name]?.friendly_name ?? name; if (config.cameras[name].enabled && cam["camera_fps"] == 0) { problems.push({ @@ -96,7 +105,15 @@ export default function useStats(stats: FrigateStats | undefined) { ); const cameraName = config?.cameras?.[name]?.friendly_name ?? name; - if (!isNaN(ffmpegAvg) && ffmpegAvg >= CameraFfmpegThreshold.error) { + + // Skip ffmpeg warnings for replay cameras when debug replay is active + if ( + !isNaN(ffmpegAvg) && + ffmpegAvg >= CameraFfmpegThreshold.error && + !( + debugReplayStatus?.active && debugReplayStatus?.replay_camera === name + ) + ) { problems.push({ text: t("stats.ffmpegHighCpuUsage", { camera: capitalizeFirstLetter(capitalizeAll(cameraName)), @@ -119,8 +136,19 @@ export default function useStats(stats: FrigateStats | undefined) { } }); + // Add message if debug replay is active + if (debugReplayStatus?.active) { + problems.push({ + text: t("stats.debugReplayActive", { + defaultValue: "Debug replay session is active", + }), + color: "text-selected", + relevantLink: "/replay", + }); + } + return problems; - }, [config, memoizedStats, t]); + }, [config, memoizedStats, t, debugReplayStatus]); return { potentialProblems }; } diff --git a/web/src/hooks/use-timeline-utils.ts b/web/src/hooks/use-timeline-utils.ts index 9445a5b49..fd3c1648e 100644 --- a/web/src/hooks/use-timeline-utils.ts +++ b/web/src/hooks/use-timeline-utils.ts @@ -3,7 +3,7 @@ import { useCallback } from "react"; export type TimelineUtilsProps = { segmentDuration: number; timelineDuration?: number; - timelineRef?: React.RefObject; + timelineRef?: React.RefObject; }; export function useTimelineUtils({ diff --git a/web/src/hooks/use-timeline-zoom.ts b/web/src/hooks/use-timeline-zoom.ts index aabd6d22b..9c9d4146d 100644 --- a/web/src/hooks/use-timeline-zoom.ts +++ b/web/src/hooks/use-timeline-zoom.ts @@ -11,7 +11,7 @@ type UseTimelineZoomProps = { zoomLevels: ZoomSettings[]; onZoomChange: (newZoomLevel: number) => void; pinchThresholdPercent?: number; - timelineRef: React.RefObject; + timelineRef: React.RefObject; timelineDuration: number; }; diff --git a/web/src/hooks/use-user-interaction.ts b/web/src/hooks/use-user-interaction.ts index 2b59a6d49..2c335eee8 100644 --- a/web/src/hooks/use-user-interaction.ts +++ b/web/src/hooks/use-user-interaction.ts @@ -1,12 +1,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; type UseUserInteractionProps = { - elementRef: React.RefObject; + elementRef: React.RefObject; }; function useUserInteraction({ elementRef }: UseUserInteractionProps) { const [userInteracting, setUserInteracting] = useState(false); - const interactionTimeout = useRef(); + const interactionTimeout = useRef(undefined); const isProgrammaticScroll = useRef(false); const setProgrammaticScroll = useCallback(() => { diff --git a/web/src/hooks/use-video-dimensions.ts b/web/src/hooks/use-video-dimensions.ts index 25b8af350..f7d9ab92d 100644 --- a/web/src/hooks/use-video-dimensions.ts +++ b/web/src/hooks/use-video-dimensions.ts @@ -7,7 +7,7 @@ export type VideoResolutionType = { }; export function useVideoDimensions( - containerRef: React.RefObject, + containerRef: React.RefObject, ) { const [{ width: containerWidth, height: containerHeight }] = useResizeObserver(containerRef); diff --git a/web/src/hooks/use-ws-message-buffer.ts b/web/src/hooks/use-ws-message-buffer.ts new file mode 100644 index 000000000..6f7f06662 --- /dev/null +++ b/web/src/hooks/use-ws-message-buffer.ts @@ -0,0 +1,99 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useWsMessageSubscribe, WsFeedMessage } from "@/api/ws"; +import { extractCameraName } from "@/utils/wsUtil"; + +type UseWsMessageBufferReturn = { + messages: WsFeedMessage[]; + clear: () => void; +}; + +type MessageFilter = { + cameraFilter?: string | string[]; // "all", specific camera name, or array of camera names (undefined in array = all) +}; + +export function useWsMessageBuffer( + maxSize: number = 2000, + paused: boolean = false, + filter?: MessageFilter, +): UseWsMessageBufferReturn { + const bufferRef = useRef([]); + const [version, setVersion] = useState(0); + const pausedRef = useRef(paused); + const filterRef = useRef(filter); + + pausedRef.current = paused; + filterRef.current = filter; + + const batchTimerRef = useRef | null>(null); + const dirtyRef = useRef(false); + + useEffect(() => { + batchTimerRef.current = setInterval(() => { + if (dirtyRef.current) { + dirtyRef.current = false; + setVersion((v) => v + 1); + } + }, 200); + + return () => { + if (batchTimerRef.current) { + clearInterval(batchTimerRef.current); + } + }; + }, []); + + const shouldIncludeMessage = useCallback((msg: WsFeedMessage): boolean => { + const currentFilter = filterRef.current; + if (!currentFilter) return true; + + // Check camera filter + const cf = currentFilter.cameraFilter; + if (cf !== undefined) { + if (Array.isArray(cf)) { + // Array of cameras: include messages matching any camera in the list + const msgCamera = extractCameraName(msg); + if (msgCamera && !cf.includes(msgCamera)) { + return false; + } + } else if (cf !== "all") { + // Single string camera filter + const msgCamera = extractCameraName(msg); + if (msgCamera !== cf) { + return false; + } + } + } + + return true; + }, []); + + useWsMessageSubscribe( + useCallback( + (msg: WsFeedMessage) => { + if (pausedRef.current) return; + if (!shouldIncludeMessage(msg)) return; + + const buf = bufferRef.current; + buf.push(msg); + if (buf.length > maxSize) { + buf.splice(0, buf.length - maxSize); + } + dirtyRef.current = true; + }, + [shouldIncludeMessage, maxSize], + ), + ); + + const clear = useCallback(() => { + bufferRef.current = []; + setVersion((v) => v + 1); + }, []); + + // version is used to trigger re-renders; we spread the buffer + // into a new array so that downstream useMemo dependencies + // see a new reference and recompute. + // eslint-disable-next-line react-hooks/exhaustive-deps + const messages = useMemo(() => [...bufferRef.current], [version]); + + return { messages, clear }; +} diff --git a/web/src/pages/Logs.tsx b/web/src/pages/Logs.tsx index 03b52acc9..b01b4c712 100644 --- a/web/src/pages/Logs.tsx +++ b/web/src/pages/Logs.tsx @@ -35,10 +35,12 @@ import { isIOS, isMobile } from "react-device-detect"; import { isPWA } from "@/utils/isPWA"; import { isInIframe } from "@/utils/isIFrame"; import { useTranslation } from "react-i18next"; +import WsMessageFeed from "@/components/ws/WsMessageFeed"; function Logs() { const { t } = useTranslation(["views/system"]); const [logService, setLogService] = useState("frigate"); + const isWebsocket = logService === "websocket"; const tabsRef = useRef(null); const lazyLogWrapperRef = useRef(null); const [logs, setLogs] = useState([]); @@ -216,6 +218,12 @@ function Logs() { }, [logService, filterSeverity, t]); useEffect(() => { + if (isWebsocket) { + setIsLoading(false); + setLogs([]); + return; + } + setIsLoading(true); setLogs([]); lastFetchedIndexRef.current = -1; @@ -494,116 +502,128 @@ function Logs() { data-nav-item={item} aria-label={`Select ${item}`} > -
{item}
+
+ {item === "websocket" ? t("logs.websocket.label") : item} +
))}
-
- - - -
-
- -
-
-
-
-
- {t("logs.type.label")} + {!isWebsocket && ( +
+ + + +
+ )} +
+ + {isWebsocket ? ( +
+ +
+ ) : ( +
+
+
+
+
+ {t("logs.type.label")} +
+
{t("logs.type.timestamp")}
+
+
+
+ {t("logs.type.tag")} +
+
+
{t("logs.type.message")}
-
- {t("logs.type.tag")} -
-
-
{t("logs.type.message")}
-
-
-
- {isLoading ? ( - - ) : ( - ( - <> - {follow && !logSettings.disableStreaming && ( -
- - - - - {t("logs.tips")} - -
- )} - - } - loading={isLoading} - /> - - )} - /> - )} +
+ {isLoading ? ( + + ) : ( + ( + <> + {follow && !logSettings.disableStreaming && ( +
+ + + + + {t("logs.tips")} + +
+ )} + + } + loading={isLoading} + /> + + )} + /> + )} +
-
+ )}
); } diff --git a/web/src/pages/Replay.tsx b/web/src/pages/Replay.tsx new file mode 100644 index 000000000..187a9a76b --- /dev/null +++ b/web/src/pages/Replay.tsx @@ -0,0 +1,725 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { Trans, useTranslation } from "react-i18next"; +import useSWR from "swr"; +import axios from "axios"; +import { toast } from "sonner"; +import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { useCameraActivity } from "@/hooks/use-camera-activity"; +import { cn } from "@/lib/utils"; +import Heading from "@/components/ui/heading"; +import { Toaster } from "@/components/ui/sonner"; +import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; +import { getIconForLabel } from "@/utils/iconUtil"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { ObjectType } from "@/types/ws"; +import WsMessageFeed from "@/components/ws/WsMessageFeed"; +import { ConfigSectionTemplate } from "@/components/config-form/sections/ConfigSectionTemplate"; + +import { LuExternalLink, LuInfo, LuSettings } from "react-icons/lu"; +import { LuSquare } from "react-icons/lu"; +import { MdReplay } from "react-icons/md"; +import { isDesktop, isMobile } from "react-device-detect"; +import Logo from "@/components/Logo"; +import { Separator } from "@/components/ui/separator"; +import { useDocDomain } from "@/hooks/use-doc-domain"; +import DebugDrawingLayer from "@/components/overlay/DebugDrawingLayer"; +import { IoMdArrowRoundBack } from "react-icons/io"; + +type DebugReplayStatus = { + active: boolean; + replay_camera: string | null; + source_camera: string | null; + start_time: number | null; + end_time: number | null; + live_ready: boolean; +}; + +type DebugOptions = { + bbox: boolean; + timestamp: boolean; + zones: boolean; + mask: boolean; + motion: boolean; + regions: boolean; + paths: boolean; +}; + +const DEFAULT_OPTIONS: DebugOptions = { + bbox: true, + timestamp: false, + zones: false, + mask: false, + motion: true, + regions: false, + paths: false, +}; + +const DEBUG_OPTION_KEYS: (keyof DebugOptions)[] = [ + "bbox", + "timestamp", + "zones", + "mask", + "motion", + "regions", + "paths", +]; + +const DEBUG_OPTION_I18N_KEY: Record = { + bbox: "boundingBoxes", + timestamp: "timestamp", + zones: "zones", + mask: "mask", + motion: "motion", + regions: "regions", + paths: "paths", +}; + +const REPLAY_INIT_SKELETON_TIMEOUT_MS = 8000; + +export default function Replay() { + const { t } = useTranslation(["views/replay", "views/settings", "common"]); + const navigate = useNavigate(); + const { getLocaleDocUrl } = useDocDomain(); + + const { + data: status, + mutate: refreshStatus, + isLoading, + } = useSWR("debug_replay/status", { + refreshInterval: 1000, + }); + const [isInitializing, setIsInitializing] = useState(true); + + // Refresh status immediately on mount to avoid showing "no session" briefly + useEffect(() => { + const initializeStatus = async () => { + await refreshStatus(); + setIsInitializing(false); + }; + initializeStatus(); + }, [refreshStatus]); + + useEffect(() => { + if (status?.live_ready) { + setShowReplayInitSkeleton(false); + } + }, [status?.live_ready]); + + const [options, setOptions] = useState(DEFAULT_OPTIONS); + const [isStopping, setIsStopping] = useState(false); + const [configDialogOpen, setConfigDialogOpen] = useState(false); + + const searchParams = useMemo(() => { + const params = new URLSearchParams(); + for (const key of DEBUG_OPTION_KEYS) { + params.set(key, options[key] ? "1" : "0"); + } + return params; + }, [options]); + + const handleSetOption = useCallback( + (key: keyof DebugOptions, value: boolean) => { + setOptions((prev) => ({ ...prev, [key]: value })); + }, + [], + ); + + const handleStop = useCallback(() => { + setIsStopping(true); + axios + .post("debug_replay/stop") + .then(() => { + toast.success(t("dialog.toast.stopped"), { + position: "top-center", + }); + refreshStatus(); + navigate("/review"); + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("dialog.toast.stopError", { error: errorMessage }), { + position: "top-center", + }); + }) + .finally(() => { + setIsStopping(false); + }); + }, [navigate, refreshStatus, t]); + + // Camera activity for the replay camera + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const replayCameraName = status?.replay_camera ?? ""; + const replayCameraConfig = replayCameraName + ? config?.cameras?.[replayCameraName] + : undefined; + + const { objects } = useCameraActivity(replayCameraConfig); + + const [showReplayInitSkeleton, setShowReplayInitSkeleton] = useState(false); + + // debug draw + const containerRef = useRef(null); + const [debugDraw, setDebugDraw] = useState(false); + + useEffect(() => { + if (!status?.active || !status.replay_camera) { + setShowReplayInitSkeleton(false); + return; + } + + setShowReplayInitSkeleton(true); + + const timeout = window.setTimeout(() => { + setShowReplayInitSkeleton(false); + }, REPLAY_INIT_SKELETON_TIMEOUT_MS); + + return () => { + window.clearTimeout(timeout); + }; + }, [status?.active, status?.replay_camera]); + + useEffect(() => { + if (status?.live_ready) { + setShowReplayInitSkeleton(false); + } + }, [status?.live_ready]); + + // Format time range for display + const timeRangeDisplay = useMemo(() => { + if (!status?.start_time || !status?.end_time) return ""; + const start = new Date(status.start_time * 1000).toLocaleString(); + const end = new Date(status.end_time * 1000).toLocaleString(); + return `${start} — ${end}`; + }, [status]); + + // Show loading state + if (isInitializing || (isLoading && !status?.active)) { + return ( +
+ +
+ ); + } + + // No active session + if (!status?.active) { + return ( +
+ + + {t("page.noSession")} + +

+ {t("page.noSessionDesc")} +

+ +
+ ); + } + + return ( +
+ + + {/* Top bar */} +
+ {isMobile && ( + + )} + +
+ + + + + + + + + + {t("page.confirmStop.title")} + + + {t("page.confirmStop.description")} + + + + + {t("page.confirmStop.cancel")} + + + {t("page.confirmStop.confirm")} + + + + +
+
+ + {/* Main content */} +
+ {/* Camera feed */} +
+ {isStopping ? ( +
+
+ +
+ {t("page.stoppingReplay")} +
+
+
+ ) : ( + status.replay_camera && ( +
+ + {debugDraw && ( + + )} + {showReplayInitSkeleton && ( +
+ +
+ +
+ {t("page.initializingReplay")} +
+
+
+ )} +
+ ) + )} +
+ + {/* Side panel */} +
+
+ + {t("title")} + +
+ {status.source_camera} + {timeRangeDisplay && ( + <> + + {timeRangeDisplay} + + )} +
+
+

{t("description")}

+
+
+ + + + {t("debug.debugging", { ns: "views/settings" })} + + {t("page.objects")} + + {t("websocket_messages")} + + + +
+
+ {DEBUG_OPTION_KEYS.map((key) => { + const i18nKey = DEBUG_OPTION_I18N_KEY[key]; + return ( +
+
+
+ + {(key === "bbox" || + key === "motion" || + key === "regions" || + key === "paths") && ( + + +
+ + + {t("button.info", { ns: "common" })} + +
+
+ + {key === "bbox" ? ( + <> +

+ + {t( + "debug.boundingBoxes.colors.label", + { + ns: "views/settings", + }, + )} + +

+
    + + debug.boundingBoxes.colors.info + +
+ + ) : ( + + {`debug.${i18nKey}.tips`} + + )} +
+
+ )} +
+
+ {t(`debug.${i18nKey}.desc`, { + ns: "views/settings", + })} +
+
+ + handleSetOption(key, checked) + } + /> +
+ ); + })} + {isDesktop && ( + <> + +
+
+
+ + + + +
+ + + {t("button.info", { ns: "common" })} + +
+
+ + {t("debug.objectShapeFilterDrawing.tips", { + ns: "views/settings", + })} +
+ + {t("readTheDocumentation", { + ns: "common", + })} + + +
+
+
+
+
+ {t("debug.objectShapeFilterDrawing.desc", { + ns: "views/settings", + })} +
+
+ { + setDebugDraw(isChecked); + }} + /> +
+ + )} +
+
+
+ + + + +
+ +
+
+
+
+
+ + + + + {t("page.configuration")} + + {t("page.configurationDesc")} + + +
+ + +
+
+
+
+ ); +} + +type ObjectListProps = { + cameraConfig?: CameraConfig; + objects?: ObjectType[]; + config?: FrigateConfig; +}; + +function ObjectList({ cameraConfig, objects, config }: ObjectListProps) { + const { t } = useTranslation(["views/settings"]); + + const colormap = useMemo(() => { + if (!config) { + return; + } + return config.model?.colormap; + }, [config]); + + const getColorForObjectName = useCallback( + (objectName: string) => { + return colormap && colormap[objectName] + ? `rgb(${colormap[objectName][2]}, ${colormap[objectName][1]}, ${colormap[objectName][0]})` + : "rgb(128, 128, 128)"; + }, + [colormap], + ); + + if (!objects || objects.length === 0) { + return ( +
+ {t("debug.noObjects", { ns: "views/settings" })} +
+ ); + } + + return ( +
+ {objects.map((obj: ObjectType) => { + return ( +
+
+
+ {getIconForLabel(obj.label, "object", "size-4 text-white")} +
+
+ {getTranslatedLabel(obj.label)} +
+
+
+
+ + {t("debug.objectShapeFilterDrawing.score", { + ns: "views/settings", + })} + : + + + {obj.score ? (obj.score * 100).toFixed(1) : "-"}% + +
+ {obj.ratio && ( +
+ + {t("debug.objectShapeFilterDrawing.ratio", { + ns: "views/settings", + })} + : + + {obj.ratio.toFixed(2)} +
+ )} + {obj.area && cameraConfig && ( +
+ + {t("debug.objectShapeFilterDrawing.area", { + ns: "views/settings", + })} + : + + + {obj.area} px ( + {( + (obj.area / + (cameraConfig.detect.width * + cameraConfig.detect.height)) * + 100 + ).toFixed(2)} + %) + +
+ )} +
+
+ ); + })} +
+ ); +} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index e686ea241..ea6c3d650 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -40,7 +40,8 @@ import UsersView from "@/views/settings/UsersView"; import RolesView from "@/views/settings/RolesView"; import UiSettingsView from "@/views/settings/UiSettingsView"; import FrigatePlusSettingsView from "@/views/settings/FrigatePlusSettingsView"; -import MaintenanceSettingsView from "@/views/settings/MaintenanceSettingsView"; +import MediaSyncSettingsView from "@/views/settings/MediaSyncSettingsView"; +import RegionGridSettingsView from "@/views/settings/RegionGridSettingsView"; import SystemDetectionModelSettingsView from "@/views/settings/SystemDetectionModelSettingsView"; import { SingleSectionPage, @@ -154,7 +155,8 @@ const allSettingsViews = [ "roles", "notifications", "frigateplus", - "maintenance", + "mediaSync", + "regionGrid", ] as const; type SettingsType = (typeof allSettingsViews)[number]; @@ -444,7 +446,10 @@ const settingsGroups = [ }, { label: "maintenance", - items: [{ key: "maintenance", component: MaintenanceSettingsView }], + items: [ + { key: "mediaSync", component: MediaSyncSettingsView }, + { key: "regionGrid", component: RegionGridSettingsView }, + ], }, ]; @@ -471,6 +476,7 @@ const CAMERA_SELECT_BUTTON_PAGES = [ "masksAndZones", "motionTuner", "triggers", + "regionGrid", ]; const ALLOWED_VIEWS_FOR_VIEWER = ["ui", "debug", "notifications"]; @@ -478,7 +484,8 @@ const ALLOWED_VIEWS_FOR_VIEWER = ["ui", "debug", "notifications"]; const LARGE_BOTTOM_MARGIN_PAGES = [ "masksAndZones", "motionTuner", - "maintenance", + "mediaSync", + "regionGrid", ]; // keys for camera sections diff --git a/web/src/types/log.ts b/web/src/types/log.ts index 2e856f574..bc0555e16 100644 --- a/web/src/types/log.ts +++ b/web/src/types/log.ts @@ -12,7 +12,7 @@ export type LogLine = { content: string; }; -export const logTypes = ["frigate", "go2rtc", "nginx"] as const; +export const logTypes = ["frigate", "go2rtc", "nginx", "websocket"] as const; export type LogType = (typeof logTypes)[number]; export type LogSettingsType = { diff --git a/web/src/utils/cameraUtil.ts b/web/src/utils/cameraUtil.ts index 543605ad0..c47c46f8f 100644 --- a/web/src/utils/cameraUtil.ts +++ b/web/src/utils/cameraUtil.ts @@ -148,3 +148,15 @@ export function detectCameraAudioFeatures( audioOutput: !!audioOutput, }; } + +const REPLAY_CAMERA_PREFIX = "_replay_"; + +/** + * Check if a camera name is a debug replay camera. + * + * @param name - The camera name to check + * @returns true if the camera is a replay camera + */ +export function isReplayCamera(name: string): boolean { + return name.startsWith(REPLAY_CAMERA_PREFIX); +} diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index c4c5afad1..9d2327cb3 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -514,13 +514,18 @@ const mergeSectionConfig = ( export function getSectionConfig( sectionKey: string, - level: "global" | "camera", + level: "global" | "camera" | "replay", ): SectionConfig { const entry = sectionConfigs[sectionKey]; if (!entry) { return {}; } - const overrides = level === "global" ? entry.global : entry.camera; + const overrides = + level === "global" + ? entry.global + : level === "replay" + ? entry.replay + : entry.camera; return mergeSectionConfig(entry.base, overrides); } diff --git a/web/src/utils/wsUtil.ts b/web/src/utils/wsUtil.ts new file mode 100644 index 000000000..411ff1645 --- /dev/null +++ b/web/src/utils/wsUtil.ts @@ -0,0 +1,53 @@ +import { WsFeedMessage } from "@/api/ws"; + +const EVENT_TOPICS = new Set([ + "events", + "reviews", + "tracked_object_update", + "triggers", +]); + +const SYSTEM_TOPICS = new Set([ + "stats", + "model_state", + "job_state", + "embeddings_reindex_progress", + "audio_transcription_state", + "birdseye_layout", +]); + +export function extractCameraName(message: WsFeedMessage): string | null { + // Try extracting from topic pattern: {camera}/motion, {camera}/audio/rms, etc. + const topicParts = message.topic.split("/"); + if ( + topicParts.length >= 2 && + !EVENT_TOPICS.has(message.topic) && + !SYSTEM_TOPICS.has(message.topic) && + message.topic !== "camera_activity" && + message.topic !== "audio_detections" && + message.topic !== "restart" && + message.topic !== "notification_test" + ) { + return topicParts[0]; + } + + // Try extracting from payload + try { + const data = + typeof message.payload === "string" + ? JSON.parse(message.payload) + : message.payload; + + if (typeof data === "object" && data !== null) { + if ("camera" in data) return data.camera as string; + if ("after" in data && data.after?.camera) + return data.after.camera as string; + if ("before" in data && data.before?.camera) + return data.before.camera as string; + } + } catch { + // ignore parse errors + } + + return null; +} diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index 5b875ae5c..1b680967f 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -14,10 +14,9 @@ import React, { useState, } from "react"; import { - ItemCallback, Layout, - Responsive, - WidthProvider, + LayoutItem, + ResponsiveGridLayout as Responsive, } from "react-grid-layout"; import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; @@ -56,7 +55,7 @@ type DraggableGridLayoutProps = { cameras: CameraConfig[]; cameraGroup: string; cameraRef: (node: HTMLElement | null) => void; - containerRef: React.RefObject; + containerRef: React.RefObject; includeBirdseye: boolean; onSelectCamera: (camera: string) => void; windowVisible: boolean; @@ -116,11 +115,8 @@ export default function DraggableGridLayout({ // grid layout - const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []); - - const [gridLayout, setGridLayout, isGridLayoutLoaded] = useUserPersistence< - Layout[] - >(`${cameraGroup}-draggable-layout`); + const [gridLayout, setGridLayout, isGridLayoutLoaded] = + useUserPersistence(`${cameraGroup}-draggable-layout`); const [group] = useUserPersistedOverlayState( "cameraGroup", @@ -158,11 +154,11 @@ export default function DraggableGridLayout({ const [currentIncludeBirdseye, setCurrentIncludeBirdseye] = useState(); const [currentGridLayout, setCurrentGridLayout] = useState< - Layout[] | undefined + Layout | undefined >(); const handleLayoutChange = useCallback( - (currentLayout: Layout[]) => { + (currentLayout: Layout) => { if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) { return; } @@ -174,7 +170,7 @@ export default function DraggableGridLayout({ ); const generateLayout = useCallback( - (baseLayout: Layout[] | undefined) => { + (baseLayout: Layout | undefined) => { if (!isGridLayoutLoaded) { return; } @@ -184,7 +180,7 @@ export default function DraggableGridLayout({ ? ["birdseye", ...cameras.map((camera) => camera?.name || "")] : cameras.map((camera) => camera?.name || ""); - const optionsMap: Layout[] = baseLayout + const optionsMap: LayoutItem[] = baseLayout ? baseLayout.filter((layout) => cameraNames?.includes(layout.i)) : []; @@ -363,12 +359,14 @@ export default function DraggableGridLayout({ ); }, [availableWidth, marginValue]); - const handleResize: ItemCallback = ( - _: Layout[], - oldLayoutItem: Layout, - layoutItem: Layout, - placeholder: Layout, + const handleResize = ( + _layout: Layout, + oldLayoutItem: LayoutItem | null, + layoutItem: LayoutItem | null, + placeholder: LayoutItem | null, ) => { + if (!oldLayoutItem || !layoutItem || !placeholder) return; + const heightDiff = layoutItem.h - oldLayoutItem.h; const widthDiff = layoutItem.w - oldLayoutItem.w; const changeCoef = oldLayoutItem.w / oldLayoutItem.h; @@ -537,8 +535,9 @@ export default function DraggableGridLayout({ currentGroups={groups} activeGroup={group} /> - setShowCircles(false)} onResizeStop={handleLayoutChange} - isDraggable={isEditMode} - isResizable={isEditMode} > {includeBirdseye && birdseyeConfig?.enabled && ( ); })} - + {isDesktop && (
{ - if (!config || cameraGroup == "default") { + if (!config) { return null; } + if (cameraGroup == "default") { + return Object.values(config.cameras) + .filter((cam) => cam.ui.dashboard) + .map((cam) => cam.name) + .join(","); + } + if (includeBirdseye && cameras.length == 0) { return Object.values(config.cameras) .filter((cam) => cam.birdseye.enabled) diff --git a/web/src/views/recording/RecordingView.tsx b/web/src/views/recording/RecordingView.tsx index 75463b1fd..5758728dc 100644 --- a/web/src/views/recording/RecordingView.tsx +++ b/web/src/views/recording/RecordingView.tsx @@ -1,6 +1,8 @@ import ReviewCard from "@/components/card/ReviewCard"; import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup"; +import DebugReplayDialog from "@/components/overlay/DebugReplayDialog"; import ExportDialog from "@/components/overlay/ExportDialog"; +import ActionsDropdown from "@/components/overlay/ActionsDropdown"; import PreviewPlayer, { PreviewController, } from "@/components/player/PreviewPlayer"; @@ -199,6 +201,11 @@ export function RecordingView({ const [exportRange, setExportRange] = useState(); const [showExportPreview, setShowExportPreview] = useState(false); + // debug replay + + const [debugReplayMode, setDebugReplayMode] = useState("none"); + const [debugReplayRange, setDebugReplayRange] = useState(); + // move to next clip const onClipEnded = useCallback(() => { @@ -269,7 +276,7 @@ export function RecordingView({ ); useEffect(() => { - if (scrubbing || exportRange) { + if (scrubbing || exportRange || debugReplayRange) { if ( currentTime > currentTimeRange.before + 60 || currentTime < currentTimeRange.after - 60 @@ -591,6 +598,23 @@ export function RecordingView({ selected={mainCamera} onSelectCamera={onSelectCamera} /> + {isDesktop && ( + { + setDebugReplayRange(range); + + if (range != undefined) { + mainControllerRef.current?.pause(); + } + }} + setMode={setDebugReplayMode} + /> + )} {isDesktop && ( {}} /> )} + {isDesktop && ( + { + const now = new Date(timeRange.before * 1000); + now.setHours(now.getHours() - 1); + setDebugReplayRange({ + after: now.getTime() / 1000, + before: timeRange.before, + }); + setDebugReplayMode("select"); + }} + onExportClick={() => { + const now = new Date(timeRange.before * 1000); + now.setHours(now.getHours() - 1); + setExportRange({ + before: timeRange.before, + after: now.getTime() / 1000, + }); + setExportMode("select"); + }} + /> + )} {isDesktop ? ( { + setDebugReplayRange(range); + + if (range != undefined) { + mainControllerRef.current?.pause(); + } + }} onUpdateFilter={updateFilter} setRange={setExportRange} setMode={setExportMode} @@ -758,7 +814,9 @@ export function RecordingView({ timeRange={currentTimeRange} cameraPreviews={allPreviews ?? []} startTimestamp={playbackStart} - hotKeys={exportMode != "select"} + hotKeys={ + exportMode != "select" && debugReplayMode != "select" + } fullscreen={fullscreen} onTimestampUpdate={(timestamp) => { setPlayerTime(timestamp); @@ -772,7 +830,11 @@ export function RecordingView({ onControllerReady={(controller) => { mainControllerRef.current = controller; }} - isScrubbing={scrubbing || exportMode == "timeline"} + isScrubbing={ + scrubbing || + exportMode == "timeline" || + debugReplayMode == "timeline" + } supportsFullscreen={supportsFullScreen} setFullResolution={setFullResolution} toggleFullscreen={toggleFullscreen} @@ -840,18 +902,29 @@ export function RecordingView({ contentRef={contentRef} mainCamera={mainCamera} timelineType={ - (exportRange == undefined ? timelineType : "timeline") ?? - "timeline" + (exportRange == undefined && debugReplayRange == undefined + ? timelineType + : "timeline") ?? "timeline" } timeRange={timeRange} mainCameraReviewItems={mainCameraReviewItems} activeReviewItem={activeReviewItem} currentTime={currentTime} - exportRange={exportMode == "timeline" ? exportRange : undefined} + exportRange={ + exportMode == "timeline" + ? exportRange + : debugReplayMode == "timeline" + ? debugReplayRange + : undefined + } setCurrentTime={setCurrentTime} manuallySetCurrentTime={manuallySetCurrentTime} setScrubbing={setScrubbing} - setExportRange={setExportRange} + setExportRange={ + debugReplayMode == "timeline" + ? setDebugReplayRange + : setExportRange + } onAnalysisOpen={onAnalysisOpen} isPlaying={mainControllerRef?.current?.isPlaying() ?? false} /> diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index a373acc82..0251c66e2 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -419,7 +419,7 @@ export default function SearchView({ >(); // keep track of previous ref to outline thumbnail when dialog closes - const prevSearchDetailRef = useRef(); + const prevSearchDetailRef = useRef(undefined); useEffect(() => { if (searchDetail === undefined && prevSearchDetailRef.current) { @@ -619,7 +619,9 @@ export default function SearchView({ return (
(itemRefs.current[index] = item)} + ref={(item) => { + itemRefs.current[index] = item; + }} data-start={value.start_time} className="relative flex flex-col rounded-lg" > diff --git a/web/src/views/settings/MasksAndZonesView.tsx b/web/src/views/settings/MasksAndZonesView.tsx index b38a7d430..a80eab572 100644 --- a/web/src/views/settings/MasksAndZonesView.tsx +++ b/web/src/views/settings/MasksAndZonesView.tsx @@ -64,6 +64,9 @@ export default function MasksAndZonesView({ ); const containerRef = useRef(null); const [editPane, setEditPane] = useState(undefined); + const editPaneRef = useRef(editPane); + editPaneRef.current = editPane; + const prevScaledRef = useRef<{ w: number; h: number } | null>(null); const [activeLine, setActiveLine] = useState(); const [snapPoints, setSnapPoints] = useState(false); @@ -350,12 +353,36 @@ export default function MasksAndZonesView({ ...globalObjectMasks, ...objectMasks, ]); - setEditingPolygons([ - ...zones, - ...motionMasks, - ...globalObjectMasks, - ...objectMasks, - ]); + // Don't overwrite editingPolygons during editing – layout shifts + // from switching to the edit pane can trigger a resize which + // recalculates scaledWidth/scaledHeight and would discard the + // newly-added polygon. Instead, rescale existing points + // proportionally. + if (editPaneRef.current === undefined) { + setEditingPolygons([ + ...zones, + ...motionMasks, + ...globalObjectMasks, + ...objectMasks, + ]); + } else if ( + prevScaledRef.current && + (prevScaledRef.current.w !== scaledWidth || + prevScaledRef.current.h !== scaledHeight) + ) { + const prevW = prevScaledRef.current.w; + const prevH = prevScaledRef.current.h; + setEditingPolygons((prev) => + prev.map((poly) => ({ + ...poly, + points: poly.points.map(([x, y]) => [ + (x / prevW) * scaledWidth, + (y / prevH) * scaledHeight, + ]), + })), + ); + } + prevScaledRef.current = { w: scaledWidth, h: scaledHeight }; } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps @@ -431,7 +458,7 @@ export default function MasksAndZonesView({ {cameraConfig && editingPolygons && (
-
+
{editPane == "zone" && ( diff --git a/web/src/views/settings/MaintenanceSettingsView.tsx b/web/src/views/settings/MediaSyncSettingsView.tsx similarity index 99% rename from web/src/views/settings/MaintenanceSettingsView.tsx rename to web/src/views/settings/MediaSyncSettingsView.tsx index df1dd7b90..afbfe3ea1 100644 --- a/web/src/views/settings/MaintenanceSettingsView.tsx +++ b/web/src/views/settings/MediaSyncSettingsView.tsx @@ -15,7 +15,7 @@ import { cn } from "@/lib/utils"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { MediaSyncStats } from "@/types/ws"; -export default function MaintenanceSettingsView() { +export default function MediaSyncSettingsView() { const { t } = useTranslation("views/settings"); const [selectedMediaTypes, setSelectedMediaTypes] = useState([ "all", @@ -103,7 +103,7 @@ export default function MaintenanceSettingsView() {
- + {t("maintenance.sync.title")} diff --git a/web/src/views/settings/RegionGridSettingsView.tsx b/web/src/views/settings/RegionGridSettingsView.tsx new file mode 100644 index 000000000..8ab571a3f --- /dev/null +++ b/web/src/views/settings/RegionGridSettingsView.tsx @@ -0,0 +1,124 @@ +import Heading from "@/components/ui/heading"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Toaster } from "@/components/ui/sonner"; +import { useCallback, useContext, useState } from "react"; +import { useTranslation } from "react-i18next"; +import axios from "axios"; +import { toast } from "sonner"; +import { StatusBarMessagesContext } from "@/context/statusbar-provider"; +import { cn } from "@/lib/utils"; + +type RegionGridSettingsViewProps = { + selectedCamera: string; +}; + +export default function RegionGridSettingsView({ + selectedCamera, +}: RegionGridSettingsViewProps) { + const { t } = useTranslation("views/settings"); + const { addMessage } = useContext(StatusBarMessagesContext)!; + const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const [isClearing, setIsClearing] = useState(false); + const [imageKey, setImageKey] = useState(0); + + const handleClear = useCallback(async () => { + setIsClearing(true); + + try { + await axios.delete(`${selectedCamera}/region_grid`); + toast.success(t("maintenance.regionGrid.clearSuccess"), { + position: "top-center", + }); + setImageKey((prev) => prev + 1); + addMessage( + "region_grid_restart", + t("maintenance.regionGrid.restartRequired"), + undefined, + "region_grid_settings", + ); + } catch { + toast.error(t("maintenance.regionGrid.clearError"), { + position: "top-center", + }); + } finally { + setIsClearing(false); + setIsConfirmOpen(false); + } + }, [selectedCamera, t, addMessage]); + + return ( + <> +
+ +
+ + {t("maintenance.regionGrid.title")} + + +
+
+

{t("maintenance.regionGrid.desc")}

+
+
+ +
+ {t("maintenance.regionGrid.title")} +
+ +
+ +
+
+
+ + + + + + {t("maintenance.regionGrid.clearConfirmTitle")} + + + {t("maintenance.regionGrid.clearConfirmDesc")} + + + + + {t("button.cancel", { ns: "common" })} + + + {t("maintenance.regionGrid.clear")} + + + + + + ); +}