diff --git a/docs/docs/configuration/motion_detection.md b/docs/docs/configuration/motion_detection.md index c22491fd0..53e63272a 100644 --- a/docs/docs/configuration/motion_detection.md +++ b/docs/docs/configuration/motion_detection.md @@ -38,7 +38,6 @@ Remember that motion detection is just used to determine when object detection s The threshold value dictates how much of a change in a pixels luminance is required to be considered motion. ```yaml -# default threshold value motion: # Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below) # Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. @@ -53,7 +52,6 @@ Watching the motion boxes in the debug view, increase the threshold until you on ### Contour Area ```yaml -# default contour_area value motion: # Optional: Minimum size in pixels in the resized motion image that counts as motion (default: shown below) # Increasing this value will prevent smaller areas of motion from being detected. Decreasing will @@ -81,27 +79,49 @@ However, if the preferred day settings do not work well at night it is recommend ## Tuning For Large Changes In Motion +### Lightning Threshold + ```yaml -# default lightning_threshold: motion: - # Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection - # needs to recalibrate. (default: shown below) - # Increasing this value will make motion detection more likely to consider lightning or ir mode changes as valid motion. - # Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching - # a doorbell camera. + # Optional: The percentage of the image used to detect lightning or + # other substantial changes where motion detection needs to + # recalibrate. (default: shown below) + # Increasing this value will make motion detection more likely + # to consider lightning or IR mode changes as valid motion. + # Decreasing this value will make motion detection more likely + # to ignore large amounts of motion such as a person + # approaching a doorbell camera. lightning_threshold: 0.8 ``` +Large changes in motion like PTZ moves and camera switches between Color and IR mode should result in a pause in object detection. `lightning_threshold` defines the percentage of the image used to detect these substantial changes. Increasing this value makes motion detection more likely to treat large changes (like IR mode switches) as valid motion. Decreasing it makes motion detection more likely to ignore large amounts of motion, such as a person approaching a doorbell camera. + +Note that `lightning_threshold` does **not** stop motion-based recordings from being saved — it only prevents additional motion analysis after the threshold is exceeded, reducing false positive object detections during high-motion periods (e.g. storms or PTZ sweeps) without interfering with recordings. + :::warning -Some cameras like doorbell cameras may have missed detections when someone walks directly in front of the camera and the lightning_threshold causes motion detection to be re-calibrated. In this case, it may be desirable to increase the `lightning_threshold` to ensure these objects are not missed. +Some cameras, like doorbell cameras, may have missed detections when someone walks directly in front of the camera and the `lightning_threshold` causes motion detection to recalibrate. In this case, it may be desirable to increase the `lightning_threshold` to ensure these objects are not missed. ::: -:::note +### Skip Motion On Large Scene Changes -Lightning threshold does not stop motion based recordings from being saved. +```yaml +motion: + # Optional: Fraction of the frame that must change in a single update + # before Frigate will completely ignore any motion in that frame. + # Values range between 0.0 and 1.0, leave unset (null) to disable. + # Setting this to 0.7 would cause Frigate to **skip** reporting + # motion boxes when more than 70% of the image appears to change + # (e.g. during lightning storms, IR/color mode switches, or other + # sudden lighting events). + skip_motion_threshold: 0.7 +``` + +This option is handy when you want to prevent large transient changes from triggering recordings or object detection. It differs from `lightning_threshold` because it completely suppresses motion instead of just forcing a recalibration. + +:::warning + +When the skip threshold is exceeded, **no motion is reported** for that frame, meaning **nothing is recorded** for that frame. That means you can miss something important, like a PTZ camera auto-tracking an object or activity while the camera is moving. If you prefer to guarantee that every frame is saved, leave this unset and accept occasional recordings containing scene noise — they typically only take up a few megabytes and are quick to scan in the timeline UI. ::: - -Large changes in motion like PTZ moves and camera switches between Color and IR mode should result in a pause in object detection. This is done via the `lightning_threshold` configuration. It is defined as the percentage of the image used to detect lightning or other substantial changes where motion detection needs to recalibrate. Increasing this value will make motion detection more likely to consider lightning or IR mode changes as valid motion. Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching a doorbell camera. diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 3efc4f0ed..cac508195 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -480,12 +480,16 @@ motion: # Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. # The value should be between 1 and 255. threshold: 30 - # Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection - # needs to recalibrate. (default: shown below) + # Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection needs + # to recalibrate and motion checks stop for that frame. Recordings are unaffected. (default: shown below) # Increasing this value will make motion detection more likely to consider lightning or ir mode changes as valid motion. - # Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching - # a doorbell camera. + # Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching a doorbell camera. lightning_threshold: 0.8 + # Optional: Fraction of the frame that must change in a single update before motion boxes are completely + # ignored. Values range between 0.0 and 1.0. When exceeded, no motion boxes are reported and **no motion + # recording** is created for that frame. Leave unset (null) to disable this feature. Use with care on PTZ + # cameras or other situations where you require guaranteed frame capture. + skip_motion_threshold: None # Optional: Minimum size in pixels in the resized motion image that counts as motion (default: shown below) # Increasing this value will prevent smaller areas of motion from being detected. Decreasing will # make motion detection more sensitive to smaller moving objects. 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/defs/tags.py b/frigate/api/defs/tags.py index 3aaaa59ef..c6f37b67f 100644 --- a/frigate/api/defs/tags.py +++ b/frigate/api/defs/tags.py @@ -11,6 +11,7 @@ class Tags(Enum): classification = "Classification" logs = "Logs" media = "Media" + motion_search = "Motion Search" notifications = "Notifications" preview = "Preview" recordings = "Recordings" diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 1e8c408e6..0a731bcee 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -22,6 +22,7 @@ from frigate.api import ( event, export, media, + motion_search, notification, preview, record, @@ -135,6 +136,7 @@ def create_fastapi_app( app.include_router(export.router) app.include_router(event.router) app.include_router(media.router) + app.include_router(motion_search.router) app.include_router(record.router) app.include_router(debug_replay.router) # App Properties diff --git a/frigate/api/media.py b/frigate/api/media.py index 0028c6152..903cf60c0 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/api/motion_search.py b/frigate/api/motion_search.py new file mode 100644 index 000000000..09bf8026d --- /dev/null +++ b/frigate/api/motion_search.py @@ -0,0 +1,292 @@ +"""Motion search API for detecting changes within a region of interest.""" + +import logging +from typing import Any, List, Optional + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field + +from frigate.api.auth import require_camera_access +from frigate.api.defs.tags import Tags +from frigate.jobs.motion_search import ( + cancel_motion_search_job, + get_motion_search_job, + start_motion_search_job, +) +from frigate.types import JobStatusTypesEnum + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.motion_search]) + + +class MotionSearchRequest(BaseModel): + """Request body for motion search.""" + + start_time: float = Field(description="Start timestamp for the search range") + end_time: float = Field(description="End timestamp for the search range") + polygon_points: List[List[float]] = Field( + description="List of [x, y] normalized coordinates (0-1) defining the ROI polygon" + ) + threshold: int = Field( + default=30, + ge=1, + le=255, + description="Pixel difference threshold (1-255)", + ) + min_area: float = Field( + default=5.0, + ge=0.1, + le=100.0, + description="Minimum change area as a percentage of the ROI", + ) + frame_skip: int = Field( + default=5, + ge=1, + le=30, + description="Process every Nth frame (1=all frames, 5=every 5th frame)", + ) + parallel: bool = Field( + default=False, + description="Enable parallel scanning across segments", + ) + max_results: int = Field( + default=25, + ge=1, + le=200, + description="Maximum number of search results to return", + ) + + +class MotionSearchResult(BaseModel): + """A single search result with timestamp and change info.""" + + timestamp: float = Field(description="Timestamp where change was detected") + change_percentage: float = Field(description="Percentage of ROI area that changed") + + +class MotionSearchMetricsResponse(BaseModel): + """Metrics collected during motion search execution.""" + + segments_scanned: int = 0 + segments_processed: int = 0 + metadata_inactive_segments: int = 0 + heatmap_roi_skip_segments: int = 0 + fallback_full_range_segments: int = 0 + frames_decoded: int = 0 + wall_time_seconds: float = 0.0 + segments_with_errors: int = 0 + + +class MotionSearchStartResponse(BaseModel): + """Response when motion search job starts.""" + + success: bool + message: str + job_id: str + + +class MotionSearchStatusResponse(BaseModel): + """Response containing job status and results.""" + + success: bool + message: str + status: str # "queued", "running", "success", "failed", or "cancelled" + results: Optional[List[MotionSearchResult]] = None + total_frames_processed: Optional[int] = None + error_message: Optional[str] = None + metrics: Optional[MotionSearchMetricsResponse] = None + + +@router.post( + "/{camera_name}/search/motion", + response_model=MotionSearchStartResponse, + dependencies=[Depends(require_camera_access)], + summary="Start motion search job", + description="""Starts an asynchronous search for significant motion changes within + a user-defined Region of Interest (ROI) over a specified time range. Returns a job_id + that can be used to poll for results.""", +) +async def start_motion_search( + request: Request, + camera_name: str, + body: MotionSearchRequest, +): + """Start an async motion search job.""" + config = request.app.frigate_config + + if camera_name not in config.cameras: + return JSONResponse( + content={"success": False, "message": f"Camera {camera_name} not found"}, + status_code=404, + ) + + # Validate polygon has at least 3 points + if len(body.polygon_points) < 3: + return JSONResponse( + content={ + "success": False, + "message": "Polygon must have at least 3 points", + }, + status_code=400, + ) + + # Validate time range + if body.start_time >= body.end_time: + return JSONResponse( + content={ + "success": False, + "message": "Start time must be before end time", + }, + status_code=400, + ) + + # Start the job using the jobs module + job_id = start_motion_search_job( + config=config, + camera_name=camera_name, + start_time=body.start_time, + end_time=body.end_time, + polygon_points=body.polygon_points, + threshold=body.threshold, + min_area=body.min_area, + frame_skip=body.frame_skip, + parallel=body.parallel, + max_results=body.max_results, + ) + + return JSONResponse( + content={ + "success": True, + "message": "Search job started", + "job_id": job_id, + } + ) + + +@router.get( + "/{camera_name}/search/motion/{job_id}", + response_model=MotionSearchStatusResponse, + dependencies=[Depends(require_camera_access)], + summary="Get motion search job status", + description="Returns the status and results (if complete) of a motion search job.", +) +async def get_motion_search_status_endpoint( + request: Request, + camera_name: str, + job_id: str, +): + """Get the status of a motion search job.""" + config = request.app.frigate_config + + if camera_name not in config.cameras: + return JSONResponse( + content={"success": False, "message": f"Camera {camera_name} not found"}, + status_code=404, + ) + + job = get_motion_search_job(job_id) + if not job: + return JSONResponse( + content={"success": False, "message": "Job not found"}, + status_code=404, + ) + + api_status = job.status + + # Build response content + response_content: dict[str, Any] = { + "success": api_status != JobStatusTypesEnum.failed, + "status": api_status, + } + + if api_status == JobStatusTypesEnum.failed: + response_content["message"] = job.error_message or "Search failed" + response_content["error_message"] = job.error_message + elif api_status == JobStatusTypesEnum.cancelled: + response_content["message"] = "Search cancelled" + response_content["total_frames_processed"] = job.total_frames_processed + elif api_status == JobStatusTypesEnum.success: + response_content["message"] = "Search complete" + if job.results: + response_content["results"] = job.results.get("results", []) + response_content["total_frames_processed"] = job.results.get( + "total_frames_processed", job.total_frames_processed + ) + else: + response_content["results"] = [] + response_content["total_frames_processed"] = job.total_frames_processed + else: + response_content["message"] = "Job processing" + response_content["total_frames_processed"] = job.total_frames_processed + # Include partial results if available (streaming) + if job.results: + response_content["results"] = job.results.get("results", []) + response_content["total_frames_processed"] = job.results.get( + "total_frames_processed", job.total_frames_processed + ) + + # Include metrics if available + if job.metrics: + response_content["metrics"] = job.metrics.to_dict() + + return JSONResponse(content=response_content) + + +@router.post( + "/{camera_name}/search/motion/{job_id}/cancel", + dependencies=[Depends(require_camera_access)], + summary="Cancel motion search job", + description="Cancels an active motion search job if it is still processing.", +) +async def cancel_motion_search_endpoint( + request: Request, + camera_name: str, + job_id: str, +): + """Cancel an active motion search job.""" + config = request.app.frigate_config + + if camera_name not in config.cameras: + return JSONResponse( + content={"success": False, "message": f"Camera {camera_name} not found"}, + status_code=404, + ) + + job = get_motion_search_job(job_id) + if not job: + return JSONResponse( + content={"success": False, "message": "Job not found"}, + status_code=404, + ) + + # Check if already finished + api_status = job.status + if api_status not in (JobStatusTypesEnum.queued, JobStatusTypesEnum.running): + return JSONResponse( + content={ + "success": True, + "message": "Job already finished", + "status": api_status, + } + ) + + # Request cancellation + cancelled = cancel_motion_search_job(job_id) + if cancelled: + return JSONResponse( + content={ + "success": True, + "message": "Search cancelled", + "status": "cancelled", + } + ) + + return JSONResponse( + content={ + "success": False, + "message": "Failed to cancel job", + }, + status_code=500, + ) diff --git a/frigate/api/record.py b/frigate/api/record.py index 789aa4a80..6eeb9fbe6 100644 --- a/frigate/api/record.py +++ b/frigate/api/record.py @@ -261,6 +261,7 @@ async def recordings( Recordings.segment_size, Recordings.motion, Recordings.objects, + Recordings.motion_heatmap, Recordings.duration, ) .where( diff --git a/frigate/app.py b/frigate/app.py index 7c8ac47e3..0add3e3b8 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -51,6 +51,7 @@ from frigate.embeddings import EmbeddingProcess, EmbeddingsContext from frigate.events.audio import AudioProcessor from frigate.events.cleanup import EventCleanup from frigate.events.maintainer import EventProcessor +from frigate.jobs.motion_search import stop_all_motion_search_jobs from frigate.log import _stop_logging from frigate.models import ( Event, @@ -599,6 +600,9 @@ class FrigateApp: # used by the docker healthcheck Path("/dev/shm/.frigate-is-stopping").touch() + # Cancel any running motion search jobs before setting stop_event + stop_all_motion_search_jobs() + self.stop_event.set() # set an end_time on entries without an end_time before exiting diff --git a/frigate/camera/activity_manager.py b/frigate/camera/activity_manager.py index 70c867073..3f229e490 100644 --- a/frigate/camera/activity_manager.py +++ b/frigate/camera/activity_manager.py @@ -236,6 +236,7 @@ class AudioActivityManager: None, "audio", {}, + None, ), EventMetadataTypeEnum.manual_event_create.value, ) diff --git a/frigate/config/camera/motion.py b/frigate/config/camera/motion.py index b6877693b..ebba8613c 100644 --- a/frigate/config/camera/motion.py +++ b/frigate/config/camera/motion.py @@ -24,10 +24,17 @@ class MotionConfig(FrigateBaseModel): lightning_threshold: float = Field( default=0.8, title="Lightning threshold", - description="Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0).", + description="Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events.", ge=0.3, le=1.0, ) + skip_motion_threshold: Optional[float] = Field( + default=None, + title="Skip motion threshold", + description="If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.", + ge=0.0, + le=1.0, + ) improve_contrast: bool = Field( default=True, title="Improve contrast", 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/jobs/motion_search.py b/frigate/jobs/motion_search.py new file mode 100644 index 000000000..d7c8f8fbc --- /dev/null +++ b/frigate/jobs/motion_search.py @@ -0,0 +1,864 @@ +"""Motion search job management with background execution and parallel verification.""" + +import logging +import os +import threading +from concurrent.futures import Future, ThreadPoolExecutor, as_completed +from dataclasses import asdict, dataclass, field +from datetime import datetime +from typing import Any, Optional + +import cv2 +import numpy as np + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import UPDATE_JOB_STATE +from frigate.jobs.job import Job +from frigate.jobs.manager import ( + get_job_by_id, + set_current_job, +) +from frigate.models import Recordings +from frigate.types import JobStatusTypesEnum + +logger = logging.getLogger(__name__) + +# Constants +HEATMAP_GRID_SIZE = 16 + + +@dataclass +class MotionSearchMetrics: + """Metrics collected during motion search execution.""" + + segments_scanned: int = 0 + segments_processed: int = 0 + metadata_inactive_segments: int = 0 + heatmap_roi_skip_segments: int = 0 + fallback_full_range_segments: int = 0 + frames_decoded: int = 0 + wall_time_seconds: float = 0.0 + segments_with_errors: int = 0 + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary.""" + return asdict(self) + + +@dataclass +class MotionSearchResult: + """A single search result with timestamp and change info.""" + + timestamp: float + change_percentage: float + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary.""" + return asdict(self) + + +@dataclass +class MotionSearchJob(Job): + """Job state for motion search operations.""" + + job_type: str = "motion_search" + camera: str = "" + start_time_range: float = 0.0 + end_time_range: float = 0.0 + polygon_points: list[list[float]] = field(default_factory=list) + threshold: int = 30 + min_area: float = 5.0 + frame_skip: int = 5 + parallel: bool = False + max_results: int = 25 + + # Track progress + total_frames_processed: int = 0 + + # Metrics for observability + metrics: Optional[MotionSearchMetrics] = None + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for WebSocket transmission.""" + d = asdict(self) + if self.metrics: + d["metrics"] = self.metrics.to_dict() + return d + + +def create_polygon_mask( + polygon_points: list[list[float]], frame_width: int, frame_height: int +) -> np.ndarray: + """Create a binary mask from normalized polygon coordinates.""" + motion_points = np.array( + [[int(p[0] * frame_width), int(p[1] * frame_height)] for p in polygon_points], + dtype=np.int32, + ) + mask = np.zeros((frame_height, frame_width), dtype=np.uint8) + cv2.fillPoly(mask, [motion_points], 255) + return mask + + +def compute_roi_bbox_normalized( + polygon_points: list[list[float]], +) -> tuple[float, float, float, float]: + """Compute the bounding box of the ROI in normalized coordinates (0-1). + + Returns (x_min, y_min, x_max, y_max) in normalized coordinates. + """ + if not polygon_points: + return (0.0, 0.0, 1.0, 1.0) + + x_coords = [p[0] for p in polygon_points] + y_coords = [p[1] for p in polygon_points] + return (min(x_coords), min(y_coords), max(x_coords), max(y_coords)) + + +def heatmap_overlaps_roi( + heatmap: dict[str, int], roi_bbox: tuple[float, float, float, float] +) -> bool: + """Check if a sparse motion heatmap has any overlap with the ROI bounding box. + + Args: + heatmap: Sparse dict mapping cell index (str) to intensity (1-255). + roi_bbox: (x_min, y_min, x_max, y_max) in normalized coordinates (0-1). + + Returns: + True if there is overlap (any active cell in the ROI region). + """ + if not isinstance(heatmap, dict): + # Invalid heatmap, assume overlap to be safe + return True + + x_min, y_min, x_max, y_max = roi_bbox + + # Convert normalized coordinates to grid cells (0-15) + grid_x_min = max(0, int(x_min * HEATMAP_GRID_SIZE)) + grid_y_min = max(0, int(y_min * HEATMAP_GRID_SIZE)) + grid_x_max = min(HEATMAP_GRID_SIZE - 1, int(x_max * HEATMAP_GRID_SIZE)) + grid_y_max = min(HEATMAP_GRID_SIZE - 1, int(y_max * HEATMAP_GRID_SIZE)) + + # Check each cell in the ROI bbox + for y in range(grid_y_min, grid_y_max + 1): + for x in range(grid_x_min, grid_x_max + 1): + idx = str(y * HEATMAP_GRID_SIZE + x) + if idx in heatmap: + return True + + return False + + +def segment_passes_activity_gate(recording: Recordings) -> bool: + """Check if a segment passes the activity gate. + + Returns True if any of motion, objects, or regions is non-zero/non-null. + Returns True if all are null (old segments without data). + """ + motion = recording.motion + objects = recording.objects + regions = recording.regions + + # Old segments without metadata - pass through (conservative) + if motion is None and objects is None and regions is None: + return True + + # Pass if any activity indicator is positive + return bool(motion) or bool(objects) or bool(regions) + + +def segment_passes_heatmap_gate( + recording: Recordings, roi_bbox: tuple[float, float, float, float] +) -> bool: + """Check if a segment passes the heatmap overlap gate. + + Returns True if: + - No heatmap is stored (old segments). + - The heatmap overlaps with the ROI bbox. + """ + heatmap = getattr(recording, "motion_heatmap", None) + if heatmap is None: + # No heatmap stored, fall back to activity gate + return True + + return heatmap_overlaps_roi(heatmap, roi_bbox) + + +class MotionSearchRunner(threading.Thread): + """Thread-based runner for motion search jobs with parallel verification.""" + + def __init__( + self, + job: MotionSearchJob, + config: FrigateConfig, + cancel_event: threading.Event, + ) -> None: + super().__init__(daemon=True, name=f"motion_search_{job.id}") + self.job = job + self.config = config + self.cancel_event = cancel_event + self.internal_stop_event = threading.Event() + self.requestor = InterProcessRequestor() + self.metrics = MotionSearchMetrics() + self.job.metrics = self.metrics + + # Worker cap: min(4, cpu_count) + cpu_count = os.cpu_count() or 1 + self.max_workers = min(4, cpu_count) + + def run(self) -> None: + """Execute the motion search job.""" + try: + self.job.status = JobStatusTypesEnum.running + self.job.start_time = datetime.now().timestamp() + self._broadcast_status() + + results = self._execute_search() + + if self.cancel_event.is_set(): + self.job.status = JobStatusTypesEnum.cancelled + else: + self.job.status = JobStatusTypesEnum.success + self.job.results = { + "results": [r.to_dict() for r in results], + "total_frames_processed": self.job.total_frames_processed, + } + + self.job.end_time = datetime.now().timestamp() + self.metrics.wall_time_seconds = self.job.end_time - self.job.start_time + self.job.metrics = self.metrics + + logger.debug( + "Motion search job %s completed: status=%s, results=%d, frames=%d", + self.job.id, + self.job.status, + len(results), + self.job.total_frames_processed, + ) + self._broadcast_status() + + except Exception as e: + logger.exception("Motion search job %s failed: %s", self.job.id, e) + self.job.status = JobStatusTypesEnum.failed + self.job.error_message = str(e) + self.job.end_time = datetime.now().timestamp() + self.metrics.wall_time_seconds = self.job.end_time - ( + self.job.start_time or 0 + ) + self.job.metrics = self.metrics + self._broadcast_status() + + finally: + if self.requestor: + self.requestor.stop() + + def _broadcast_status(self) -> None: + """Broadcast job status update via IPC to WebSocket subscribers.""" + if self.job.status == JobStatusTypesEnum.running and self.job.start_time: + self.metrics.wall_time_seconds = ( + datetime.now().timestamp() - self.job.start_time + ) + + try: + self.requestor.send_data(UPDATE_JOB_STATE, self.job.to_dict()) + except Exception as e: + logger.warning("Failed to broadcast motion search status: %s", e) + + def _should_stop(self) -> bool: + """Check if processing should stop due to cancellation or internal limits.""" + return self.cancel_event.is_set() or self.internal_stop_event.is_set() + + def _execute_search(self) -> list[MotionSearchResult]: + """Main search execution logic.""" + camera_name = self.job.camera + camera_config = self.config.cameras.get(camera_name) + if not camera_config: + raise ValueError(f"Camera {camera_name} not found") + + frame_width = camera_config.detect.width + frame_height = camera_config.detect.height + + # Create polygon mask + polygon_mask = create_polygon_mask( + self.job.polygon_points, frame_width, frame_height + ) + + if np.count_nonzero(polygon_mask) == 0: + logger.warning("Polygon mask is empty for job %s", self.job.id) + return [] + + # Compute ROI bbox in normalized coordinates for heatmap gate + roi_bbox = compute_roi_bbox_normalized(self.job.polygon_points) + + # Query recordings + recordings = list( + Recordings.select() + .where( + ( + Recordings.start_time.between( + self.job.start_time_range, self.job.end_time_range + ) + ) + | ( + Recordings.end_time.between( + self.job.start_time_range, self.job.end_time_range + ) + ) + | ( + (self.job.start_time_range > Recordings.start_time) + & (self.job.end_time_range < Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.asc()) + ) + + if not recordings: + logger.debug("No recordings found for motion search job %s", self.job.id) + return [] + + logger.debug( + "Motion search job %s: queried %d recording segments for camera %s " + "(range %.1f - %.1f)", + self.job.id, + len(recordings), + camera_name, + self.job.start_time_range, + self.job.end_time_range, + ) + + self.metrics.segments_scanned = len(recordings) + + # Apply activity and heatmap gates + filtered_recordings = [] + for recording in recordings: + if not segment_passes_activity_gate(recording): + self.metrics.metadata_inactive_segments += 1 + self.metrics.segments_processed += 1 + logger.debug( + "Motion search job %s: segment %s skipped by activity gate " + "(motion=%s, objects=%s, regions=%s)", + self.job.id, + recording.id, + recording.motion, + recording.objects, + recording.regions, + ) + continue + if not segment_passes_heatmap_gate(recording, roi_bbox): + self.metrics.heatmap_roi_skip_segments += 1 + self.metrics.segments_processed += 1 + logger.debug( + "Motion search job %s: segment %s skipped by heatmap gate " + "(heatmap present=%s, roi_bbox=%s)", + self.job.id, + recording.id, + recording.motion_heatmap is not None, + roi_bbox, + ) + continue + filtered_recordings.append(recording) + + self._broadcast_status() + + # Fallback: if all segments were filtered out, scan all segments + # This allows motion search to find things the detector missed + if not filtered_recordings and recordings: + logger.info( + "All %d segments filtered by gates, falling back to full scan", + len(recordings), + ) + self.metrics.fallback_full_range_segments = len(recordings) + filtered_recordings = recordings + + logger.debug( + "Motion search job %s: %d/%d segments passed gates " + "(activity_skipped=%d, heatmap_skipped=%d)", + self.job.id, + len(filtered_recordings), + len(recordings), + self.metrics.metadata_inactive_segments, + self.metrics.heatmap_roi_skip_segments, + ) + + if self.job.parallel: + return self._search_motion_parallel(filtered_recordings, polygon_mask) + + return self._search_motion_sequential(filtered_recordings, polygon_mask) + + def _search_motion_parallel( + self, + recordings: list[Recordings], + polygon_mask: np.ndarray, + ) -> list[MotionSearchResult]: + """Search for motion in parallel across segments, streaming results.""" + all_results: list[MotionSearchResult] = [] + total_frames = 0 + next_recording_idx_to_merge = 0 + + logger.debug( + "Motion search job %s: starting motion search with %d workers " + "across %d segments", + self.job.id, + self.max_workers, + len(recordings), + ) + + # Initialize partial results on the job so they stream to the frontend + self.job.results = {"results": [], "total_frames_processed": 0} + + with ThreadPoolExecutor(max_workers=self.max_workers) as executor: + futures: dict[Future, int] = {} + completed_segments: dict[int, tuple[list[MotionSearchResult], int]] = {} + + for idx, recording in enumerate(recordings): + if self._should_stop(): + break + + future = executor.submit( + self._process_recording_for_motion, + recording.path, + recording.start_time, + recording.end_time, + self.job.start_time_range, + self.job.end_time_range, + polygon_mask, + self.job.threshold, + self.job.min_area, + self.job.frame_skip, + ) + futures[future] = idx + + for future in as_completed(futures): + if self._should_stop(): + # Cancel remaining futures + for f in futures: + f.cancel() + break + + recording_idx = futures[future] + recording = recordings[recording_idx] + + try: + results, frames = future.result() + self.metrics.segments_processed += 1 + completed_segments[recording_idx] = (results, frames) + + while next_recording_idx_to_merge in completed_segments: + segment_results, segment_frames = completed_segments.pop( + next_recording_idx_to_merge + ) + + all_results.extend(segment_results) + total_frames += segment_frames + self.job.total_frames_processed = total_frames + self.metrics.frames_decoded = total_frames + + if segment_results: + deduped = self._deduplicate_results(all_results) + self.job.results = { + "results": [ + r.to_dict() for r in deduped[: self.job.max_results] + ], + "total_frames_processed": total_frames, + } + + self._broadcast_status() + + if segment_results and len(deduped) >= self.job.max_results: + self.internal_stop_event.set() + for pending_future in futures: + pending_future.cancel() + break + + next_recording_idx_to_merge += 1 + + if self.internal_stop_event.is_set(): + break + + except Exception as e: + self.metrics.segments_processed += 1 + self.metrics.segments_with_errors += 1 + self._broadcast_status() + logger.warning( + "Error processing segment %s: %s", + recording.path, + e, + ) + + self.job.total_frames_processed = total_frames + self.metrics.frames_decoded = total_frames + + logger.debug( + "Motion search job %s: motion search complete, " + "found %d raw results, decoded %d frames, %d segment errors", + self.job.id, + len(all_results), + total_frames, + self.metrics.segments_with_errors, + ) + + # Sort and deduplicate results + all_results.sort(key=lambda x: x.timestamp) + return self._deduplicate_results(all_results)[: self.job.max_results] + + def _search_motion_sequential( + self, + recordings: list[Recordings], + polygon_mask: np.ndarray, + ) -> list[MotionSearchResult]: + """Search for motion sequentially across segments, streaming results.""" + all_results: list[MotionSearchResult] = [] + total_frames = 0 + + logger.debug( + "Motion search job %s: starting sequential motion search across %d segments", + self.job.id, + len(recordings), + ) + + self.job.results = {"results": [], "total_frames_processed": 0} + + for recording in recordings: + if self.cancel_event.is_set(): + break + + try: + results, frames = self._process_recording_for_motion( + recording.path, + recording.start_time, + recording.end_time, + self.job.start_time_range, + self.job.end_time_range, + polygon_mask, + self.job.threshold, + self.job.min_area, + self.job.frame_skip, + ) + all_results.extend(results) + total_frames += frames + + self.job.total_frames_processed = total_frames + self.metrics.frames_decoded = total_frames + self.metrics.segments_processed += 1 + + if results: + all_results.sort(key=lambda x: x.timestamp) + deduped = self._deduplicate_results(all_results)[ + : self.job.max_results + ] + self.job.results = { + "results": [r.to_dict() for r in deduped], + "total_frames_processed": total_frames, + } + + self._broadcast_status() + + if results and len(deduped) >= self.job.max_results: + break + + except Exception as e: + self.metrics.segments_processed += 1 + self.metrics.segments_with_errors += 1 + self._broadcast_status() + logger.warning("Error processing segment %s: %s", recording.path, e) + + self.job.total_frames_processed = total_frames + self.metrics.frames_decoded = total_frames + + logger.debug( + "Motion search job %s: sequential motion search complete, " + "found %d raw results, decoded %d frames, %d segment errors", + self.job.id, + len(all_results), + total_frames, + self.metrics.segments_with_errors, + ) + + all_results.sort(key=lambda x: x.timestamp) + return self._deduplicate_results(all_results)[: self.job.max_results] + + def _deduplicate_results( + self, results: list[MotionSearchResult], min_gap: float = 1.0 + ) -> list[MotionSearchResult]: + """Deduplicate results that are too close together.""" + if not results: + return results + + deduplicated: list[MotionSearchResult] = [] + last_timestamp = 0.0 + + for result in results: + if result.timestamp - last_timestamp >= min_gap: + deduplicated.append(result) + last_timestamp = result.timestamp + + return deduplicated + + def _process_recording_for_motion( + self, + recording_path: str, + recording_start: float, + recording_end: float, + search_start: float, + search_end: float, + polygon_mask: np.ndarray, + threshold: int, + min_area: float, + frame_skip: int, + ) -> tuple[list[MotionSearchResult], int]: + """Process a single recording file for motion detection. + + This method is designed to be called from a thread pool. + + Args: + min_area: Minimum change area as a percentage of the ROI (0-100). + """ + results: list[MotionSearchResult] = [] + frames_processed = 0 + + if not os.path.exists(recording_path): + logger.warning("Recording file not found: %s", recording_path) + return results, frames_processed + + cap = cv2.VideoCapture(recording_path) + if not cap.isOpened(): + logger.error("Could not open recording: %s", recording_path) + return results, frames_processed + + try: + fps = cap.get(cv2.CAP_PROP_FPS) or 30.0 + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + recording_duration = recording_end - recording_start + + # Calculate frame range + start_offset = max(0, search_start - recording_start) + end_offset = min(recording_duration, search_end - recording_start) + start_frame = int(start_offset * fps) + end_frame = int(end_offset * fps) + start_frame = max(0, min(start_frame, total_frames - 1)) + end_frame = max(0, min(end_frame, total_frames)) + + if start_frame >= end_frame: + return results, frames_processed + + cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame) + + # Get ROI bounding box + roi_bbox = cv2.boundingRect(polygon_mask) + roi_x, roi_y, roi_w, roi_h = roi_bbox + + prev_frame_gray = None + frame_step = max(frame_skip, 1) + frame_idx = start_frame + + while frame_idx < end_frame: + if self._should_stop(): + break + + ret, frame = cap.read() + if not ret: + frame_idx += 1 + continue + + if (frame_idx - start_frame) % frame_step != 0: + frame_idx += 1 + continue + + frames_processed += 1 + + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + # Handle frame dimension changes + if gray.shape != polygon_mask.shape: + resized_mask = cv2.resize( + polygon_mask, (gray.shape[1], gray.shape[0]), cv2.INTER_NEAREST + ) + current_bbox = cv2.boundingRect(resized_mask) + else: + resized_mask = polygon_mask + current_bbox = roi_bbox + + roi_x, roi_y, roi_w, roi_h = current_bbox + cropped_gray = gray[roi_y : roi_y + roi_h, roi_x : roi_x + roi_w] + cropped_mask = resized_mask[ + roi_y : roi_y + roi_h, roi_x : roi_x + roi_w + ] + + cropped_mask_area = np.count_nonzero(cropped_mask) + if cropped_mask_area == 0: + frame_idx += 1 + continue + + # Convert percentage to pixel count for this ROI + min_area_pixels = int((min_area / 100.0) * cropped_mask_area) + + masked_gray = cv2.bitwise_and( + cropped_gray, cropped_gray, mask=cropped_mask + ) + + if prev_frame_gray is not None: + diff = cv2.absdiff(prev_frame_gray, masked_gray) + diff_blurred = cv2.GaussianBlur(diff, (3, 3), 0) + _, thresh = cv2.threshold( + diff_blurred, threshold, 255, cv2.THRESH_BINARY + ) + thresh_dilated = cv2.dilate(thresh, None, iterations=1) + thresh_masked = cv2.bitwise_and( + thresh_dilated, thresh_dilated, mask=cropped_mask + ) + + change_pixels = cv2.countNonZero(thresh_masked) + if change_pixels > min_area_pixels: + contours, _ = cv2.findContours( + thresh_masked, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + total_change_area = sum( + cv2.contourArea(c) + for c in contours + if cv2.contourArea(c) >= min_area_pixels + ) + if total_change_area > 0: + frame_time_offset = (frame_idx - start_frame) / fps + timestamp = ( + recording_start + start_offset + frame_time_offset + ) + change_percentage = ( + total_change_area / cropped_mask_area + ) * 100 + results.append( + MotionSearchResult( + timestamp=timestamp, + change_percentage=round(change_percentage, 2), + ) + ) + + prev_frame_gray = masked_gray + frame_idx += 1 + + finally: + cap.release() + + logger.debug( + "Motion search segment complete: %s, %d frames processed, %d results found", + recording_path, + frames_processed, + len(results), + ) + return results, frames_processed + + +# Module-level state for managing per-camera jobs +_motion_search_jobs: dict[str, tuple[MotionSearchJob, threading.Event]] = {} +_jobs_lock = threading.Lock() + + +def stop_all_motion_search_jobs() -> None: + """Cancel all running motion search jobs for clean shutdown.""" + with _jobs_lock: + for job_id, (job, cancel_event) in _motion_search_jobs.items(): + if job.status in (JobStatusTypesEnum.queued, JobStatusTypesEnum.running): + cancel_event.set() + logger.debug("Signalling motion search job %s to stop", job_id) + + +def start_motion_search_job( + config: FrigateConfig, + camera_name: str, + start_time: float, + end_time: float, + polygon_points: list[list[float]], + threshold: int = 30, + min_area: float = 5.0, + frame_skip: int = 5, + parallel: bool = False, + max_results: int = 25, +) -> str: + """Start a new motion search job. + + Returns the job ID. + """ + job = MotionSearchJob( + camera=camera_name, + start_time_range=start_time, + end_time_range=end_time, + polygon_points=polygon_points, + threshold=threshold, + min_area=min_area, + frame_skip=frame_skip, + parallel=parallel, + max_results=max_results, + ) + + cancel_event = threading.Event() + + with _jobs_lock: + _motion_search_jobs[job.id] = (job, cancel_event) + + set_current_job(job) + + runner = MotionSearchRunner(job, config, cancel_event) + runner.start() + + logger.debug( + "Started motion search job %s for camera %s: " + "time_range=%.1f-%.1f, threshold=%d, min_area=%.1f%%, " + "frame_skip=%d, parallel=%s, max_results=%d, polygon_points=%d vertices", + job.id, + camera_name, + start_time, + end_time, + threshold, + min_area, + frame_skip, + parallel, + max_results, + len(polygon_points), + ) + return job.id + + +def get_motion_search_job(job_id: str) -> Optional[MotionSearchJob]: + """Get a motion search job by ID.""" + with _jobs_lock: + job_entry = _motion_search_jobs.get(job_id) + if job_entry: + return job_entry[0] + # Check completed jobs via manager + return get_job_by_id("motion_search", job_id) + + +def cancel_motion_search_job(job_id: str) -> bool: + """Cancel a motion search job. + + Returns True if cancellation was initiated, False if job not found. + """ + with _jobs_lock: + job_entry = _motion_search_jobs.get(job_id) + if not job_entry: + return False + + job, cancel_event = job_entry + + if job.status not in (JobStatusTypesEnum.queued, JobStatusTypesEnum.running): + # Already finished + return True + + cancel_event.set() + job.status = JobStatusTypesEnum.cancelled + job_payload = job.to_dict() + logger.info("Cancelled motion search job %s", job_id) + + requestor: Optional[InterProcessRequestor] = None + try: + requestor = InterProcessRequestor() + requestor.send_data(UPDATE_JOB_STATE, job_payload) + except Exception as e: + logger.warning( + "Failed to broadcast cancelled motion search job %s: %s", job_id, e + ) + finally: + if requestor: + requestor.stop() + + return True diff --git a/frigate/models.py b/frigate/models.py index fd5061613..d927a12c8 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -78,6 +78,7 @@ class Recordings(Model): dBFS = IntegerField(null=True) segment_size = FloatField(default=0) # this should be stored as MB regions = IntegerField(null=True) + motion_heatmap = JSONField(null=True) # 16x16 grid, 256 values (0-255) class ExportCase(Model): diff --git a/frigate/motion/improved_motion.py b/frigate/motion/improved_motion.py index a8f2ab12c..b821e9532 100644 --- a/frigate/motion/improved_motion.py +++ b/frigate/motion/improved_motion.py @@ -176,11 +176,32 @@ class ImprovedMotionDetector(MotionDetector): motion_boxes = [] pct_motion = 0 + # skip motion entirely if the scene change percentage exceeds configured + # threshold. this is useful to ignore lighting storms, IR mode switches, + # etc. rather than registering them as brief motion and then recalibrating. + # note: skipping means the frame is dropped and **no recording will be + # created**, which could hide a legitimate object if the camera is actively + # auto‑tracking. the alternative is to allow motion and accept a small + # recording that can be reviewed in the timeline. disabled by default (None). + if ( + self.config.skip_motion_threshold is not None + and pct_motion > self.config.skip_motion_threshold + ): + # force a recalibration so we transition to the new background + self.calibrating = True + return [] + # once the motion is less than 5% and the number of contours is < 4, assume its calibrated if pct_motion < 0.05 and len(motion_boxes) <= 4: self.calibrating = False - # if calibrating or the motion contours are > 80% of the image area (lightning, ir, ptz) recalibrate + # if calibrating or the motion contours are > 80% of the image area + # (lightning, ir, ptz) recalibrate. the lightning threshold does **not** + # stop motion detection entirely; it simply halts additional processing for + # the current frame once the percentage crosses the threshold. this helps + # reduce false positive object detections and CPU usage during high‑motion + # events. recordings continue to be generated because users expect data + # while a PTZ camera is moving. if self.calibrating or pct_motion > self.config.lightning_threshold: self.calibrating = True diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index 7b54d6bd1..68040476a 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -50,11 +50,13 @@ class SegmentInfo: active_object_count: int, region_count: int, average_dBFS: int, + motion_heatmap: dict[str, int] | None = None, ) -> None: self.motion_count = motion_count self.active_object_count = active_object_count self.region_count = region_count self.average_dBFS = average_dBFS + self.motion_heatmap = motion_heatmap def should_discard_segment(self, retain_mode: RetainModeEnum) -> bool: keep = False @@ -454,6 +456,59 @@ class RecordingMaintainer(threading.Thread): if end_time < retain_cutoff: self.drop_segment(cache_path) + def _compute_motion_heatmap( + self, camera: str, motion_boxes: list[tuple[int, int, int, int]] + ) -> dict[str, int] | None: + """Compute a 16x16 motion intensity heatmap from motion boxes. + + Returns a sparse dict mapping cell index (as string) to intensity (1-255). + Only cells with motion are included. + + Args: + camera: Camera name to get detect dimensions from. + motion_boxes: List of (x1, y1, x2, y2) pixel coordinates. + + Returns: + Sparse dict like {"45": 3, "46": 5}, or None if no boxes. + """ + if not motion_boxes: + return None + + camera_config = self.config.cameras.get(camera) + if not camera_config: + return None + + frame_width = camera_config.detect.width + frame_height = camera_config.detect.height + + if frame_width <= 0 or frame_height <= 0: + return None + + GRID_SIZE = 16 + counts: dict[int, int] = {} + + for box in motion_boxes: + if len(box) < 4: + continue + x1, y1, x2, y2 = box + + # Convert pixel coordinates to grid cells + grid_x1 = max(0, int((x1 / frame_width) * GRID_SIZE)) + grid_y1 = max(0, int((y1 / frame_height) * GRID_SIZE)) + grid_x2 = min(GRID_SIZE - 1, int((x2 / frame_width) * GRID_SIZE)) + grid_y2 = min(GRID_SIZE - 1, int((y2 / frame_height) * GRID_SIZE)) + + for y in range(grid_y1, grid_y2 + 1): + for x in range(grid_x1, grid_x2 + 1): + idx = y * GRID_SIZE + x + counts[idx] = min(255, counts.get(idx, 0) + 1) + + if not counts: + return None + + # Convert to string keys for JSON storage + return {str(k): v for k, v in counts.items()} + def segment_stats( self, camera: str, start_time: datetime.datetime, end_time: datetime.datetime ) -> SegmentInfo: @@ -461,6 +516,8 @@ class RecordingMaintainer(threading.Thread): active_count = 0 region_count = 0 motion_count = 0 + all_motion_boxes: list[tuple[int, int, int, int]] = [] + for frame in self.object_recordings_info[camera]: # frame is after end time of segment if frame[0] > end_time.timestamp(): @@ -479,6 +536,8 @@ class RecordingMaintainer(threading.Thread): ) motion_count += len(frame[2]) region_count += len(frame[3]) + # Collect motion boxes for heatmap computation + all_motion_boxes.extend(frame[2]) audio_values = [] for frame in self.audio_recordings_info[camera]: @@ -498,8 +557,14 @@ class RecordingMaintainer(threading.Thread): average_dBFS = 0 if not audio_values else np.average(audio_values) + motion_heatmap = self._compute_motion_heatmap(camera, all_motion_boxes) + return SegmentInfo( - motion_count, active_count, region_count, round(average_dBFS) + motion_count, + active_count, + region_count, + round(average_dBFS), + motion_heatmap, ) async def move_segment( @@ -590,6 +655,7 @@ class RecordingMaintainer(threading.Thread): Recordings.regions.name: segment_info.region_count, Recordings.dBFS.name: segment_info.average_dBFS, Recordings.segment_size.name: segment_size, + Recordings.motion_heatmap.name: segment_info.motion_heatmap, } except Exception as e: logger.error(f"Unable to store recording segment {cache_path}") diff --git a/frigate/test/test_motion_detector.py b/frigate/test/test_motion_detector.py new file mode 100644 index 000000000..cdf4210a5 --- /dev/null +++ b/frigate/test/test_motion_detector.py @@ -0,0 +1,91 @@ +import unittest + +import numpy as np + +from frigate.config.camera.motion import MotionConfig +from frigate.motion.improved_motion import ImprovedMotionDetector + + +class TestImprovedMotionDetector(unittest.TestCase): + def setUp(self): + # small frame for testing; actual frames are grayscale + self.frame_shape = (100, 100) # height, width + self.config = MotionConfig() + # motion detector assumes a rasterized_mask attribute exists on config + # when update_mask() is called; add one manually by bypassing pydantic. + object.__setattr__( + self.config, + "rasterized_mask", + np.ones((self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8), + ) + + # create minimal PTZ metrics stub to satisfy detector checks + class _Stub: + def __init__(self, value=False): + self.value = value + + def is_set(self): + return bool(self.value) + + class DummyPTZ: + def __init__(self): + self.autotracker_enabled = _Stub(False) + self.motor_stopped = _Stub(False) + self.stop_time = _Stub(0) + + self.detector = ImprovedMotionDetector( + self.frame_shape, self.config, fps=30, ptz_metrics=DummyPTZ() + ) + + # establish a baseline frame (all zeros) + base_frame = np.zeros( + (self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8 + ) + self.detector.detect(base_frame) + + def _half_change_frame(self) -> np.ndarray: + """Produce a frame where roughly half of the pixels are different.""" + frame = np.zeros((self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8) + # flip the top half to white + frame[: self.frame_shape[0] // 2, :] = 255 + return frame + + def test_skip_motion_threshold_default(self): + """With the default (None) setting, motion should always be reported.""" + frame = self._half_change_frame() + boxes = self.detector.detect(frame) + self.assertTrue( + boxes, "Expected motion boxes when skip threshold is unset (disabled)" + ) + + def test_skip_motion_threshold_applied(self): + """Setting a low skip threshold should prevent any boxes from being returned.""" + # change the config and update the detector reference + self.config.skip_motion_threshold = 0.4 + self.detector.config = self.config + self.detector.update_mask() + + frame = self._half_change_frame() + boxes = self.detector.detect(frame) + self.assertEqual( + boxes, + [], + "Motion boxes should be empty when scene change exceeds skip threshold", + ) + + def test_skip_motion_threshold_does_not_affect_calibration(self): + """Even when skipping, the detector should go into calibrating state.""" + self.config.skip_motion_threshold = 0.4 + self.detector.config = self.config + self.detector.update_mask() + + frame = self._half_change_frame() + _ = self.detector.detect(frame) + self.assertTrue( + self.detector.calibrating, + "Detector should be in calibrating state after skip event", + ) + + +if __name__ == "__main__": + unittest.main() 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/migrations/035_add_motion_heatmap.py b/migrations/035_add_motion_heatmap.py new file mode 100644 index 000000000..b6962083e --- /dev/null +++ b/migrations/035_add_motion_heatmap.py @@ -0,0 +1,34 @@ +"""Peewee migrations -- 035_add_motion_heatmap.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.python(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql('ALTER TABLE "recordings" ADD COLUMN "motion_heatmap" TEXT NULL') + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/web/package-lock.json b/web/package-lock.json index 9f7382839..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", @@ -21,12 +22,13 @@ "@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.3", "@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", @@ -44,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", @@ -55,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", @@ -86,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" }, @@ -95,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", @@ -110,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" } @@ -765,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", @@ -797,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": { @@ -1293,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": "*", @@ -1312,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": "*", @@ -1358,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": "*", @@ -1387,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", @@ -1423,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", @@ -1578,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": "*", @@ -1696,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", @@ -1726,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", @@ -1768,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", @@ -1811,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", @@ -1882,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", @@ -1916,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", @@ -1965,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": "*", @@ -1991,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", @@ -2036,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": "*", @@ -2060,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", @@ -2091,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", @@ -2119,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": "*", @@ -2181,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": "*", @@ -2286,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==", @@ -2310,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", @@ -2357,40 +2923,14 @@ } } }, - "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==", + "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", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", "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-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==", - "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/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", @@ -2407,6 +2947,316 @@ } } }, + "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", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "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-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/@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/@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", @@ -2470,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" }, @@ -2512,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": "*", @@ -2553,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==", @@ -2576,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": "*", @@ -2609,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": "*", @@ -2627,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": "*", @@ -2642,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", @@ -2702,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": "*", @@ -2755,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", @@ -2795,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", @@ -2848,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", @@ -2893,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", @@ -2996,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", @@ -3030,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", @@ -3063,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", @@ -3195,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": "*", @@ -3212,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", @@ -3243,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": "*", @@ -3279,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": "*", @@ -3302,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": { @@ -3381,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", @@ -5141,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", @@ -5165,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", @@ -5198,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": "*" } }, @@ -5652,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", @@ -6074,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", @@ -6087,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", @@ -6256,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", @@ -6318,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": { @@ -6823,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", @@ -7002,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", @@ -7100,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", @@ -7137,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", @@ -7449,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": { @@ -7746,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", @@ -7834,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": { @@ -7858,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", @@ -8019,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", @@ -8034,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", @@ -8048,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", @@ -8542,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", @@ -8603,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": { @@ -8751,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", @@ -8762,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", @@ -8781,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", @@ -8953,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", @@ -9893,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", @@ -9919,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": { @@ -9994,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", @@ -10103,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": { @@ -10232,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", @@ -10392,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", @@ -10868,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" } @@ -10935,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": { @@ -10960,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", @@ -10986,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" }, @@ -11058,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", @@ -11075,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": { @@ -11115,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": { @@ -11177,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": { @@ -11274,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", @@ -11299,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", @@ -11331,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", @@ -11725,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", @@ -11754,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", @@ -11805,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": { @@ -12395,6 +12232,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12444,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", @@ -12558,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, @@ -12847,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": { @@ -13047,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 32d27ab5e..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 .", @@ -27,12 +28,13 @@ "@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.3", "@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", @@ -50,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", @@ -61,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", @@ -92,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" }, @@ -101,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", @@ -116,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/config/cameras.json b/web/public/locales/en/config/cameras.json index bbb8d4b45..5880d30c3 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -264,7 +264,11 @@ }, "lightning_threshold": { "label": "Lightning threshold", - "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0)." + "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events." + }, + "skip_motion_threshold": { + "label": "Skip motion threshold", + "description": "If more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Range 0.0 to 1.0." }, "improve_contrast": { "label": "Improve contrast", @@ -864,7 +868,8 @@ "description": "A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used." }, "enabled": { - "label": "Whether this zone is active. Disabled zones are ignored at runtime." + "label": "Enabled", + "description": "Enable or disable this zone. Disabled zones are ignored at runtime." }, "enabled_in_config": { "label": "Keep track of original state of zone." diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index 1bda2ab44..5268c1b02 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -1391,7 +1391,11 @@ }, "lightning_threshold": { "label": "Lightning threshold", - "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0)." + "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events." + }, + "skip_motion_threshold": { + "label": "Skip motion threshold", + "description": "If more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Range 0.0 to 1.0." }, "improve_contrast": { "label": "Improve contrast", diff --git a/web/public/locales/en/views/events.json b/web/public/locales/en/views/events.json index ea3ee853d..ec0b29116 100644 --- a/web/public/locales/en/views/events.json +++ b/web/public/locales/en/views/events.json @@ -61,5 +61,25 @@ "detected": "detected", "normalActivity": "Normal", "needsReview": "Needs review", - "securityConcern": "Security concern" + "securityConcern": "Security concern", + "motionSearch": { + "menuItem": "Motion search", + "openMenu": "Camera options" + }, + "motionPreviews": { + "menuItem": "View motion previews", + "title": "Motion previews: {{camera}}", + "mobileSettingsTitle": "Motion Preview Settings", + "mobileSettingsDesc": "Adjust playback speed and dimming, and choose a date to review motion-only clips.", + "dim": "Dim", + "dimAria": "Adjust dimming intensity", + "dimDesc": "Increase dimming to increase motion area visibility.", + "speed": "Speed", + "speedAria": "Select preview playback speed", + "speedDesc": "Choose how quickly preview clips play.", + "back": "Back", + "empty": "No previews available", + "noPreview": "Preview unavailable", + "seekAria": "Seek {{camera}} player to {{time}}" + } } diff --git a/web/public/locales/en/views/motionSearch.json b/web/public/locales/en/views/motionSearch.json new file mode 100644 index 000000000..6e22c3203 --- /dev/null +++ b/web/public/locales/en/views/motionSearch.json @@ -0,0 +1,75 @@ +{ + "documentTitle": "Motion Search - Frigate", + "title": "Motion Search", + "description": "Draw a polygon to define the region of interest, and specify a time range to search for motion changes within that region.", + "selectCamera": "Motion Search is loading", + "startSearch": "Start Search", + "searchStarted": "Search started", + "searchCancelled": "Search cancelled", + "cancelSearch": "Cancel", + "searching": "Search in progress.", + "searchComplete": "Search complete", + "noResultsYet": "Run a search to find motion changes in the selected region", + "noChangesFound": "No pixel changes detected in the selected region", + "changesFound_one": "Found {{count}} motion change", + "changesFound_other": "Found {{count}} motion changes", + "framesProcessed": "{{count}} frames processed", + "jumpToTime": "Jump to this time", + "results": "Results", + "showSegmentHeatmap": "Heatmap", + "newSearch": "New Search", + "clearResults": "Clear Results", + "clearROI": "Clear polygon", + "polygonControls": { + "points_one": "{{count}} point", + "points_other": "{{count}} points", + "undo": "Undo last point", + "reset": "Reset polygon" + }, + "motionHeatmapLabel": "Motion Heatmap", + "dialog": { + "title": "Motion Search", + "cameraLabel": "Camera", + "previewAlt": "Camera preview for {{camera}}" + }, + "timeRange": { + "title": "Search Range", + "start": "Start time", + "end": "End time" + }, + "settings": { + "title": "Search Settings", + "parallelMode": "Parallel mode", + "parallelModeDesc": "Scan multiple recording segments at the same time (faster, but significantly more CPU intensive)", + "threshold": "Sensitivity Threshold", + "thresholdDesc": "Lower values detect smaller changes (1-255)", + "minArea": "Minimum Change Area", + "minAreaDesc": "Minimum percentage of the region of interest that must change to be considered significant", + "frameSkip": "Frame Skip", + "frameSkipDesc": "Process every Nth frame. Set this to your camera's frame rate to process one frame per second (e.g. 5 for a 5 FPS camera, 30 for a 30 FPS camera). Higher values will be faster, but may miss short motion events.", + "maxResults": "Maximum Results", + "maxResultsDesc": "Stop after this many matching timestamps" + }, + "errors": { + "noCamera": "Please select a camera", + "noROI": "Please draw a region of interest", + "noTimeRange": "Please select a time range", + "invalidTimeRange": "End time must be after start time", + "searchFailed": "Search failed: {{message}}", + "polygonTooSmall": "Polygon must have at least 3 points", + "unknown": "Unknown error" + }, + "changePercentage": "{{percentage}}% changed", + "metrics": { + "title": "Search Metrics", + "segmentsScanned": "Segments scanned", + "segmentsProcessed": "Processed", + "segmentsSkippedInactive": "Skipped (no activity)", + "segmentsSkippedHeatmap": "Skipped (no ROI overlap)", + "fallbackFullRange": "Fallback full-range scan", + "framesDecoded": "Frames decoded", + "wallTime": "Search time", + "segmentErrors": "Segment errors", + "seconds": "{{seconds}}s" + } +} diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 0b61b278b..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": { diff --git a/web/src/components/config-form/section-configs/motion.ts b/web/src/components/config-form/section-configs/motion.ts index 2cab58117..bab142d92 100644 --- a/web/src/components/config-form/section-configs/motion.ts +++ b/web/src/components/config-form/section-configs/motion.ts @@ -8,6 +8,7 @@ const motion: SectionConfigOverrides = { "enabled", "threshold", "lightning_threshold", + "skip_motion_threshold", "improve_contrast", "contour_area", "delta_alpha", @@ -22,6 +23,7 @@ const motion: SectionConfigOverrides = { hiddenFields: ["enabled_in_config", "mask", "raw_mask"], advancedFields: [ "lightning_threshold", + "skip_motion_threshold", "delta_alpha", "frame_alpha", "frame_height", diff --git a/web/src/components/config-form/theme/fields/LayoutGridField.tsx b/web/src/components/config-form/theme/fields/LayoutGridField.tsx index 9953794d0..2354b7551 100644 --- a/web/src/components/config-form/theme/fields/LayoutGridField.tsx +++ b/web/src/components/config-form/theme/fields/LayoutGridField.tsx @@ -114,10 +114,17 @@ interface PropertyElement { content: React.ReactElement; } +/** Shape of the props that RJSF injects into each property element. */ +interface RjsfElementProps { + schema?: { type?: string | string[] }; + uiSchema?: Record & { + "ui:widget"?: string; + "ui:options"?: Record; + }; +} + function isObjectLikeElement(item: PropertyElement) { - const fieldSchema = item.content.props?.schema as - | { type?: string | string[] } - | undefined; + const fieldSchema = (item.content.props as RjsfElementProps)?.schema; return fieldSchema?.type === "object"; } @@ -163,16 +170,21 @@ function GridLayoutObjectFieldTemplate( // Override the properties rendering with grid layout const isHiddenProp = (prop: (typeof properties)[number]) => - prop.content.props.uiSchema?.["ui:widget"] === "hidden"; + (prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] === + "hidden"; const visibleProps = properties.filter((prop) => !isHiddenProp(prop)); // Separate regular and advanced properties const advancedProps = visibleProps.filter( - (p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true, + (p) => + (p.content.props as RjsfElementProps).uiSchema?.["ui:options"] + ?.advanced === true, ); const regularProps = visibleProps.filter( - (p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true, + (p) => + (p.content.props as RjsfElementProps).uiSchema?.["ui:options"] + ?.advanced !== true, ); const hasModifiedAdvanced = advancedProps.some((prop) => isPathModified([...fieldPath, prop.name]), diff --git a/web/src/components/config-form/theme/templates/FieldTemplate.tsx b/web/src/components/config-form/theme/templates/FieldTemplate.tsx index becf720df..20dd967c1 100644 --- a/web/src/components/config-form/theme/templates/FieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/FieldTemplate.tsx @@ -448,6 +448,9 @@ export function FieldTemplate(props: FieldTemplateProps) { ); }; + const errorsProps = errors?.props as { errors?: unknown[] } | undefined; + const hasFieldErrors = !!errors && (errorsProps?.errors?.length ?? 0) > 0; + const renderStandardLabel = () => { if (!shouldRenderStandardLabel) { return null; @@ -459,7 +462,7 @@ export function FieldTemplate(props: FieldTemplateProps) { className={cn( "text-sm font-medium", isModified && "text-danger", - errors && errors.props?.errors?.length > 0 && "text-destructive", + hasFieldErrors && "text-destructive", )} > {finalLabel} @@ -497,7 +500,7 @@ export function FieldTemplate(props: FieldTemplateProps) { className={cn( "text-sm font-medium", isModified && "text-danger", - errors && errors.props?.errors?.length > 0 && "text-destructive", + hasFieldErrors && "text-destructive", )} > {finalLabel} diff --git a/web/src/components/config-form/theme/templates/MultiSchemaFieldTemplate.tsx b/web/src/components/config-form/theme/templates/MultiSchemaFieldTemplate.tsx index d845e2c61..8c2462df4 100644 --- a/web/src/components/config-form/theme/templates/MultiSchemaFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/MultiSchemaFieldTemplate.tsx @@ -1,6 +1,7 @@ // Custom MultiSchemaFieldTemplate to handle anyOf [Type, null] fields // Renders simple nullable types as single inputs instead of dropdowns +import type { JSX } from "react"; import { MultiSchemaFieldTemplateProps, StrictRJSFSchema, diff --git a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx index 808557d46..3ba4cb0bc 100644 --- a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx @@ -25,6 +25,15 @@ import { import get from "lodash/get"; import { AddPropertyButton, AdvancedCollapsible } from "../components"; +/** Shape of the props that RJSF injects into each property element. */ +interface RjsfElementProps { + schema?: { type?: string | string[] }; + uiSchema?: Record & { + "ui:widget"?: string; + "ui:options"?: Record; + }; +} + export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { const { title, @@ -182,16 +191,21 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { uiSchema?.["ui:options"]?.disableNestedCard === true; const isHiddenProp = (prop: (typeof properties)[number]) => - prop.content.props.uiSchema?.["ui:widget"] === "hidden"; + (prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] === + "hidden"; const visibleProps = properties.filter((prop) => !isHiddenProp(prop)); // Check for advanced section grouping const advancedProps = visibleProps.filter( - (p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true, + (p) => + (p.content.props as RjsfElementProps).uiSchema?.["ui:options"] + ?.advanced === true, ); const regularProps = visibleProps.filter( - (p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true, + (p) => + (p.content.props as RjsfElementProps).uiSchema?.["ui:options"] + ?.advanced !== true, ); const hasModifiedAdvanced = advancedProps.some((prop) => checkSubtreeModified([...fieldPath, prop.name]), @@ -333,9 +347,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { const ungrouped = items.filter((item) => !grouped.has(item.name)); const isObjectLikeField = (item: (typeof properties)[number]) => { - const fieldSchema = item.content.props.schema as - | { type?: string | string[] } - | undefined; + const fieldSchema = (item.content.props as RjsfElementProps)?.schema; return fieldSchema?.type === "object"; }; diff --git a/web/src/components/dynamic/TimeAgo.tsx b/web/src/components/dynamic/TimeAgo.tsx index 15731470a..b8b7c3f8b 100644 --- a/web/src/components/dynamic/TimeAgo.tsx +++ b/web/src/components/dynamic/TimeAgo.tsx @@ -1,4 +1,5 @@ import { t } from "i18next"; +import type { JSX } from "react"; import { FunctionComponent, useEffect, useMemo, useState } from "react"; interface IProp { diff --git a/web/src/components/indicators/Chip.tsx b/web/src/components/indicators/Chip.tsx index 06e905c83..e6de10057 100644 --- a/web/src/components/indicators/Chip.tsx +++ b/web/src/components/indicators/Chip.tsx @@ -1,8 +1,8 @@ import { cn } from "@/lib/utils"; import { LogSeverity } from "@/types/log"; -import { ReactNode, useMemo, useRef } from "react"; +import { ReactNode, useMemo } from "react"; import { isIOS } from "react-device-detect"; -import { CSSTransition } from "react-transition-group"; +import { AnimatePresence, motion } from "framer-motion"; type ChipProps = { className?: string; @@ -17,39 +17,31 @@ export default function Chip({ in: inProp = true, onClick, }: ChipProps) { - const nodeRef = useRef(null); - return ( - -
{ - e.stopPropagation(); + + {inProp && ( + { + e.stopPropagation(); - if (onClick) { - onClick(); - } - }} - > - {children} -
-
+ if (onClick) { + onClick(); + } + }} + > + {children} + + )} + ); } diff --git a/web/src/components/mobile/MobilePage.tsx b/web/src/components/mobile/MobilePage.tsx index 4b6c41c7a..ee56468cb 100644 --- a/web/src/components/mobile/MobilePage.tsx +++ b/web/src/components/mobile/MobilePage.tsx @@ -55,9 +55,9 @@ export function MobilePage({ }); return ( - + {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/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/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/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 22eef83a2..b91efd84b 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -1,5 +1,6 @@ import { MutableRefObject, + ReactNode, useCallback, useEffect, useRef, @@ -57,6 +58,7 @@ type HlsVideoPlayerProps = { isDetailMode?: boolean; camera?: string; currentTimeOverride?: number; + transformedOverlay?: ReactNode; }; export default function HlsVideoPlayer({ @@ -81,6 +83,7 @@ export default function HlsVideoPlayer({ isDetailMode = false, camera, currentTimeOverride, + transformedOverlay, }: HlsVideoPlayerProps) { const { t } = useTranslation("components/player"); const { data: config } = useSWR("config"); @@ -91,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(); @@ -350,157 +353,162 @@ export default function HlsVideoPlayer({ height: isMobile ? "100%" : undefined, }} > - {isDetailMode && - camera && - currentTime && - loadedMetadata && - videoDimensions.width > 0 && - videoDimensions.height > 0 && ( -
- { - if (onSeekToTime) { - onSeekToTime(timestamp, play); - } +
+ {transformedOverlay} + {isDetailMode && + camera && + currentTime && + loadedMetadata && + videoDimensions.width > 0 && + videoDimensions.height > 0 && ( +
-
- )} -
+ )} +
); 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/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index 7fe5bd50b..c8d95090d 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -1,4 +1,11 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { useApiHost } from "@/api"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; @@ -40,6 +47,7 @@ type DynamicVideoPlayerProps = { setFullResolution: React.Dispatch>; toggleFullscreen: () => void; containerRef?: React.MutableRefObject; + transformedOverlay?: ReactNode; }; export default function DynamicVideoPlayer({ className, @@ -58,6 +66,7 @@ export default function DynamicVideoPlayer({ setFullResolution, toggleFullscreen, containerRef, + transformedOverlay, }: DynamicVideoPlayerProps) { const { t } = useTranslation(["components/player"]); const apiHost = useApiHost(); @@ -312,6 +321,7 @@ export default function DynamicVideoPlayer({ isDetailMode={isDetailMode} camera={contextCamera || camera} currentTimeOverride={currentTime} + transformedOverlay={transformedOverlay} /> )} ; + containerRef: RefObject; camera: string; width: number; height: number; 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/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 cefd8f2d3..2796bc968 100644 --- a/web/src/components/timeline/MotionReviewTimeline.tsx +++ b/web/src/components/timeline/MotionReviewTimeline.tsx @@ -25,6 +25,7 @@ export type MotionReviewTimelineProps = { timestampSpread: number; timelineStart: number; timelineEnd: number; + scrollToTime?: number; showHandlebar?: boolean; handlebarTime?: number; setHandlebarTime?: React.Dispatch>; @@ -41,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; @@ -58,6 +59,7 @@ export function MotionReviewTimeline({ timestampSpread, timelineStart, timelineEnd, + scrollToTime, showHandlebar = false, handlebarTime, setHandlebarTime, @@ -176,6 +178,15 @@ export function MotionReviewTimeline({ [], ); + // allow callers to request the timeline center on a specific time + useEffect(() => { + if (scrollToTime == undefined) return; + + setTimeout(() => { + scrollToSegment(alignStartDateToTimeline(scrollToTime), true, "auto"); + }, 0); + }, [scrollToTime, scrollToSegment, alignStartDateToTimeline]); + // keep handlebar centered when zooming useEffect(() => { setTimeout(() => { diff --git a/web/src/components/timeline/ReviewTimeline.tsx b/web/src/components/timeline/ReviewTimeline.tsx index 8a3b6d2da..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; @@ -343,9 +343,12 @@ export function ReviewTimeline({ useEffect(() => { if (onHandlebarDraggingChange) { - onHandlebarDraggingChange(isDraggingHandlebar); + // Keep existing callback name but treat it as a generic dragging signal. + // This allows consumers (e.g. export-handle timelines) to correctly + // enable preview scrubbing while dragging export handles. + onHandlebarDraggingChange(isDragging); } - }, [isDraggingHandlebar, onHandlebarDraggingChange]); + }, [isDragging, onHandlebarDraggingChange]); const isHandlebarInNoRecordingPeriod = useMemo(() => { if (!getRecordingAvailability || handlebarTime === undefined) return false; 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/progress.tsx b/web/src/components/ui/progress.tsx new file mode 100644 index 000000000..105fb6500 --- /dev/null +++ b/web/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } 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/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-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index cf5bf4653..71507c2af 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -8,14 +8,19 @@ import { import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; import { MotionData, ReviewSegment } from "@/types/review"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { useTimelineUtils } from "./use-timeline-utils"; import { AudioDetection, ObjectType } from "@/types/ws"; +import { useTimelineUtils } from "./use-timeline-utils"; import useDeepMemo from "./use-deep-memo"; import { isEqual } from "lodash"; import { useAutoFrigateStats } from "./use-stats"; import useSWR from "swr"; import { getAttributeLabels } from "@/utils/iconUtil"; +export type MotionOnlyRange = { + start_time: number; + end_time: number; +}; + type useCameraActivityReturn = { enabled?: boolean; activeTracking: boolean; @@ -204,9 +209,9 @@ export function useCameraMotionNextTimestamp( return []; } - const ranges = []; - let currentSegmentStart = null; - let currentSegmentEnd = null; + const ranges: [number, number][] = []; + let currentSegmentStart: number | null = null; + let currentSegmentEnd: number | null = null; // align motion start to timeline start const offset = @@ -215,13 +220,19 @@ export function useCameraMotionNextTimestamp( segmentDuration; const startIndex = Math.abs(Math.floor(offset / 15)); + const now = Date.now() / 1000; for ( let i = startIndex; i < motionData.length; i = i + segmentDuration / 15 ) { - const motionStart = motionData[i].start_time; + const motionStart = motionData[i]?.start_time; + + if (motionStart == undefined) { + continue; + } + const motionEnd = motionStart + segmentDuration; const segmentMotion = motionData @@ -230,10 +241,10 @@ export function useCameraMotionNextTimestamp( const overlappingReviewItems = reviewItems.some( (item) => (item.start_time >= motionStart && item.start_time < motionEnd) || - ((item.end_time ?? Date.now() / 1000) > motionStart && - (item.end_time ?? Date.now() / 1000) <= motionEnd) || + ((item.end_time ?? now) > motionStart && + (item.end_time ?? now) <= motionEnd) || (item.start_time <= motionStart && - (item.end_time ?? Date.now() / 1000) >= motionEnd), + (item.end_time ?? now) >= motionEnd), ); if (!segmentMotion || overlappingReviewItems) { @@ -241,16 +252,14 @@ export function useCameraMotionNextTimestamp( currentSegmentStart = motionStart; } currentSegmentEnd = motionEnd; - } else { - if (currentSegmentStart !== null) { - ranges.push([currentSegmentStart, currentSegmentEnd]); - currentSegmentStart = null; - currentSegmentEnd = null; - } + } else if (currentSegmentStart !== null && currentSegmentEnd !== null) { + ranges.push([currentSegmentStart, currentSegmentEnd]); + currentSegmentStart = null; + currentSegmentEnd = null; } } - if (currentSegmentStart !== null) { + if (currentSegmentStart !== null && currentSegmentEnd !== null) { ranges.push([currentSegmentStart, currentSegmentEnd]); } @@ -304,3 +313,93 @@ export function useCameraMotionNextTimestamp( return nextTimestamp; } + +export function useCameraMotionOnlyRanges( + segmentDuration: number, + reviewItems: ReviewSegment[], + motionData: MotionData[], +) { + const motionOnlyRanges = useMemo(() => { + if (!motionData?.length || !reviewItems) { + return []; + } + + const fallbackBucketDuration = Math.max(1, segmentDuration / 2); + const normalizedMotionData = Array.from( + motionData + .reduce((accumulator, item) => { + const currentMotion = accumulator.get(item.start_time) ?? 0; + accumulator.set( + item.start_time, + Math.max(currentMotion, item.motion ?? 0), + ); + return accumulator; + }, new Map()) + .entries(), + ) + .map(([start_time, motion]) => ({ start_time, motion })) + .sort((left, right) => left.start_time - right.start_time); + + const bucketRanges: MotionOnlyRange[] = []; + const now = Date.now() / 1000; + + for (let i = 0; i < normalizedMotionData.length; i++) { + const motionStart = normalizedMotionData[i].start_time; + const motionEnd = motionStart + fallbackBucketDuration; + + const overlappingReviewItems = reviewItems.some( + (item) => + (item.start_time >= motionStart && item.start_time < motionEnd) || + ((item.end_time ?? now) > motionStart && + (item.end_time ?? now) <= motionEnd) || + (item.start_time <= motionStart && + (item.end_time ?? now) >= motionEnd), + ); + + const isMotionOnlySegment = + (normalizedMotionData[i].motion ?? 0) > 0 && !overlappingReviewItems; + + if (!isMotionOnlySegment) { + continue; + } + + bucketRanges.push({ + start_time: motionStart, + end_time: motionEnd, + }); + } + + if (!bucketRanges.length) { + return []; + } + + const mergedRanges = bucketRanges.reduce( + (ranges, range) => { + if (!ranges.length) { + return [range]; + } + + const previousRange = ranges[ranges.length - 1]; + const isContiguous = + range.start_time <= previousRange.end_time + 0.001 && + range.start_time >= previousRange.end_time - 0.001; + + if (isContiguous) { + previousRange.end_time = Math.max( + previousRange.end_time, + range.end_time, + ); + return ranges; + } + + ranges.push(range); + return ranges; + }, + [], + ); + + return mergedRanges; + }, [motionData, reviewItems, segmentDuration]); + + return motionOnlyRanges; +} 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-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/pages/Events.tsx b/web/src/pages/Events.tsx index 540a50777..e3f6e4fae 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -1,5 +1,6 @@ import ActivityIndicator from "@/components/indicators/activity-indicator"; import useApiFilter from "@/hooks/use-api-filter"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; import { useCameraPreviews } from "@/hooks/use-camera-previews"; import { useTimezone } from "@/hooks/use-date-utils"; import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state"; @@ -21,6 +22,7 @@ import { getEndOfDayTimestamp, } from "@/utils/dateUtil"; import EventView from "@/views/events/EventView"; +import MotionSearchView from "@/views/motion-search/MotionSearchView"; import { RecordingView } from "@/views/recording/RecordingView"; import axios from "axios"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -34,6 +36,7 @@ export default function Events() { revalidateOnFocus: false, }); const timezone = useTimezone(config); + const allowedCameras = useAllowedCameras(); // recordings viewer @@ -52,6 +55,74 @@ export default function Events() { undefined, false, ); + const [motionPreviewsCamera, setMotionPreviewsCamera] = useOverlayState< + string | undefined + >("motionPreviewsCamera", undefined); + + const [motionSearchCamera, setMotionSearchCamera] = useState( + null, + ); + const [motionSearchDay, setMotionSearchDay] = useState( + undefined, + ); + + const motionSearchCameras = useMemo(() => { + if (!config?.cameras) { + return [] as string[]; + } + + return Object.keys(config.cameras).filter((cam) => + allowedCameras.includes(cam), + ); + }, [allowedCameras, config?.cameras]); + + const selectedMotionSearchCamera = useMemo(() => { + if (!motionSearchCamera) { + return null; + } + + if (motionSearchCameras.includes(motionSearchCamera)) { + return motionSearchCamera; + } + + return motionSearchCameras[0] ?? null; + }, [motionSearchCamera, motionSearchCameras]); + + const motionSearchTimeRange = useMemo(() => { + if (motionSearchDay) { + return { + after: getBeginningOfDayTimestamp(new Date(motionSearchDay)), + before: getEndOfDayTimestamp(new Date(motionSearchDay)), + }; + } + + const now = Date.now() / 1000; + return { + after: now - 86400, + before: now, + }; + }, [motionSearchDay]); + + const closeMotionSearch = useCallback(() => { + setMotionSearchCamera(null); + setMotionSearchDay(undefined); + setBeforeTs(Date.now() / 1000); + }, []); + + const handleMotionSearchCameraSelect = useCallback((camera: string) => { + setMotionSearchCamera(camera); + }, []); + + const handleMotionSearchDaySelect = useCallback((day: Date | undefined) => { + if (day == undefined) { + setMotionSearchDay(undefined); + return; + } + + const normalizedDay = new Date(day); + normalizedDay.setHours(0, 0, 0, 0); + setMotionSearchDay(normalizedDay); + }, []); const [notificationTab, setNotificationTab] = useState("timeline"); @@ -508,7 +579,24 @@ export default function Events() { ); } } else { - return ( + return motionSearchCamera ? ( + !config || !selectedMotionSearchCamera ? ( + + ) : ( + + ) + ) : ( + setMotionPreviewsCamera(camera ?? undefined) + } + setMotionSearchCamera={setMotionSearchCamera} pullLatestData={reloadData} updateFilter={onUpdateFilter} /> diff --git a/web/src/pages/MotionSearch.tsx b/web/src/pages/MotionSearch.tsx new file mode 100644 index 000000000..c1651b72e --- /dev/null +++ b/web/src/pages/MotionSearch.tsx @@ -0,0 +1,112 @@ +import { useEffect, useMemo, useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useTimezone } from "@/hooks/use-date-utils"; +import MotionSearchView from "@/views/motion-search/MotionSearchView"; +import { + getBeginningOfDayTimestamp, + getEndOfDayTimestamp, +} from "@/utils/dateUtil"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; +import { useSearchEffect } from "@/hooks/use-overlay-state"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; + +export default function MotionSearch() { + const { t } = useTranslation(["views/motionSearch"]); + + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const timezone = useTimezone(config); + + useEffect(() => { + document.title = t("documentTitle"); + }, [t]); + + // Get allowed cameras + const allowedCameras = useAllowedCameras(); + + const cameras = useMemo(() => { + if (!config?.cameras) return []; + return Object.keys(config.cameras).filter((cam) => + allowedCameras.includes(cam), + ); + }, [config?.cameras, allowedCameras]); + + // Selected camera state + const [selectedCamera, setSelectedCamera] = useState(null); + const [cameraLocked, setCameraLocked] = useState(false); + + useSearchEffect("camera", (camera: string) => { + if (cameras.length > 0 && cameras.includes(camera)) { + setSelectedCamera(camera); + setCameraLocked(true); + } + return false; + }); + + // Initialize with first camera when available (only if not set by camera param) + useEffect(() => { + if (cameras.length === 0) return; + if (!selectedCamera) { + setSelectedCamera(cameras[0]); + } + }, [cameras, selectedCamera]); + + // Time range state - default to last 24 hours + const [selectedDay, setSelectedDay] = useState(undefined); + + const timeRange = useMemo(() => { + if (selectedDay) { + return { + after: getBeginningOfDayTimestamp(new Date(selectedDay)), + before: getEndOfDayTimestamp(new Date(selectedDay)), + }; + } + // Default to last 24 hours + const now = Date.now() / 1000; + return { + after: now - 86400, + before: now, + }; + }, [selectedDay]); + + const handleCameraSelect = useCallback((camera: string) => { + setSelectedCamera(camera); + }, []); + + const handleDaySelect = useCallback((day: Date | undefined) => { + if (day == undefined) { + setSelectedDay(undefined); + return; + } + + const normalizedDay = new Date(day); + normalizedDay.setHours(0, 0, 0, 0); + setSelectedDay(normalizedDay); + }, []); + + if (!config || cameras.length === 0) { + return ( +
+ +
+ ); + } + + return ( + + ); +} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index cdf0caa68..e6ee4e014 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,10 +476,18 @@ const CAMERA_SELECT_BUTTON_PAGES = [ "masksAndZones", "motionTuner", "triggers", + "regionGrid", ]; const ALLOWED_VIEWS_FOR_VIEWER = ["ui", "debug", "notifications"]; +const LARGE_BOTTOM_MARGIN_PAGES = [ + "masksAndZones", + "motionTuner", + "mediaSync", + "regionGrid", +]; + // keys for camera sections const CAMERA_SECTION_MAPPING: Record = { detect: "cameraDetect", diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index dc3554940..dcf3c312f 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -106,6 +106,7 @@ export interface CameraConfig { frame_height: number; improve_contrast: boolean; lightning_threshold: number; + skip_motion_threshold: number | null; mask: { [maskId: string]: { friendly_name?: string; diff --git a/web/src/types/motionSearch.ts b/web/src/types/motionSearch.ts new file mode 100644 index 000000000..6e919c22f --- /dev/null +++ b/web/src/types/motionSearch.ts @@ -0,0 +1,46 @@ +/** + * Types for the Motion Search feature + */ + +export interface MotionSearchResult { + timestamp: number; + change_percentage: number; +} + +export interface MotionSearchRequest { + start_time: number; + end_time: number; + polygon_points: number[][]; + parallel?: boolean; + threshold?: number; + min_area?: number; + frame_skip?: number; + max_results?: number; +} + +export interface MotionSearchStartResponse { + success: boolean; + message: string; + job_id: string; +} + +export interface MotionSearchMetrics { + segments_scanned: number; + segments_processed: number; + metadata_inactive_segments: number; + heatmap_roi_skip_segments: number; + fallback_full_range_segments: number; + frames_decoded: number; + wall_time_seconds: number; + segments_with_errors: number; +} + +export interface MotionSearchStatusResponse { + success: boolean; + message: string; + status: "queued" | "running" | "success" | "failed" | "cancelled"; + results?: MotionSearchResult[]; + total_frames_processed?: number; + error_message?: string; + metrics?: MotionSearchMetrics; +} diff --git a/web/src/types/record.ts b/web/src/types/record.ts index dbe43653a..107a8d86e 100644 --- a/web/src/types/record.ts +++ b/web/src/types/record.ts @@ -11,6 +11,7 @@ export type Recording = { duration: number; motion: number; objects: number; + motion_heatmap?: Record | null; dBFS: number; }; diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 70067ff5c..b8167b7dd 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -1,11 +1,21 @@ import Logo from "@/components/Logo"; import NewReviewData from "@/components/dynamic/NewReviewData"; +import CalendarFilterButton from "@/components/filter/CalendarFilterButton"; import ReviewActionGroup from "@/components/filter/ReviewActionGroup"; import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup"; import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer"; import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { VolumeSlider } from "@/components/ui/slider"; +import { + Select, + SelectContent, + SelectItem, + SelectSeparator, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { useScrollLockout } from "@/hooks/use-mouse-listener"; import { FrigateConfig } from "@/types/frigateConfig"; @@ -22,6 +32,7 @@ import { ZoomLevel, } from "@/types/review"; import { getChunkedTimeRange } from "@/utils/timelineUtil"; +import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import axios from "axios"; import { MutableRefObject, @@ -34,9 +45,18 @@ import { import { isDesktop, isMobile, isMobileOnly } from "react-device-detect"; import { LuFolderCheck, LuFolderX } from "react-icons/lu"; import { MdCircle } from "react-icons/md"; +import { FiMoreVertical } from "react-icons/fi"; +import { IoMdArrowRoundBack } from "react-icons/io"; import useSWR from "swr"; import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline"; import { Button } from "@/components/ui/button"; +import BlurredIconButton from "@/components/button/BlurredIconButton"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import PreviewPlayer, { PreviewController, } from "@/components/player/PreviewPlayer"; @@ -44,7 +64,10 @@ import SummaryTimeline from "@/components/timeline/SummaryTimeline"; import { RecordingStartingPoint } from "@/types/record"; import VideoControls from "@/components/player/VideoControls"; import { TimeRange } from "@/types/timeline"; -import { useCameraMotionNextTimestamp } from "@/hooks/use-camera-activity"; +import { + useCameraMotionNextTimestamp, + useCameraMotionOnlyRanges, +} from "@/hooks/use-camera-activity"; import useOptimisticState from "@/hooks/use-optimistic-state"; import { Skeleton } from "@/components/ui/skeleton"; import scrollIntoView from "scroll-into-view-if-needed"; @@ -56,6 +79,10 @@ import { GiSoundWaves } from "react-icons/gi"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { useTimelineZoom } from "@/hooks/use-timeline-zoom"; import { useTranslation } from "react-i18next"; +import { FaCog } from "react-icons/fa"; +import ReviewActivityCalendar from "@/components/overlay/ReviewActivityCalendar"; +import PlatformAwareDialog from "@/components/overlay/dialog/PlatformAwareDialog"; +import MotionPreviewsPane from "./MotionPreviewsPane"; import { EmptyCard } from "@/components/card/EmptyCard"; import { EmptyCardData } from "@/types/card"; @@ -75,6 +102,9 @@ type EventViewProps = { markItemAsReviewed: (review: ReviewSegment) => void; markAllItemsAsReviewed: (currentItems: ReviewSegment[]) => void; onOpenRecording: (recordingInfo: RecordingStartingPoint) => void; + motionPreviewsCamera: string | null; + setMotionPreviewsCamera: (camera: string | null) => void; + setMotionSearchCamera: (camera: string) => void; pullLatestData: () => void; updateFilter: (filter: ReviewFilter) => void; }; @@ -94,6 +124,9 @@ export default function EventView({ markItemAsReviewed, markAllItemsAsReviewed, onOpenRecording, + motionPreviewsCamera, + setMotionPreviewsCamera, + setMotionSearchCamera, pullLatestData, updateFilter, }: EventViewProps) { @@ -274,6 +307,15 @@ export default function EventView({ 100, ); + const motionPreviewsOpen = + severity === "significant_motion" && motionPreviewsCamera != null; + + useEffect(() => { + if (severity !== "significant_motion") { + setMotionPreviewsCamera(null); + } + }, [setMotionPreviewsCamera, severity]); + // review filter info const reviewFilterList = useMemo(() => { @@ -301,124 +343,136 @@ export default function EventView({ return (
-
- {isMobile && ( - - )} - - value ? setSeverityToggle(value) : null - } // don't allow the severity to be unselected - > - + {isMobile && ( + + )} + + value ? setSeverityToggle(value) : null + } // don't allow the severity to be unselected > -
- {reviewCounts.alert > -1 ? ( - reviewCounts.alert - ) : ( - - )} -
-
- -
- {t("alerts")} +
{reviewCounts.alert > -1 ? ( - ` ∙ ${reviewCounts.alert}` + reviewCounts.alert ) : ( - + )}
-
- - -
+ +
+ {t("alerts")} + {reviewCounts.alert > -1 ? ( + ` ∙ ${reviewCounts.alert}` + ) : ( + + )} +
+
+
+ - {reviewCounts.detection > -1 ? ( - reviewCounts.detection - ) : ( - - )} -
-
- -
- {t("detections")} +
{reviewCounts.detection > -1 ? ( - ` ∙ ${reviewCounts.detection}` + reviewCounts.detection ) : ( - + )}
-
- - - -
- -
{t("motion.label")}
-
-
- +
+ +
+ {t("detections")} + {reviewCounts.detection > -1 ? ( + ` ∙ ${reviewCounts.detection}` + ) : ( + + )} +
+
+ + + +
+ +
{t("motion.label")}
+
+
+ - {selectedReviews.length <= 0 ? ( - - ) : ( - + {selectedReviews.length <= 0 ? ( + + ) : ( + + )} +
+ )} + +
- -
+ > {severity != "significant_motion" && ( @@ -898,10 +958,16 @@ type MotionReviewProps = { significant_motion: ReviewSegment[]; }; relevantPreviews?: Preview[]; + reviewSummary?: ReviewSummary; + recordingsSummary?: RecordingsSummary; timeRange: TimeRange; startTime?: number; filter?: ReviewFilter; motionOnly?: boolean; + updateFilter: (filter: ReviewFilter) => void; + motionPreviewsCamera: string | null; + setMotionPreviewsCamera: (camera: string | null) => void; + setMotionSearchCamera: (camera: string) => void; emptyCardData: EmptyCardData; onOpenRecording: (data: RecordingStartingPoint) => void; }; @@ -909,13 +975,20 @@ function MotionReview({ contentRef, reviewItems, relevantPreviews, + reviewSummary, + recordingsSummary, timeRange, startTime, filter, motionOnly = false, + updateFilter, + motionPreviewsCamera, + setMotionPreviewsCamera, + setMotionSearchCamera, emptyCardData, onOpenRecording, }: MotionReviewProps) { + const { t } = useTranslation(["views/events", "common"]); const segmentDuration = 30; const { data: config } = useSWR("config"); @@ -961,6 +1034,15 @@ function MotionReview({ }, ]); + const { data: overlapReviewSegments } = useSWR([ + "review", + { + before: alignedBefore, + after: alignedAfter, + cameras: filter?.cameras?.join(",") ?? null, + }, + ]); + // timeline time const timeRangeSegments = useMemo( @@ -973,19 +1055,29 @@ function MotionReview({ return timeRangeSegments.ranges.length - 1; } - return timeRangeSegments.ranges.findIndex( + const index = timeRangeSegments.ranges.findIndex( (seg) => seg.after <= startTime && seg.before >= startTime, ); + + if (index === -1) { + return timeRangeSegments.ranges.length - 1; + } + + return index; // only render once // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const [selectedRangeIdx, setSelectedRangeIdx] = useState(initialIndex); const [currentTime, setCurrentTime] = useState( - startTime ?? timeRangeSegments.ranges[selectedRangeIdx]?.before, + startTime ?? + timeRangeSegments.ranges[selectedRangeIdx]?.before ?? + timeRangeSegments.end, ); const currentTimeRange = useMemo( - () => timeRangeSegments.ranges[selectedRangeIdx], + () => + timeRangeSegments.ranges[selectedRangeIdx] ?? + timeRangeSegments.ranges[timeRangeSegments.ranges.length - 1], [selectedRangeIdx, timeRangeSegments], ); @@ -1023,18 +1115,86 @@ function MotionReview({ const [playbackRate, setPlaybackRate] = useState(8); const [controlsOpen, setControlsOpen] = useState(false); + const [dimStrength, setDimStrength] = useState(82); + const [isPreviewSettingsOpen, setIsPreviewSettingsOpen] = useState(false); + + const objectReviewItems = useMemo( + () => + (overlapReviewSegments ?? []).filter( + (item) => + item.severity === "alert" || + item.severity === "detection" || + (item.data.detections?.length ?? 0) > 0 || + (item.data.objects?.length ?? 0) > 0, + ), + [overlapReviewSegments], + ); const nextTimestamp = useCameraMotionNextTimestamp( timeRangeSegments.end, segmentDuration, motionOnly, - reviewItems?.all ?? [], + objectReviewItems, motionData ?? [], currentTime, ); const timeoutIdRef = useRef(null); + const selectedMotionPreviewCamera = useMemo( + () => + reviewCameras.find((camera) => camera.name === motionPreviewsCamera) ?? + null, + [motionPreviewsCamera, reviewCameras], + ); + + const onUpdateSelectedDay = useCallback( + (day?: Date) => { + updateFilter({ + ...filter, + after: day == undefined ? undefined : day.getTime() / 1000, + before: day == undefined ? undefined : getEndOfDayTimestamp(day), + }); + }, + [filter, updateFilter], + ); + + const selectedCameraMotionData = useMemo(() => { + if (!motionPreviewsCamera) { + return []; + } + + return (motionData ?? []).filter((item) => { + const cameras = item.camera.split(",").map((camera) => camera.trim()); + return cameras.includes(motionPreviewsCamera); + }); + }, [motionData, motionPreviewsCamera]); + + const selectedCameraReviewItems = useMemo(() => { + if (!motionPreviewsCamera) { + return []; + } + + return objectReviewItems.filter( + (item) => item.camera === motionPreviewsCamera, + ); + }, [motionPreviewsCamera, objectReviewItems]); + + const motionPreviewRanges = useCameraMotionOnlyRanges( + segmentDuration, + selectedCameraReviewItems, + selectedCameraMotionData, + ); + + useEffect(() => { + if ( + motionPreviewsCamera && + !reviewCameras.some((camera) => camera.name === motionPreviewsCamera) + ) { + setMotionPreviewsCamera(null); + } + }, [motionPreviewsCamera, reviewCameras, setMotionPreviewsCamera]); + useEffect(() => { if (nextTimestamp) { if (!playing && timeoutIdRef.current != null) { @@ -1124,132 +1284,349 @@ function MotionReview({ return ( <> -
-
3 && - isMobile && - "portrait:md:grid-cols-2 landscape:md:grid-cols-3", - isDesktop && "grid-cols-2 lg:grid-cols-3", - "gap-2 overflow-auto px-1 md:mx-2 md:gap-4 xl:grid-cols-3 3xl:grid-cols-4", - )} - > - {reviewCameras.map((camera) => { - let grow; - let spans; - const aspectRatio = camera.detect.width / camera.detect.height; - if (aspectRatio > 2) { - grow = "aspect-wide"; - spans = "sm:col-span-2"; - } else if (aspectRatio < 1) { - grow = "h-full aspect-tall"; - spans = "md:row-span-2"; - } else { - grow = "aspect-video"; - } - const detectionType = getDetectionType(camera.name); - return ( -
- {motionData ? ( - <> - { - videoPlayersRef.current[camera.name] = controller; - }} - onClick={() => - onOpenRecording({ - camera: camera.name, - startTime: Math.min( - currentTime, - Date.now() / 1000 - 30, - ), - severity: "significant_motion", - }) - } - /> -
- - ) : ( - + {motionPreviewsCamera && selectedMotionPreviewCamera ? ( + <> +
+ + +
+ {isDesktop && ( + + )} + + + {isDesktop && t("motionPreviews.mobileSettingsTitle")} + + } + content={ +
+ {!isDesktop && ( +
+
+ {t("motionPreviews.mobileSettingsTitle")} +
+
+ {t("motionPreviews.mobileSettingsDesc")} +
+
+ )} + +
+
+
+ {t("motionPreviews.speed")} +
+
+ {t("motionPreviews.speedDesc")} +
+
+ +
+ +
+
+
{t("motionPreviews.dim")}
+
+ {t("motionPreviews.dimDesc")} +
+
+
+ { + const nextValue = values[0]; + if (nextValue == undefined) { + return; + } + + setDimStrength(nextValue); + }} + /> +
+
+ + {!isDesktop && ( + <> + + +
+ { + onUpdateSelectedDay(day); + setIsPreviewSettingsOpen(false); + }} + /> +
+
+ +
+ + )} +
+ } + contentClassName={cn( + isDesktop + ? "w-80" + : "scrollbar-container max-h-[75dvh] overflow-y-auto overflow-x-hidden px-4", )} -
- ); - })} -
-
-
- {motionData ? ( - +
+
+ + { - if (playing && scrubbing) { - setPlaying(false); - } - - setScrubbing(scrubbing); + cameraPreviews={relevantPreviews} + motionRanges={motionPreviewRanges} + isLoadingMotionRanges={ + motionData == undefined || overlapReviewSegments == undefined + } + playbackRate={playbackRate} + nonMotionAlpha={dimStrength / 100} + onSeek={(timestamp) => { + onOpenRecording({ + camera: selectedMotionPreviewCamera.name, + startTime: timestamp, + severity: "significant_motion", + }); }} - dense={isMobileOnly} - isZooming={false} - zoomDirection={null} - alwaysShowMotionLine={true} /> - ) : ( - - )} -
+ + ) : ( +
+
3 && + isMobile && + "portrait:md:grid-cols-2 landscape:md:grid-cols-3", + isDesktop && "grid-cols-2 lg:grid-cols-3", + "gap-2 overflow-auto px-1 md:mx-2 md:gap-4 xl:grid-cols-3 3xl:grid-cols-4", + )} + > + {reviewCameras.map((camera) => { + let grow; + let spans; + const aspectRatio = camera.detect.width / camera.detect.height; + if (aspectRatio > 2) { + grow = "aspect-wide"; + spans = "sm:col-span-2"; + } else if (aspectRatio < 1) { + grow = "h-full aspect-tall"; + spans = "md:row-span-2"; + } else { + grow = "aspect-video"; + } + const detectionType = getDetectionType(camera.name); + return ( +
+ {motionData ? ( + <> + { + videoPlayersRef.current[camera.name] = controller; + }} + onClick={() => + onOpenRecording({ + camera: camera.name, + startTime: Math.min( + currentTime, + Date.now() / 1000 - 30, + ), + severity: "significant_motion", + }) + } + /> +
+
+ + + e.stopPropagation()} + > + + + + + { + e.stopPropagation(); + setMotionPreviewsCamera(camera.name); + }} + > + {t("motionPreviews.menuItem")} + + { + e.stopPropagation(); + setMotionSearchCamera(camera.name); + }} + > + {t("motionSearch.menuItem")} + + + +
+ + ) : ( + + )} +
+ ); + })} +
+
+ )} + {!selectedMotionPreviewCamera && ( +
+ {motionData ? ( + { + if (playing && scrubbing) { + setPlaying(false); + } - { - const wasPlaying = playing; + setScrubbing(scrubbing); + }} + dense={isMobileOnly} + isZooming={false} + zoomDirection={null} + alwaysShowMotionLine={true} + /> + ) : ( + + )} +
+ )} - if (wasPlaying) { - setPlaying(false); - } + {!selectedMotionPreviewCamera && ( + { + const wasPlaying = playing; - setCurrentTime(currentTime + diff); + if (wasPlaying) { + setPlaying(false); + } - if (wasPlaying) { - setTimeout(() => setPlaying(true), 100); - } - }} - onSetPlaybackRate={setPlaybackRate} - /> + setCurrentTime(currentTime + diff); + + if (wasPlaying) { + setTimeout(() => setPlaying(true), 100); + } + }} + onSetPlaybackRate={setPlaybackRate} + /> + )} ); } diff --git a/web/src/views/events/MotionPreviewsPane.tsx b/web/src/views/events/MotionPreviewsPane.tsx new file mode 100644 index 000000000..331a9af33 --- /dev/null +++ b/web/src/views/events/MotionPreviewsPane.tsx @@ -0,0 +1,898 @@ +import { MotionOnlyRange } from "@/hooks/use-camera-activity"; +import { Preview } from "@/types/preview"; +import { + MutableRefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { isCurrentHour } from "@/utils/dateUtil"; +import { useTranslation } from "react-i18next"; +import { CameraConfig } from "@/types/frigateConfig"; +import useSWR from "swr"; +import { baseUrl } from "@/api/baseUrl"; +import { Recording } from "@/types/record"; +import { useResizeObserver } from "@/hooks/resize-observer"; +import { Skeleton } from "@/components/ui/skeleton"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import TimeAgo from "@/components/dynamic/TimeAgo"; +import { useFormattedTimestamp } from "@/hooks/use-date-utils"; +import { FrigateConfig } from "@/types/frigateConfig"; + +const MOTION_HEATMAP_GRID_SIZE = 16; +const MIN_MOTION_CELL_ALPHA = 0.06; + +function getPreviewForMotionRange( + cameraPreviews: Preview[], + cameraName: string, + range: MotionOnlyRange, +) { + const matchingPreviews = cameraPreviews.filter( + (preview) => + preview.camera === cameraName && + preview.end > range.start_time && + preview.start < range.end_time, + ); + + if (!matchingPreviews.length) { + return; + } + + const getOverlap = (preview: Preview) => + Math.max( + 0, + Math.min(preview.end, range.end_time) - + Math.max(preview.start, range.start_time), + ); + + return matchingPreviews.reduce((best, current) => { + return getOverlap(current) > getOverlap(best) ? current : best; + }); +} + +function getRangeOverlapSeconds( + rangeStart: number, + rangeEnd: number, + recordingStart: number, + recordingEnd: number, +) { + return Math.max( + 0, + Math.min(rangeEnd, recordingEnd) - Math.max(rangeStart, recordingStart), + ); +} + +function getMotionHeatmapForRange( + recordings: Recording[], + range: MotionOnlyRange, +) { + const weightedHeatmap = new Map(); + let totalWeight = 0; + + recordings.forEach((recording) => { + const overlapSeconds = getRangeOverlapSeconds( + range.start_time, + range.end_time, + recording.start_time, + recording.end_time, + ); + + if (overlapSeconds <= 0) { + return; + } + + totalWeight += overlapSeconds; + + if (!recording.motion_heatmap) { + return; + } + + Object.entries(recording.motion_heatmap).forEach( + ([cellIndex, intensity]) => { + const index = Number(cellIndex); + const level = Number(intensity); + + if (Number.isNaN(index) || Number.isNaN(level) || level <= 0) { + return; + } + + const existingWeight = weightedHeatmap.get(index) ?? 0; + weightedHeatmap.set(index, existingWeight + level * overlapSeconds); + }, + ); + }); + + if (!totalWeight || weightedHeatmap.size === 0) { + return null; + } + + const mergedHeatmap: Record = {}; + weightedHeatmap.forEach((weightedLevel, index) => { + const normalizedLevel = Math.max( + 0, + Math.min(255, Math.round(weightedLevel / totalWeight)), + ); + + if (normalizedLevel > 0) { + mergedHeatmap[index.toString()] = normalizedLevel; + } + }); + + return Object.keys(mergedHeatmap).length > 0 ? mergedHeatmap : null; +} + +type MotionPreviewClipProps = { + cameraName: string; + range: MotionOnlyRange; + playbackRate: number; + preview?: Preview; + fallbackFrameTimes?: number[]; + motionHeatmap?: Record | null; + nonMotionAlpha: number; + isVisible: boolean; + onSeek: (timestamp: number) => void; +}; + +function MotionPreviewClip({ + cameraName, + range, + playbackRate, + preview, + fallbackFrameTimes, + motionHeatmap, + nonMotionAlpha, + isVisible, + onSeek, +}: MotionPreviewClipProps) { + const { t } = useTranslation(["views/events", "common"]); + const { data: config } = useSWR("config"); + const videoRef = useRef(null); + const dimOverlayCanvasRef = useRef(null); + const overlayContainerRef = useRef(null); + const [{ width: overlayWidth, height: overlayHeight }] = + useResizeObserver(overlayContainerRef); + const [videoLoaded, setVideoLoaded] = useState(false); + const [videoPlaying, setVideoPlaying] = useState(false); + const [fallbackImageLoaded, setFallbackImageLoaded] = useState(false); + const [mediaDimensions, setMediaDimensions] = useState<{ + width: number; + height: number; + } | null>(null); + + const [fallbackFrameIndex, setFallbackFrameIndex] = useState(0); + const [fallbackFramesReady, setFallbackFramesReady] = useState(false); + + const formattedDate = useFormattedTimestamp( + range.start_time, + config?.ui.time_format == "24hour" + ? t("time.formattedTimestampMonthDayHourMinute.24hour", { + ns: "common", + }) + : t("time.formattedTimestampMonthDayHourMinute.12hour", { + ns: "common", + }), + config?.ui.timezone, + ); + const fallbackFrameSrcs = useMemo(() => { + if (!fallbackFrameTimes || fallbackFrameTimes.length === 0) { + return [] as string[]; + } + + return fallbackFrameTimes.map( + (frameTime) => + `${baseUrl}api/preview/preview_${cameraName}-${frameTime}.webp/thumbnail.webp`, + ); + }, [cameraName, fallbackFrameTimes]); + + useEffect(() => { + setFallbackFrameIndex(0); + setFallbackFramesReady(false); + }, [range.start_time, range.end_time, fallbackFrameTimes]); + + useEffect(() => { + if (fallbackFrameSrcs.length === 0) { + setFallbackFramesReady(false); + return; + } + + let cancelled = false; + + const preloadFrames = async () => { + await Promise.allSettled( + fallbackFrameSrcs.map( + (src) => + new Promise((resolve) => { + const image = new Image(); + image.onload = () => resolve(); + image.onerror = () => resolve(); + image.src = src; + }), + ), + ); + + if (!cancelled) { + setFallbackFramesReady(true); + } + }; + + void preloadFrames(); + + return () => { + cancelled = true; + }; + }, [fallbackFrameSrcs]); + + useEffect(() => { + if (!fallbackFramesReady || fallbackFrameSrcs.length <= 1 || !isVisible) { + return; + } + + const intervalMs = Math.max( + 50, + Math.round(1000 / Math.max(1, playbackRate)), + ); + const intervalId = window.setInterval(() => { + setFallbackFrameIndex((previous) => { + return (previous + 1) % fallbackFrameSrcs.length; + }); + }, intervalMs); + + return () => { + window.clearInterval(intervalId); + }; + }, [fallbackFrameSrcs.length, fallbackFramesReady, isVisible, playbackRate]); + + const fallbackFrameSrc = useMemo(() => { + if (fallbackFrameSrcs.length === 0) { + return undefined; + } + + return fallbackFrameSrcs[fallbackFrameIndex] ?? fallbackFrameSrcs[0]; + }, [fallbackFrameIndex, fallbackFrameSrcs]); + + useEffect(() => { + setVideoLoaded(false); + setVideoPlaying(false); + setMediaDimensions(null); + }, [preview?.src]); + + useEffect(() => { + if (!preview || !isVisible || videoLoaded || !videoRef.current) { + return; + } + + if (videoRef.current.currentSrc || videoRef.current.error) { + setVideoLoaded(true); + } + }, [isVisible, preview, videoLoaded]); + + useEffect(() => { + setFallbackImageLoaded(false); + setMediaDimensions(null); + }, [fallbackFrameSrcs]); + + useEffect(() => { + if (!fallbackFrameSrc || !isVisible || !fallbackFramesReady) { + return; + } + + setFallbackImageLoaded(true); + }, [fallbackFrameSrc, fallbackFramesReady, isVisible]); + + const showLoadingIndicator = + (preview != undefined && isVisible && !videoPlaying) || + (fallbackFrameSrc != undefined && isVisible && !fallbackImageLoaded); + + const clipStart = useMemo(() => { + if (!preview) { + return 0; + } + + return Math.max(0, range.start_time - preview.start); + }, [preview, range.start_time]); + + const clipEnd = useMemo(() => { + if (!preview) { + return 0; + } + + const previewDuration = preview.end - preview.start; + return Math.min( + previewDuration, + Math.max(clipStart + 0.1, range.end_time - preview.start), + ); + }, [clipStart, preview, range.end_time]); + + const resetPlayback = useCallback(() => { + if (!videoRef.current || !preview) { + return; + } + + videoRef.current.currentTime = clipStart; + videoRef.current.playbackRate = playbackRate; + }, [clipStart, playbackRate, preview]); + + useEffect(() => { + if (!videoRef.current || !preview) { + return; + } + + if (!isVisible) { + videoRef.current.pause(); + videoRef.current.currentTime = clipStart; + return; + } + + if (videoRef.current.readyState >= 2) { + resetPlayback(); + void videoRef.current.play().catch(() => undefined); + } + }, [clipStart, isVisible, preview, resetPlayback]); + + const drawDimOverlay = useCallback(() => { + if (!dimOverlayCanvasRef.current) { + return; + } + + const canvas = dimOverlayCanvasRef.current; + const context = canvas.getContext("2d"); + + if (!context) { + return; + } + + if (overlayWidth <= 0 || overlayHeight <= 0) { + return; + } + + const width = Math.max(1, overlayWidth); + const height = Math.max(1, overlayHeight); + const dpr = window.devicePixelRatio || 1; + const pixelWidth = Math.max(1, Math.round(width * dpr)); + const pixelHeight = Math.max(1, Math.round(height * dpr)); + + if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { + canvas.width = pixelWidth; + canvas.height = pixelHeight; + } + + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + + context.setTransform(dpr, 0, 0, dpr, 0, 0); + context.clearRect(0, 0, width, height); + + if (!motionHeatmap) { + return; + } + + // Calculate the actual rendered media area (object-contain letterboxing) + let drawX = 0; + let drawY = 0; + let drawWidth = width; + let drawHeight = height; + + if ( + mediaDimensions && + mediaDimensions.width > 0 && + mediaDimensions.height > 0 + ) { + const containerAspect = width / height; + const mediaAspect = mediaDimensions.width / mediaDimensions.height; + + if (mediaAspect < containerAspect) { + // Portrait / tall: constrained by height, bars on left and right + drawHeight = height; + drawWidth = height * mediaAspect; + drawX = (width - drawWidth) / 2; + drawY = 0; + } else { + // Wide / landscape: constrained by width, bars on top and bottom + drawWidth = width; + drawHeight = width / mediaAspect; + drawX = 0; + drawY = (height - drawHeight) / 2; + } + } + + const heatmapLevels = Object.values(motionHeatmap) + .map((value) => Number(value)) + .filter((value) => Number.isFinite(value) && value > 0); + + const maxHeatmapLevel = + heatmapLevels.length > 0 ? Math.max(...heatmapLevels) : 0; + + const maskCanvas = document.createElement("canvas"); + maskCanvas.width = MOTION_HEATMAP_GRID_SIZE; + maskCanvas.height = MOTION_HEATMAP_GRID_SIZE; + + const maskContext = maskCanvas.getContext("2d"); + if (!maskContext) { + return; + } + + const imageData = maskContext.createImageData( + MOTION_HEATMAP_GRID_SIZE, + MOTION_HEATMAP_GRID_SIZE, + ); + + for (let index = 0; index < MOTION_HEATMAP_GRID_SIZE ** 2; index++) { + const level = Number(motionHeatmap[index.toString()] ?? 0); + const normalizedLevel = + maxHeatmapLevel > 0 + ? Math.min(1, Math.max(0, level / maxHeatmapLevel)) + : 0; + const boostedLevel = Math.sqrt(normalizedLevel); + const alpha = + nonMotionAlpha - + boostedLevel * (nonMotionAlpha - MIN_MOTION_CELL_ALPHA); + + const pixelOffset = index * 4; + imageData.data[pixelOffset] = 0; + imageData.data[pixelOffset + 1] = 0; + imageData.data[pixelOffset + 2] = 0; + imageData.data[pixelOffset + 3] = Math.round( + Math.max(0, Math.min(1, alpha)) * 255, + ); + } + + maskContext.putImageData(imageData, 0, 0); + context.imageSmoothingEnabled = true; + context.imageSmoothingQuality = "high"; + context.drawImage(maskCanvas, drawX, drawY, drawWidth, drawHeight); + }, [ + motionHeatmap, + nonMotionAlpha, + overlayHeight, + overlayWidth, + mediaDimensions, + ]); + + useEffect(() => { + drawDimOverlay(); + }, [drawDimOverlay]); + + return ( +
onSeek(range.start_time)} + > + {showLoadingIndicator && ( + + )} + {preview ? ( + <> + + {motionHeatmap && ( +
+ ); +} + +type MotionPreviewsPaneProps = { + camera: CameraConfig; + contentRef: MutableRefObject; + cameraPreviews: Preview[]; + motionRanges: MotionOnlyRange[]; + isLoadingMotionRanges?: boolean; + playbackRate: number; + nonMotionAlpha: number; + onSeek: (timestamp: number) => void; +}; + +export default function MotionPreviewsPane({ + camera, + contentRef, + cameraPreviews, + motionRanges, + isLoadingMotionRanges = false, + playbackRate, + nonMotionAlpha, + onSeek, +}: MotionPreviewsPaneProps) { + const { t } = useTranslation(["views/events"]); + const [scrollContainer, setScrollContainer] = useState( + null, + ); + + const [windowVisible, setWindowVisible] = useState(true); + useEffect(() => { + const visibilityListener = () => { + setWindowVisible(document.visibilityState == "visible"); + }; + + addEventListener("visibilitychange", visibilityListener); + + return () => { + removeEventListener("visibilitychange", visibilityListener); + }; + }, []); + + const [visibleClips, setVisibleClips] = useState([]); + const [hasVisibilityData, setHasVisibilityData] = useState(false); + const clipObserver = useRef(null); + + const recordingTimeRange = useMemo(() => { + if (!motionRanges.length) { + return null; + } + + return motionRanges.reduce( + (bounds, range) => ({ + after: Math.min(bounds.after, range.start_time), + before: Math.max(bounds.before, range.end_time), + }), + { + after: motionRanges[0].start_time, + before: motionRanges[0].end_time, + }, + ); + }, [motionRanges]); + + const { data: cameraRecordings } = useSWR( + recordingTimeRange + ? [ + `${camera.name}/recordings`, + { + after: Math.floor(recordingTimeRange.after), + before: Math.ceil(recordingTimeRange.before), + }, + ] + : null, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + }, + ); + const { data: previewFrames } = useSWR( + recordingTimeRange + ? `preview/${camera.name}/start/${Math.floor(recordingTimeRange.after)}/end/${Math.ceil(recordingTimeRange.before)}/frames` + : null, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + }, + ); + + const previewFrameTimes = useMemo(() => { + if (!previewFrames) { + return [] as number[]; + } + + return previewFrames + .map((frame) => { + const timestampPart = frame.split("-").at(-1)?.replace(".webp", ""); + return timestampPart ? Number(timestampPart) : NaN; + }) + .filter((value) => Number.isFinite(value)) + .sort((a, b) => a - b); + }, [previewFrames]); + + const getFallbackFrameTimesForRange = useCallback( + (range: MotionOnlyRange) => { + if (!isCurrentHour(range.end_time) || previewFrameTimes.length === 0) { + return [] as number[]; + } + + const inRangeFrames = previewFrameTimes.filter( + (frameTime) => + frameTime >= range.start_time && frameTime <= range.end_time, + ); + + // Use all in-range frames when enough data exists for natural animation + if (inRangeFrames.length > 1) { + return inRangeFrames; + } + + // If sparse, keep the single in-range frame and add only the next 2 frames + if (inRangeFrames.length === 1) { + const inRangeFrame = inRangeFrames[0]; + const nextFrames = previewFrameTimes + .filter((frameTime) => frameTime > inRangeFrame) + .slice(0, 2); + + return [inRangeFrame, ...nextFrames]; + } + + const nextFramesFromStart = previewFrameTimes + .filter((frameTime) => frameTime >= range.start_time) + .slice(0, 3); + // If no in-range frame exists, take up to 3 frames starting at clip start + if (nextFramesFromStart.length > 0) { + return nextFramesFromStart; + } + + const lastFrame = previewFrameTimes.at(-1); + return lastFrame != undefined ? [lastFrame] : []; + }, + [previewFrameTimes], + ); + + const setContentNode = useCallback( + (node: HTMLDivElement | null) => { + contentRef.current = node; + setScrollContainer(node); + }, + [contentRef], + ); + + useEffect(() => { + if (!scrollContainer) { + return; + } + + const visibleClipIds = new Set(); + clipObserver.current = new IntersectionObserver( + (entries) => { + setHasVisibilityData(true); + + entries.forEach((entry) => { + const clipId = (entry.target as HTMLElement).dataset.clipId; + + if (!clipId) { + return; + } + + if (entry.isIntersecting) { + visibleClipIds.add(clipId); + } else { + visibleClipIds.delete(clipId); + } + }); + + const rootRect = scrollContainer.getBoundingClientRect(); + const prunedVisibleClipIds = [...visibleClipIds].filter((clipId) => { + const clipElement = scrollContainer.querySelector( + `[data-clip-id="${clipId}"]`, + ); + + if (!clipElement) { + return false; + } + + const clipRect = clipElement.getBoundingClientRect(); + + return ( + clipRect.bottom > rootRect.top && clipRect.top < rootRect.bottom + ); + }); + + setVisibleClips(prunedVisibleClipIds); + }, + { + root: scrollContainer, + threshold: 0, + }, + ); + + scrollContainer + .querySelectorAll("[data-clip-id]") + .forEach((node) => { + clipObserver.current?.observe(node); + }); + + return () => { + clipObserver.current?.disconnect(); + }; + }, [scrollContainer]); + + const clipRef = useCallback((node: HTMLElement | null) => { + if (!clipObserver.current) { + return; + } + + try { + if (node) { + clipObserver.current.observe(node); + } + } catch { + // no op + } + }, []); + + const clipData = useMemo( + () => + motionRanges + .filter((range) => range.end_time > range.start_time) + .sort((left, right) => right.start_time - left.start_time) + .map((range) => { + const preview = getPreviewForMotionRange( + cameraPreviews, + camera.name, + range, + ); + + return { + range, + preview, + fallbackFrameTimes: !preview + ? getFallbackFrameTimesForRange(range) + : undefined, + motionHeatmap: getMotionHeatmapForRange( + cameraRecordings ?? [], + range, + ), + }; + }), + [ + cameraPreviews, + camera.name, + cameraRecordings, + getFallbackFrameTimesForRange, + motionRanges, + ], + ); + + const hasCurrentHourRanges = useMemo( + () => motionRanges.some((range) => isCurrentHour(range.end_time)), + [motionRanges], + ); + + const isLoadingPane = + isLoadingMotionRanges || + (motionRanges.length > 0 && cameraRecordings == undefined) || + (hasCurrentHourRanges && previewFrames == undefined); + + if (isLoadingPane) { + return ( + + ); + } + + return ( +
+
+ {clipData.length === 0 ? ( +
+ {t("motionPreviews.empty")} +
+ ) : ( +
+ {clipData.map( + ({ range, preview, fallbackFrameTimes, motionHeatmap }, idx) => ( +
+ +
+ ), + )} +
+ )} +
+
+ ); +} 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 && (
void; + config: FrigateConfig; + cameras: string[]; + selectedCamera: string | null; + onCameraSelect: (camera: string) => void; + cameraLocked?: boolean; + polygonPoints: number[][]; + setPolygonPoints: React.Dispatch>; + isDrawingROI: boolean; + setIsDrawingROI: React.Dispatch>; + parallelMode: boolean; + setParallelMode: React.Dispatch>; + threshold: number; + setThreshold: React.Dispatch>; + minArea: number; + setMinArea: React.Dispatch>; + frameSkip: number; + setFrameSkip: React.Dispatch>; + maxResults: number; + setMaxResults: React.Dispatch>; + searchRange?: TimeRange; + setSearchRange: React.Dispatch>; + defaultRange: TimeRange; + isSearching: boolean; + canStartSearch: boolean; + onStartSearch: () => void; + timezone?: string; +}; + +export default function MotionSearchDialog({ + open, + onOpenChange, + config, + cameras, + selectedCamera, + onCameraSelect, + cameraLocked = false, + polygonPoints, + setPolygonPoints, + isDrawingROI, + setIsDrawingROI, + parallelMode, + setParallelMode, + threshold, + setThreshold, + minArea, + setMinArea, + frameSkip, + setFrameSkip, + maxResults, + setMaxResults, + searchRange, + setSearchRange, + defaultRange, + isSearching, + canStartSearch, + onStartSearch, + timezone, +}: MotionSearchDialogProps) { + const { t } = useTranslation(["views/motionSearch", "common"]); + const apiHost = useApiHost(); + const containerRef = useRef(null); + const [{ width: containerWidth, height: containerHeight }] = + useResizeObserver(containerRef); + const [imageLoaded, setImageLoaded] = useState(false); + + const cameraConfig = useMemo(() => { + if (!selectedCamera) return undefined; + return config.cameras[selectedCamera]; + }, [config, selectedCamera]); + + const polygonClosed = useMemo( + () => !isDrawingROI && polygonPoints.length >= 3, + [isDrawingROI, polygonPoints.length], + ); + + const undoPolygonPoint = useCallback(() => { + if (polygonPoints.length === 0 || isSearching) { + return; + } + + setPolygonPoints((prev) => prev.slice(0, -1)); + setIsDrawingROI(true); + }, [isSearching, setIsDrawingROI, setPolygonPoints, polygonPoints.length]); + + const resetPolygon = useCallback(() => { + if (polygonPoints.length === 0 || isSearching) { + return; + } + + setPolygonPoints([]); + setIsDrawingROI(true); + }, [isSearching, polygonPoints.length, setIsDrawingROI, setPolygonPoints]); + + const imageSize = useMemo(() => { + if (!containerWidth || !containerHeight || !cameraConfig) { + return { width: 0, height: 0 }; + } + + const cameraAspectRatio = + cameraConfig.detect.width / cameraConfig.detect.height; + const availableAspectRatio = containerWidth / containerHeight; + + if (availableAspectRatio >= cameraAspectRatio) { + return { + width: containerHeight * cameraAspectRatio, + height: containerHeight, + }; + } + + return { + width: containerWidth, + height: containerWidth / cameraAspectRatio, + }; + }, [containerWidth, containerHeight, cameraConfig]); + + useEffect(() => { + setImageLoaded(false); + }, [selectedCamera]); + + const Overlay = isDesktop ? Dialog : Drawer; + const Content = isDesktop ? DialogContent : DrawerContent; + + return ( + + event.preventDefault(), + } + : {})} + className={cn( + isDesktop + ? "scrollbar-container max-h-[90dvh] overflow-y-auto sm:max-w-[75%]" + : "flex max-h-[90dvh] flex-col overflow-hidden rounded-lg pb-4", + )} + > +
+ + + {t("dialog.title")} + +

+ {t("description")} +

+
+ +
+
+ {(!cameraLocked || !selectedCamera) && ( +
+
+
+ + +
+
+
+ )} + + +
+ +
+ {selectedCamera && cameraConfig && imageSize.width > 0 ? ( +
+ {t("dialog.previewAlt", setImageLoaded(true)} + /> + {!imageLoaded && ( +
+ +
+ )} + +
+ ) : ( +
+ {t("selectCamera")} +
+ )} +
+
+
+
+ + {selectedCamera && ( +
+
+ {t("polygonControls.points", { + count: polygonPoints.length, + })} + {polygonClosed && } +
+
+ + + + + + {t("polygonControls.undo")} + + + + + + + + {t("polygonControls.reset")} + + +
+
+ )} +
+ +
+
+

+ {t("settings.title")} +

+
+
+ +
+ setThreshold(value)} + /> + {threshold} +
+

+ {t("settings.thresholdDesc")} +

+
+
+ +
+ setMinArea(value)} + /> + {minArea}% +
+

+ {t("settings.minAreaDesc")} +

+
+
+ +
+ setFrameSkip(value)} + /> + {frameSkip} +
+

+ {t("settings.frameSkipDesc")} +

+
+
+
+ + +
+

+ {t("settings.parallelModeDesc")} +

+
+
+ +
+ setMaxResults(value)} + /> + {maxResults} +
+

+ {t("settings.maxResultsDesc")} +

+
+
+
+ + + + +
+
+
+
+
+ ); +} + +type SearchRangeSelectorProps = { + range?: TimeRange; + setRange: React.Dispatch>; + defaultRange: TimeRange; + timeFormat?: "browser" | "12hour" | "24hour"; + timezone?: string; +}; + +function SearchRangeSelector({ + range, + setRange, + defaultRange, + timeFormat, + timezone, +}: SearchRangeSelectorProps) { + const { t } = useTranslation(["views/motionSearch", "common"]); + const [startOpen, setStartOpen] = useState(false); + const [endOpen, setEndOpen] = useState(false); + + const timezoneOffset = useMemo( + () => + timezone ? Math.round(getUTCOffset(new Date(), timezone)) : undefined, + [timezone], + ); + const localTimeOffset = useMemo( + () => + Math.round( + getUTCOffset( + new Date(), + Intl.DateTimeFormat().resolvedOptions().timeZone, + ), + ), + [], + ); + + const startTime = useMemo(() => { + let time = range?.after ?? defaultRange.after; + + if (timezoneOffset !== undefined) { + time = time + (timezoneOffset - localTimeOffset) * 60; + } + + return time; + }, [range, defaultRange, timezoneOffset, localTimeOffset]); + + const endTime = useMemo(() => { + let time = range?.before ?? defaultRange.before; + + if (timezoneOffset !== undefined) { + time = time + (timezoneOffset - localTimeOffset) * 60; + } + + return time; + }, [range, defaultRange, timezoneOffset, localTimeOffset]); + + const formattedStart = useFormattedTimestamp( + startTime, + timeFormat === "24hour" + ? t("time.formattedTimestamp.24hour", { ns: "common" }) + : t("time.formattedTimestamp.12hour", { ns: "common" }), + ); + const formattedEnd = useFormattedTimestamp( + endTime, + timeFormat === "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]); + + return ( +
+ +
+ +
+ { + if (!open) { + setStartOpen(false); + } + }} + modal={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); + } + }} + modal={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/views/motion-search/MotionSearchROICanvas.tsx b/web/src/views/motion-search/MotionSearchROICanvas.tsx new file mode 100644 index 000000000..f393a9cfb --- /dev/null +++ b/web/src/views/motion-search/MotionSearchROICanvas.tsx @@ -0,0 +1,398 @@ +import { useCallback, useMemo, useRef } from "react"; +import { Stage, Layer, Line, Circle, Image } from "react-konva"; +import Konva from "konva"; +import type { KonvaEventObject } from "konva/lib/Node"; +import { flattenPoints } from "@/utils/canvasUtil"; +import { cn } from "@/lib/utils"; +import { useResizeObserver } from "@/hooks/resize-observer"; + +type MotionSearchROICanvasProps = { + camera: string; + width: number; + height: number; + polygonPoints: number[][]; + setPolygonPoints: React.Dispatch>; + isDrawing: boolean; + setIsDrawing: React.Dispatch>; + isInteractive?: boolean; + motionHeatmap?: Record | null; + showMotionHeatmap?: boolean; +}; + +export default function MotionSearchROICanvas({ + width, + height, + polygonPoints, + setPolygonPoints, + isDrawing, + setIsDrawing, + isInteractive = true, + motionHeatmap, + showMotionHeatmap = false, +}: MotionSearchROICanvasProps) { + const containerRef = useRef(null); + const stageRef = useRef(null); + const [{ width: containerWidth, height: containerHeight }] = + useResizeObserver(containerRef); + + const stageSize = useMemo( + () => ({ + width: containerWidth > 0 ? Math.ceil(containerWidth) : 0, + height: containerHeight > 0 ? Math.ceil(containerHeight) : 0, + }), + [containerHeight, containerWidth], + ); + + const videoRect = useMemo(() => { + const stageWidth = stageSize.width; + const stageHeight = stageSize.height; + const sourceWidth = width > 0 ? width : 1; + const sourceHeight = height > 0 ? height : 1; + + if (stageWidth <= 0 || stageHeight <= 0) { + return { x: 0, y: 0, width: 0, height: 0 }; + } + + const stageAspect = stageWidth / stageHeight; + const sourceAspect = sourceWidth / sourceHeight; + + if (stageAspect > sourceAspect) { + const fittedHeight = stageHeight; + const fittedWidth = fittedHeight * sourceAspect; + return { + x: (stageWidth - fittedWidth) / 2, + y: 0, + width: fittedWidth, + height: fittedHeight, + }; + } + + const fittedWidth = stageWidth; + const fittedHeight = fittedWidth / sourceAspect; + return { + x: 0, + y: (stageHeight - fittedHeight) / 2, + width: fittedWidth, + height: fittedHeight, + }; + }, [height, stageSize.height, stageSize.width, width]); + + // Convert normalized points to stage coordinates + const scaledPoints = useMemo(() => { + return polygonPoints.map((point) => [ + videoRect.x + point[0] * videoRect.width, + videoRect.y + point[1] * videoRect.height, + ]); + }, [ + polygonPoints, + videoRect.height, + videoRect.width, + videoRect.x, + videoRect.y, + ]); + + const flattenedPoints = useMemo( + () => flattenPoints(scaledPoints), + [scaledPoints], + ); + + const heatmapOverlayCanvas = useMemo(() => { + if ( + !showMotionHeatmap || + !motionHeatmap || + videoRect.width === 0 || + videoRect.height === 0 + ) { + return null; + } + + const gridSize = 16; + const heatmapLevels = Object.values(motionHeatmap) + .map((value) => Number(value)) + .filter((value) => Number.isFinite(value) && value > 0); + + const maxHeatmapLevel = + heatmapLevels.length > 0 ? Math.max(...heatmapLevels) : 0; + + if (maxHeatmapLevel <= 0) { + return null; + } + + const maskCanvas = document.createElement("canvas"); + maskCanvas.width = gridSize; + maskCanvas.height = gridSize; + + const maskContext = maskCanvas.getContext("2d"); + if (!maskContext) { + return null; + } + + const imageData = maskContext.createImageData(gridSize, gridSize); + const heatmapStops = [ + { t: 0, r: 0, g: 0, b: 255 }, + { t: 0.25, r: 0, g: 255, b: 255 }, + { t: 0.5, r: 0, g: 255, b: 0 }, + { t: 0.75, r: 255, g: 255, b: 0 }, + { t: 1, r: 255, g: 0, b: 0 }, + ]; + + const getHeatmapColor = (value: number) => { + const clampedValue = Math.min(1, Math.max(0, value)); + + const upperIndex = heatmapStops.findIndex( + (stop) => stop.t >= clampedValue, + ); + if (upperIndex <= 0) { + return heatmapStops[0]; + } + + const lower = heatmapStops[upperIndex - 1]; + const upper = heatmapStops[upperIndex]; + const range = upper.t - lower.t; + const blend = range > 0 ? (clampedValue - lower.t) / range : 0; + + return { + r: Math.round(lower.r + (upper.r - lower.r) * blend), + g: Math.round(lower.g + (upper.g - lower.g) * blend), + b: Math.round(lower.b + (upper.b - lower.b) * blend), + }; + }; + + for (let index = 0; index < gridSize ** 2; index++) { + const level = Number(motionHeatmap[index.toString()] ?? 0); + const normalizedLevel = + level > 0 ? Math.min(1, Math.max(0, level / maxHeatmapLevel)) : 0; + const alpha = + level > 0 + ? Math.min(0.95, Math.max(0.1, 0.15 + normalizedLevel * 0.5)) + : 0; + const color = getHeatmapColor(normalizedLevel); + + const pixelOffset = index * 4; + imageData.data[pixelOffset] = color.r; + imageData.data[pixelOffset + 1] = color.g; + imageData.data[pixelOffset + 2] = color.b; + imageData.data[pixelOffset + 3] = Math.round(alpha * 255); + } + + maskContext.putImageData(imageData, 0, 0); + + return maskCanvas; + }, [motionHeatmap, showMotionHeatmap, videoRect.height, videoRect.width]); + + // Handle mouse click to add point + const handleMouseDown = useCallback( + (e: KonvaEventObject) => { + if (!isInteractive || !isDrawing) return; + if (videoRect.width <= 0 || videoRect.height <= 0) return; + + const stage = e.target.getStage(); + if (!stage) return; + + const mousePos = stage.getPointerPosition(); + if (!mousePos) return; + + const intersection = stage.getIntersection(mousePos); + + // If clicking on first point and we have at least 3 points, close the polygon + if (polygonPoints.length >= 3 && intersection?.name() === "point-0") { + setIsDrawing(false); + return; + } + + // Only add point if not clicking on an existing point + if (intersection?.getClassName() !== "Circle") { + const clampedX = Math.min( + Math.max(mousePos.x, videoRect.x), + videoRect.x + videoRect.width, + ); + const clampedY = Math.min( + Math.max(mousePos.y, videoRect.y), + videoRect.y + videoRect.height, + ); + + // Convert to normalized coordinates (0-1) + const normalizedX = (clampedX - videoRect.x) / videoRect.width; + const normalizedY = (clampedY - videoRect.y) / videoRect.height; + + setPolygonPoints([...polygonPoints, [normalizedX, normalizedY]]); + } + }, + [ + isDrawing, + polygonPoints, + setPolygonPoints, + setIsDrawing, + isInteractive, + videoRect.height, + videoRect.width, + videoRect.x, + videoRect.y, + ], + ); + + // Handle point drag + const handlePointDragMove = useCallback( + (e: KonvaEventObject, index: number) => { + if (!isInteractive) return; + const stage = e.target.getStage(); + if (!stage) return; + + const pos = { x: e.target.x(), y: e.target.y() }; + + // Constrain to fitted video boundaries + pos.x = Math.max( + videoRect.x, + Math.min(pos.x, videoRect.x + videoRect.width), + ); + pos.y = Math.max( + videoRect.y, + Math.min(pos.y, videoRect.y + videoRect.height), + ); + + // Convert to normalized coordinates + const normalizedX = (pos.x - videoRect.x) / videoRect.width; + const normalizedY = (pos.y - videoRect.y) / videoRect.height; + + const newPoints = [...polygonPoints]; + newPoints[index] = [normalizedX, normalizedY]; + setPolygonPoints(newPoints); + }, + [ + polygonPoints, + setPolygonPoints, + isInteractive, + videoRect.height, + videoRect.width, + videoRect.x, + videoRect.y, + ], + ); + + // Handle right-click to delete point + const handleContextMenu = useCallback( + (e: KonvaEventObject, index: number) => { + if (!isInteractive) return; + e.evt.preventDefault(); + + if (polygonPoints.length <= 3 && !isDrawing) { + // Don't delete if we have a closed polygon with minimum points + return; + } + + const newPoints = polygonPoints.filter((_, i) => i !== index); + setPolygonPoints(newPoints); + + // If we deleted enough points, go back to drawing mode + if (newPoints.length < 3) { + setIsDrawing(true); + } + }, + [polygonPoints, isDrawing, setPolygonPoints, setIsDrawing, isInteractive], + ); + + // Handle mouse hover on first point + const handleMouseOverPoint = useCallback( + (e: KonvaEventObject, index: number) => { + if (!isInteractive) return; + if (!isDrawing || polygonPoints.length < 3 || index !== 0) return; + e.target.scale({ x: 2, y: 2 }); + }, + [isDrawing, isInteractive, polygonPoints.length], + ); + + const handleMouseOutPoint = useCallback( + (e: KonvaEventObject, index: number) => { + if (!isInteractive) return; + if (index === 0) { + e.target.scale({ x: 1, y: 1 }); + } + }, + [isInteractive], + ); + + const vertexRadius = 6; + const polygonColorString = "rgba(66, 135, 245, 0.8)"; + const polygonFillColor = "rgba(66, 135, 245, 0.2)"; + + return ( +
+ {stageSize.width > 0 && stageSize.height > 0 && ( + e.evt.preventDefault()} + className="absolute inset-0" + > + + {/* Segment heatmap overlay */} + {heatmapOverlayCanvas && ( + + )} + + {/* Polygon outline */} + {scaledPoints.length > 0 && ( + = 3} + fill={ + !isDrawing && scaledPoints.length >= 3 + ? polygonFillColor + : undefined + } + /> + )} + + {/* Draw line from last point to cursor when drawing */} + {isDrawing && scaledPoints.length > 0 && ( + + )} + + {/* Vertex points */} + {scaledPoints.map((point, index) => ( + handlePointDragMove(e, index)} + onMouseOver={(e) => handleMouseOverPoint(e, index)} + onMouseOut={(e) => handleMouseOutPoint(e, index)} + onContextMenu={(e) => handleContextMenu(e, index)} + /> + ))} + + + )} +
+ ); +} diff --git a/web/src/views/motion-search/MotionSearchView.tsx b/web/src/views/motion-search/MotionSearchView.tsx new file mode 100644 index 000000000..6789dad89 --- /dev/null +++ b/web/src/views/motion-search/MotionSearchView.tsx @@ -0,0 +1,1491 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import axios from "axios"; +import { isDesktop, isMobile } from "react-device-detect"; +import Logo from "@/components/Logo"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { TimeRange } from "@/types/timeline"; +import { RecordingsSummary } from "@/types/review"; +import { ExportMode } from "@/types/filter"; +import { + MotionSearchRequest, + MotionSearchStartResponse, + MotionSearchStatusResponse, + MotionSearchResult, + MotionSearchMetrics, +} from "@/types/motionSearch"; + +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; +import { Toaster } from "@/components/ui/sonner"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; +import { Skeleton } from "@/components/ui/skeleton"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Progress } from "@/components/ui/progress"; + +import DynamicVideoPlayer from "@/components/player/dynamic/DynamicVideoPlayer"; +import { DynamicVideoController } from "@/components/player/dynamic/DynamicVideoController"; +import { DetailStreamProvider } from "@/context/detail-stream-context"; +import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline"; +import CalendarFilterButton from "@/components/filter/CalendarFilterButton"; +import ExportDialog from "@/components/overlay/ExportDialog"; +import SaveExportOverlay from "@/components/overlay/SaveExportOverlay"; +import ReviewActivityCalendar from "@/components/overlay/ReviewActivityCalendar"; +import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; +import { SelectSeparator } from "@/components/ui/select"; + +import { useResizeObserver } from "@/hooks/resize-observer"; +import { useFullscreen } from "@/hooks/use-fullscreen"; +import { useTimelineZoom } from "@/hooks/use-timeline-zoom"; +import { useTimelineUtils } from "@/hooks/use-timeline-utils"; +import { useCameraPreviews } from "@/hooks/use-camera-previews"; +import { getChunkedTimeDay } from "@/utils/timelineUtil"; + +import { MotionData, ZoomLevel } from "@/types/review"; +import { + ASPECT_VERTICAL_LAYOUT, + ASPECT_WIDE_LAYOUT, + Recording, + RecordingSegment, +} from "@/types/record"; +import { VideoResolutionType } from "@/types/live"; +import { useFormattedTimestamp } from "@/hooks/use-date-utils"; +import MotionSearchROICanvas from "./MotionSearchROICanvas"; +import MotionSearchDialog from "./MotionSearchDialog"; +import { IoMdArrowRoundBack } from "react-icons/io"; +import { FaArrowDown, FaCalendarAlt, FaCog, FaFire } from "react-icons/fa"; +import { useNavigate } from "react-router-dom"; +import { LuSearch } from "react-icons/lu"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; + +type MotionSearchViewProps = { + config: FrigateConfig; + cameras: string[]; + selectedCamera: string | null; + onCameraSelect: (camera: string) => void; + cameraLocked?: boolean; + selectedDay: Date | undefined; + onDaySelect: (day: Date | undefined) => void; + timeRange: TimeRange; + timezone: string | undefined; + onBack?: () => void; +}; + +const DEFAULT_EXPORT_WINDOW_SECONDS = 60; + +export default function MotionSearchView({ + config, + cameras, + selectedCamera, + onCameraSelect, + cameraLocked = false, + selectedDay, + onDaySelect, + timeRange, + timezone, + onBack, +}: MotionSearchViewProps) { + const { t } = useTranslation([ + "views/motionSearch", + "common", + "views/recording", + ]); + const navigate = useNavigate(); + + const resultTimestampFormat = useMemo( + () => + config.ui?.time_format === "24hour" + ? t("time.formattedTimestamp.24hour", { ns: "common" }) + : t("time.formattedTimestamp.12hour", { ns: "common" }), + [config.ui?.time_format, t], + ); + + // Refs + const contentRef = useRef(null); + const mainLayoutRef = useRef(null); + const timelineRef = useRef(null); + const mainControllerRef = useRef(null); + const jobIdRef = useRef(null); + const jobCameraRef = useRef(null); + + const [isSearchDialogOpen, setIsSearchDialogOpen] = useState(true); + const [isMobileSettingsOpen, setIsMobileSettingsOpen] = useState(false); + const [mobileSettingsMode, setMobileSettingsMode] = useState< + "actions" | "calendar" + >("actions"); + + // Recordings summary for calendar – defer until dialog is closed + // so the preview image in the dialog loads without competing requests + const { data: recordingsSummary } = useSWR( + selectedCamera && !isSearchDialogOpen + ? [ + "recordings/summary", + { + timezone: timezone, + cameras: selectedCamera, + }, + ] + : null, + ); + + // Camera previews – defer until dialog is closed + const allPreviews = useCameraPreviews( + isSearchDialogOpen ? { after: 0, before: 0 } : timeRange, + { + camera: selectedCamera ?? undefined, + }, + ); + + // ROI state + const [polygonPoints, setPolygonPoints] = useState([]); + const [isDrawingROI, setIsDrawingROI] = useState(true); + + // Search settings + const [parallelMode, setParallelMode] = useState(false); + const [threshold, setThreshold] = useState(30); + const [minArea, setMinArea] = useState(20); + const [frameSkip, setFrameSkip] = useState(10); + const [maxResults, setMaxResults] = useState(25); + + // Job state + const [jobId, setJobId] = useState(null); + const [jobCamera, setJobCamera] = useState(null); + + // Job polling with SWR + const { data: jobStatus } = useSWR( + jobId && jobCamera ? [`${jobCamera}/search/motion/${jobId}`] : null, + { refreshInterval: 1000 }, + ); + + // Search state + const [isSearching, setIsSearching] = useState(false); + const [searchResults, setSearchResults] = useState([]); + const [showSegmentHeatmap, setShowSegmentHeatmap] = useState(false); + const [searchMetrics, setSearchMetrics] = + useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [searchRange, setSearchRange] = useState( + undefined, + ); + const [pendingSeekTime, setPendingSeekTime] = useState(null); + + // Export state + const [exportMode, setExportMode] = useState("none"); + const [exportRange, setExportRange] = useState(); + const [showExportPreview, setShowExportPreview] = useState(false); + + // Timeline state + const initialStartTime = timeRange.before - 60; + const [scrubbing, setScrubbing] = useState(false); + const [currentTime, setCurrentTime] = useState(initialStartTime); + const [playerTime, setPlayerTime] = useState(initialStartTime); + const [playbackStart, setPlaybackStart] = useState(initialStartTime); + + const chunkedTimeRange = useMemo( + () => getChunkedTimeDay(timeRange), + [timeRange], + ); + + const [selectedRangeIdx, setSelectedRangeIdx] = useState(() => { + const ranges = getChunkedTimeDay(timeRange); + const index = ranges.findIndex( + (chunk) => + chunk.after <= initialStartTime && chunk.before >= initialStartTime, + ); + return index === -1 ? ranges.length - 1 : index; + }); + + const currentTimeRange = useMemo( + () => + chunkedTimeRange[selectedRangeIdx] ?? + chunkedTimeRange[chunkedTimeRange.length - 1], + [selectedRangeIdx, chunkedTimeRange], + ); + + const clampExportTime = useCallback( + (value: number) => + Math.min(timeRange.before, Math.max(timeRange.after, value)), + [timeRange.after, timeRange.before], + ); + + const buildDefaultExportRange = useCallback( + (anchorTime: number): TimeRange => { + const halfWindow = DEFAULT_EXPORT_WINDOW_SECONDS / 2; + let after = clampExportTime(anchorTime - halfWindow); + let before = clampExportTime(anchorTime + halfWindow); + + if (before <= after) { + before = clampExportTime(timeRange.before); + after = clampExportTime(before - DEFAULT_EXPORT_WINDOW_SECONDS); + } + + return { after, before }; + }, + [clampExportTime, timeRange.before], + ); + + const setExportStartTime = useCallback< + React.Dispatch> + >( + (value) => { + setExportRange((prev) => { + const resolvedValue = + typeof value === "function" + ? value(prev?.after ?? currentTime) + : value; + const after = clampExportTime(resolvedValue); + const before = Math.max( + after, + clampExportTime( + prev?.before ?? after + DEFAULT_EXPORT_WINDOW_SECONDS, + ), + ); + return { after, before }; + }); + }, + [clampExportTime, currentTime], + ); + + const setExportEndTime = useCallback< + React.Dispatch> + >( + (value) => { + setExportRange((prev) => { + const resolvedValue = + typeof value === "function" + ? value(prev?.before ?? currentTime) + : value; + const before = clampExportTime(resolvedValue); + const after = Math.min( + before, + clampExportTime( + prev?.after ?? before - DEFAULT_EXPORT_WINDOW_SECONDS, + ), + ); + return { after, before }; + }); + }, + [clampExportTime, currentTime], + ); + + useEffect(() => { + if (exportMode !== "timeline" || exportRange) { + return; + } + + setExportRange(buildDefaultExportRange(currentTime)); + }, [exportMode, exportRange, buildDefaultExportRange, currentTime]); + + const handleExportPreview = useCallback(() => { + if (!exportRange) { + toast.error( + t("export.toast.error.noVaildTimeSelected", { + ns: "components/dialog", + }), + { + position: "top-center", + }, + ); + return; + } + + setShowExportPreview(true); + }, [exportRange, setShowExportPreview, t]); + + const handleExportCancel = useCallback(() => { + setShowExportPreview(false); + setExportRange(undefined); + setExportMode("none"); + }, [setExportMode, setExportRange, setShowExportPreview]); + + const setExportRangeWithPause = useCallback( + (range: TimeRange | undefined) => { + setExportRange(range); + + if (range != undefined) { + mainControllerRef.current?.pause(); + } + }, + [setExportRange], + ); + + const openMobileExport = useCallback(() => { + const now = new Date(timeRange.before * 1000); + now.setHours(now.getHours() - 1); + + setExportRangeWithPause({ + before: timeRange.before, + after: now.getTime() / 1000, + }); + setExportMode("select"); + setIsMobileSettingsOpen(false); + setMobileSettingsMode("actions"); + }, [setExportRangeWithPause, timeRange.before]); + + const handleExportSave = useCallback(() => { + if (!exportRange || !selectedCamera) { + toast.error( + t("export.toast.error.noVaildTimeSelected", { + ns: "components/dialog", + }), + { + position: "top-center", + }, + ); + return; + } + + if (exportRange.before < exportRange.after) { + toast.error( + t("export.toast.error.endTimeMustAfterStartTime", { + ns: "components/dialog", + }), + { position: "top-center" }, + ); + return; + } + + axios + .post( + `export/${selectedCamera}/start/${Math.round(exportRange.after)}/end/${Math.round(exportRange.before)}`, + { + playback: "realtime", + }, + ) + .then((response) => { + if (response.status == 200) { + toast.success( + t("export.toast.success", { ns: "components/dialog" }), + { + position: "top-center", + action: ( + + + + ), + }, + ); + setShowExportPreview(false); + setExportRange(undefined); + setExportMode("none"); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error( + t("export.toast.error.failed", { + ns: "components/dialog", + error: errorMessage, + }), + { position: "top-center" }, + ); + }); + }, [ + exportRange, + selectedCamera, + setExportMode, + setExportRange, + setShowExportPreview, + t, + ]); + + useEffect(() => { + if (!searchRange) { + setSearchRange(timeRange); + } + }, [searchRange, timeRange]); + + // Video player state + const [fullResolution, setFullResolution] = useState({ + width: 0, + height: 0, + }); + + // Fullscreen + const { fullscreen, toggleFullscreen, supportsFullScreen } = + useFullscreen(mainLayoutRef); + + // Timeline zoom settings + const [zoomSettings, setZoomSettings] = useState({ + segmentDuration: 30, + timestampSpread: 15, + }); + + const possibleZoomLevels: ZoomLevel[] = useMemo( + () => [ + { segmentDuration: 30, timestampSpread: 15 }, + { segmentDuration: 15, timestampSpread: 5 }, + { segmentDuration: 5, timestampSpread: 1 }, + ], + [], + ); + + const handleZoomChange = useCallback( + (newZoomLevel: number) => { + setZoomSettings(possibleZoomLevels[newZoomLevel]); + }, + [possibleZoomLevels], + ); + + const currentZoomLevel = useMemo( + () => + possibleZoomLevels.findIndex( + (level) => level.segmentDuration === zoomSettings.segmentDuration, + ), + [possibleZoomLevels, zoomSettings.segmentDuration], + ); + + const { isZooming, zoomDirection } = useTimelineZoom({ + zoomSettings, + zoomLevels: possibleZoomLevels, + onZoomChange: handleZoomChange, + timelineRef: timelineRef, + timelineDuration: timeRange.after - timeRange.before, + }); + + // Motion data for timeline + const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( + { segmentDuration: zoomSettings.segmentDuration }, + ); + + const alignedAfter = alignStartDateToTimeline(timeRange.after); + const alignedBefore = alignEndDateToTimeline(timeRange.before); + + const { data: motionData, isLoading: isMotionLoading } = useSWR( + selectedCamera && !isSearchDialogOpen + ? [ + "review/activity/motion", + { + before: alignedBefore, + after: alignedAfter, + scale: Math.round(zoomSettings.segmentDuration / 2), + cameras: selectedCamera, + }, + ] + : null, + ); + + const { data: noRecordings } = useSWR( + selectedCamera && !isSearchDialogOpen + ? [ + "recordings/unavailable", + { + before: alignedBefore, + after: alignedAfter, + scale: Math.round(zoomSettings.segmentDuration), + cameras: selectedCamera, + }, + ] + : null, + ); + + const recordingParams = useMemo( + () => ({ + before: currentTimeRange.before, + after: currentTimeRange.after, + }), + [currentTimeRange], + ); + + const { data: playbackRecordings } = useSWR( + selectedCamera && !isSearchDialogOpen + ? [`${selectedCamera}/recordings`, recordingParams] + : null, + { revalidateOnFocus: false }, + ); + + const activeSegmentHeatmap = useMemo(() => { + if (!showSegmentHeatmap || !playbackRecordings?.length) { + return null; + } + + const activeSegment = playbackRecordings.find( + (recording) => + recording.start_time <= currentTime && + recording.end_time >= currentTime, + ); + + return activeSegment?.motion_heatmap ?? null; + }, [currentTime, playbackRecordings, showSegmentHeatmap]); + + // Camera aspect ratio + const getCameraAspect = useCallback( + (cam: string) => { + if (!config) return undefined; + if ( + cam === selectedCamera && + fullResolution.width && + fullResolution.height + ) { + return fullResolution.width / fullResolution.height; + } + const camera = config.cameras[cam]; + if (!camera) return undefined; + return camera.detect.width / camera.detect.height; + }, + [config, fullResolution, selectedCamera], + ); + + const mainCameraAspect = useMemo(() => { + if (!selectedCamera) return "normal"; + const aspectRatio = getCameraAspect(selectedCamera); + if (!aspectRatio) return "normal"; + if (aspectRatio > ASPECT_WIDE_LAYOUT) return "wide"; + if (aspectRatio < ASPECT_VERTICAL_LAYOUT) return "tall"; + return "normal"; + }, [getCameraAspect, selectedCamera]); + + const grow = useMemo(() => { + if (mainCameraAspect === "wide") return "w-full aspect-wide"; + if (mainCameraAspect === "tall") { + return isDesktop + ? "size-full aspect-tall flex flex-col justify-center" + : "size-full"; + } + return "w-full aspect-video"; + }, [mainCameraAspect]); + + // Container resize observer + const [{ width: containerWidth, height: containerHeight }] = + useResizeObserver(mainLayoutRef); + + const useHeightBased = useMemo(() => { + if (!containerWidth || !containerHeight || !selectedCamera) return false; + const cameraAspectRatio = getCameraAspect(selectedCamera); + if (!cameraAspectRatio) return false; + const availableAspectRatio = containerWidth / containerHeight; + return availableAspectRatio >= cameraAspectRatio; + }, [containerWidth, containerHeight, getCameraAspect, selectedCamera]); + + const onClipEnded = useCallback(() => { + if (!mainControllerRef.current) { + return; + } + + if (selectedRangeIdx < chunkedTimeRange.length - 1) { + setSelectedRangeIdx(selectedRangeIdx + 1); + } + }, [selectedRangeIdx, chunkedTimeRange]); + + const updateSelectedSegment = useCallback( + (nextTime: number, updateStartTime: boolean) => { + const index = chunkedTimeRange.findIndex( + (segment) => segment.after <= nextTime && segment.before >= nextTime, + ); + + if (index != -1) { + if (updateStartTime) { + setPlaybackStart(nextTime); + } + + setSelectedRangeIdx(index); + } + }, + [chunkedTimeRange], + ); + + // Handle scrubbing + useEffect(() => { + if (scrubbing || exportRange) { + if ( + currentTime > currentTimeRange.before + 60 || + currentTime < currentTimeRange.after - 60 + ) { + updateSelectedSegment(currentTime, false); + return; + } + + mainControllerRef.current?.scrubToTimestamp(currentTime); + } + // we only want to seek when current time updates + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + currentTime, + scrubbing, + timeRange, + currentTimeRange, + updateSelectedSegment, + ]); + + useEffect(() => { + if (pendingSeekTime != null) { + return; + } + + const nextTime = timeRange.before - 60; + const index = chunkedTimeRange.findIndex( + (segment) => segment.after <= nextTime && segment.before >= nextTime, + ); + + setCurrentTime(nextTime); + setPlayerTime(nextTime); + setPlaybackStart(nextTime); + setSelectedRangeIdx(index === -1 ? chunkedTimeRange.length - 1 : index); + mainControllerRef.current?.seekToTimestamp(nextTime, true); + }, [pendingSeekTime, timeRange, chunkedTimeRange]); + + useEffect(() => { + if (!scrubbing) { + if (Math.abs(currentTime - playerTime) > 10) { + if ( + currentTimeRange.after <= currentTime && + currentTimeRange.before >= currentTime + ) { + mainControllerRef.current?.seekToTimestamp(currentTime, true); + } else { + updateSelectedSegment(currentTime, true); + } + } else if (playerTime != currentTime) { + mainControllerRef.current?.play(); + } + } + // we only want to seek when current time doesn't match the player update time + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentTime, scrubbing, playerTime]); + + // Manually seek to timestamp + const manuallySetCurrentTime = useCallback( + (time: number, play: boolean = false) => { + if (!currentTimeRange) { + return; + } + + setCurrentTime(time); + + if (currentTimeRange.after <= time && currentTimeRange.before >= time) { + mainControllerRef.current?.seekToTimestamp(time, play); + } else { + updateSelectedSegment(time, true); + } + }, + [currentTimeRange, updateSelectedSegment], + ); + + const canStartSearch = Boolean( + selectedCamera && + searchRange && + searchRange.before >= searchRange.after && + polygonPoints.length >= 3 && + !isDrawingROI, + ); + + const cancelMotionSearchJob = useCallback( + async (jobIdToCancel: string | null, cameraToCancel: string | null) => { + if (!jobIdToCancel || !cameraToCancel) { + return; + } + + try { + await axios.post( + `${cameraToCancel}/search/motion/${jobIdToCancel}/cancel`, + ); + } catch { + // Best effort cancellation. + } + }, + [], + ); + + const cancelMotionSearchJobViaBeacon = useCallback( + (jobIdToCancel: string | null, cameraToCancel: string | null) => { + if (!jobIdToCancel || !cameraToCancel) { + return; + } + + const url = `${window.location.origin}/api/${cameraToCancel}/search/motion/${jobIdToCancel}/cancel`; + + const xhr = new XMLHttpRequest(); + try { + xhr.open("POST", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.setRequestHeader("X-CSRF-TOKEN", "1"); + xhr.setRequestHeader("X-CACHE-BYPASS", "1"); + xhr.withCredentials = true; + xhr.send("{}"); + } catch { + // Best effort cancellation during unload. + } + }, + [], + ); + + useEffect(() => { + jobIdRef.current = jobId; + }, [jobId]); + + useEffect(() => { + jobCameraRef.current = jobCamera; + }, [jobCamera]); + + useEffect(() => { + return () => { + cancelMotionSearchJobViaBeacon(jobIdRef.current, jobCameraRef.current); + void cancelMotionSearchJob(jobIdRef.current, jobCameraRef.current); + }; + }, [cancelMotionSearchJob, cancelMotionSearchJobViaBeacon]); + + useEffect(() => { + const handleBeforeUnload = () => { + cancelMotionSearchJobViaBeacon(jobIdRef.current, jobCameraRef.current); + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, [cancelMotionSearchJobViaBeacon]); + + const handleNewSearch = useCallback(() => { + if (jobId && jobCamera) { + void cancelMotionSearchJob(jobId, jobCamera); + if (isSearching) { + toast.message(t("searchCancelled")); + } + } + setSearchResults([]); + setSearchMetrics(null); + setIsSearching(false); + setJobId(null); + setJobCamera(null); + setHasSearched(false); + setPendingSeekTime(null); + setSearchRange(timeRange); + setIsSearchDialogOpen(true); + }, [cancelMotionSearchJob, isSearching, jobCamera, jobId, t, timeRange]); + + // Perform motion search + const performSearch = useCallback(async () => { + if (!selectedCamera) { + toast.error(t("errors.noCamera")); + return; + } + + if (polygonPoints.length < 3) { + toast.error(t("errors.polygonTooSmall")); + return; + } + + if (!searchRange) { + toast.error(t("errors.noTimeRange")); + return; + } + + if (searchRange.before < searchRange.after) { + toast.error(t("errors.invalidTimeRange")); + return; + } + + setIsSearching(true); + setSearchResults([]); + setHasSearched(true); + + try { + const request: MotionSearchRequest = { + start_time: searchRange.after, + end_time: searchRange.before, + polygon_points: polygonPoints, + parallel: parallelMode, + threshold, + min_area: minArea, + frame_skip: frameSkip, + max_results: maxResults, + }; + + const response = await axios.post( + `${selectedCamera}/search/motion`, + request, + ); + + if (response.data.success) { + setJobId(response.data.job_id); + setJobCamera(selectedCamera); + setIsSearchDialogOpen(false); + toast.success(t("searchStarted")); + } else { + toast.error( + t("errors.searchFailed", { message: response.data.message }), + ); + setIsSearching(false); + } + } catch (error) { + let errorMessage = t("errors.unknown"); + + if (axios.isAxiosError<{ message?: string; detail?: string }>(error)) { + const responseData = error.response?.data as + | { + message?: unknown; + detail?: unknown; + error?: unknown; + errors?: unknown; + } + | string + | undefined; + + if (typeof responseData === "string") { + errorMessage = responseData; + } else if (responseData) { + const apiMessage = + responseData.message ?? + responseData.detail ?? + responseData.error ?? + responseData.errors; + + if (Array.isArray(apiMessage)) { + errorMessage = apiMessage.join(", "); + } else if (typeof apiMessage === "string") { + errorMessage = apiMessage; + } else if (apiMessage) { + errorMessage = JSON.stringify(apiMessage); + } else { + errorMessage = error.message || errorMessage; + } + } else { + errorMessage = error.message || errorMessage; + } + } else if (error instanceof Error) { + errorMessage = error.message; + } + + toast.error(t("errors.searchFailed", { message: errorMessage })); + setIsSearching(false); + } + }, [ + selectedCamera, + polygonPoints, + searchRange, + parallelMode, + threshold, + minArea, + frameSkip, + maxResults, + t, + ]); + + // Monitor job status and update UI when complete + useEffect(() => { + if (!jobStatus) { + return; + } + + if (jobStatus.status === "success") { + setSearchResults(jobStatus.results ?? []); + setSearchMetrics(jobStatus.metrics ?? null); + setIsSearching(false); + setJobId(null); + setJobCamera(null); + toast.success( + t("changesFound", { count: jobStatus.results?.length ?? 0 }), + ); + } else if ( + jobStatus.status === "queued" || + jobStatus.status === "running" + ) { + setSearchMetrics(jobStatus.metrics ?? null); + // Stream partial results as they arrive + if (jobStatus.results && jobStatus.results.length > 0) { + setSearchResults(jobStatus.results); + } + } else if (jobStatus.status === "failed") { + setIsSearching(false); + setJobId(null); + setJobCamera(null); + toast.error( + t("errors.searchFailed", { + message: jobStatus.error_message || jobStatus.message, + }), + ); + } else if (jobStatus.status === "cancelled") { + setIsSearching(false); + setJobId(null); + setJobCamera(null); + toast.message(t("searchCancelled")); + } + }, [jobStatus, t]); + + // Handle result click + const handleResultClick = useCallback( + (result: MotionSearchResult) => { + if ( + result.timestamp < timeRange.after || + result.timestamp > timeRange.before + ) { + setPendingSeekTime(result.timestamp); + onDaySelect(new Date(result.timestamp * 1000)); + return; + } + + manuallySetCurrentTime(result.timestamp, true); + }, + [manuallySetCurrentTime, onDaySelect, timeRange], + ); + + useEffect(() => { + if (pendingSeekTime == null) { + return; + } + + if ( + pendingSeekTime >= timeRange.after && + pendingSeekTime <= timeRange.before + ) { + manuallySetCurrentTime(pendingSeekTime, true); + setPendingSeekTime(null); + } + }, [pendingSeekTime, timeRange, manuallySetCurrentTime]); + + if (!selectedCamera) { + return ( +
+

{t("selectCamera")}

+
+ ); + } + + const timelinePanel = ( + <> +
+
+ + + + {!isMotionLoading ? ( + setScrubbing(dragging)} + showExportHandles={exportMode === "timeline" && Boolean(exportRange)} + exportStartTime={exportRange?.after} + exportEndTime={exportRange?.before} + setExportStartTime={setExportStartTime} + setExportEndTime={setExportEndTime} + isZooming={isZooming} + zoomDirection={zoomDirection} + onZoomChange={handleZoomChange} + possibleZoomLevels={possibleZoomLevels} + currentZoomLevel={currentZoomLevel} + /> + ) : ( + + )} + + ); + + const progressMetrics = jobStatus?.metrics ?? searchMetrics; + const progressValue = + progressMetrics && progressMetrics.segments_scanned > 0 + ? Math.min( + 100, + (progressMetrics.segments_processed / + progressMetrics.segments_scanned) * + 100, + ) + : 0; + + const resultsPanel = ( + <> +
+

{t("results")}

+
+ + + {isSearching && ( +
+
+
+ +
{t("searching")}
+
+ +
+ +
+ )} + {searchMetrics && searchResults.length > 0 && ( +
+
+
+ {t("metrics.segmentsScanned")} + + {searchMetrics.segments_scanned} + +
+ {searchMetrics.segments_processed > 0 && ( +
+ {t("metrics.segmentsProcessed")} + + {searchMetrics.segments_processed} + +
+ )} + {searchMetrics.metadata_inactive_segments > 0 && ( +
+ {t("metrics.segmentsSkippedInactive")} + + {searchMetrics.metadata_inactive_segments} + +
+ )} + {searchMetrics.heatmap_roi_skip_segments > 0 && ( +
+ {t("metrics.segmentsSkippedHeatmap")} + + {searchMetrics.heatmap_roi_skip_segments} + +
+ )} + {searchMetrics.fallback_full_range_segments > 0 && ( +
+ {t("metrics.fallbackFullRange")} + + {searchMetrics.fallback_full_range_segments} + +
+ )} +
+ {t("metrics.framesDecoded")} + + {searchMetrics.frames_decoded} + +
+
+ {t("metrics.wallTime")} + + {t("metrics.seconds", { + seconds: searchMetrics.wall_time_seconds.toFixed(1), + })} + +
+ {searchMetrics.segments_with_errors > 0 && ( +
+ {t("metrics.segmentErrors")} + + {searchMetrics.segments_with_errors} + +
+ )} +
+
+ )} + + {searchResults.length === 0 && !isSearching ? ( +
+ {hasSearched ? t("noChangesFound") : t("noResultsYet")} +
+ ) : searchResults.length > 0 ? ( +
+ {searchResults.map((result, index) => ( + handleResultClick(result)} + /> + ))} +
+ ) : null} +
+ + ); + + return ( + +
+ + + + {/* Header */} +
+ {isMobile && ( + + )} + {(cameraLocked || onBack) && ( +
+ +
+ )} +
+
+ + +
+ + {isDesktop ? ( + <> + + + + ) : ( + { + setIsMobileSettingsOpen(open); + + if (!open) { + setMobileSettingsMode("actions"); + } + }} + > + + + + + {mobileSettingsMode == "actions" ? ( +
+ + +
+ ) : ( +
+
+
setMobileSettingsMode("actions")} + > + {t("button.back", { ns: "common" })} +
+
+ {t("calendar", { ns: "views/recording" })} +
+
+
+ { + onDaySelect(day); + setIsMobileSettingsOpen(false); + setMobileSettingsMode("actions"); + }} + /> +
+ +
+ +
+
+ )} +
+
+ )} + +
+
+ + {!isDesktop && ( +
+ +
+ )} + + {/* Main Content */} +
+ {/* Video Player with ROI Canvas */} +
+
+ {/* Video Player */} + { + setPlayerTime(timestamp); + setCurrentTime(timestamp); + }} + onClipEnded={onClipEnded} + onSeekToTime={manuallySetCurrentTime} + onControllerReady={(controller) => { + mainControllerRef.current = controller; + }} + isScrubbing={scrubbing || exportMode == "timeline"} + supportsFullscreen={supportsFullScreen} + setFullResolution={setFullResolution} + toggleFullscreen={toggleFullscreen} + containerRef={mainLayoutRef} + transformedOverlay={ + + } + /> +
+
+ + {isDesktop ? ( + <> +
+ {timelinePanel} +
+ +
{resultsPanel}
+ + ) : ( +
+
+ {timelinePanel} +
+ +
+ {resultsPanel} +
+
+ )} +
+
+
+ ); +} + +type SearchResultItemProps = { + result: MotionSearchResult; + timezone: string | undefined; + timestampFormat: string; + onClick: () => void; +}; + +function SearchResultItem({ + result, + timezone, + timestampFormat, + onClick, +}: SearchResultItemProps) { + const { t } = useTranslation(["views/motionSearch"]); + const formattedTime = useFormattedTimestamp( + result.timestamp, + timestampFormat, + timezone, + ); + + return ( + + ); +} 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/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")} + + + + + + ); +}