From 1f1d546326ffaa10144f78067050dd655b1b4266 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 1 Mar 2026 14:41:33 -0600 Subject: [PATCH 1/9] fix masks and zones layout issues at high browser zoom levels (#22181) --- web/src/components/settings/PolygonCanvas.tsx | 2 +- web/src/components/settings/PolygonItem.tsx | 16 +++++-- web/src/views/settings/MasksAndZonesView.tsx | 43 +++++++++++++++---- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/web/src/components/settings/PolygonCanvas.tsx b/web/src/components/settings/PolygonCanvas.tsx index 30b203bd3..0db8db258 100644 --- a/web/src/components/settings/PolygonCanvas.tsx +++ b/web/src/components/settings/PolygonCanvas.tsx @@ -258,7 +258,7 @@ export function PolygonCanvas({ const updatedPolygons = [...polygons]; const activePolygon = updatedPolygons[activePolygonIndex]; - if (containerRef.current && !activePolygon.isFinished) { + if (containerRef.current && activePolygon && !activePolygon.isFinished) { containerRef.current.style.cursor = "crosshair"; } }; diff --git a/web/src/components/settings/PolygonItem.tsx b/web/src/components/settings/PolygonItem.tsx index 8fc6b639e..13522f9fc 100644 --- a/web/src/components/settings/PolygonItem.tsx +++ b/web/src/components/settings/PolygonItem.tsx @@ -329,7 +329,7 @@ export default function PolygonItem({
setHoveredPolygonIndex(index)} onMouseLeave={() => setHoveredPolygonIndex(null)} @@ -341,7 +341,7 @@ export default function PolygonItem({ }} >
)} {!isMobile && hoveredPolygonIndex === index && ( -
+
(null); const [editPane, setEditPane] = useState(undefined); + const editPaneRef = useRef(editPane); + editPaneRef.current = editPane; + const prevScaledRef = useRef<{ w: number; h: number } | null>(null); const [activeLine, setActiveLine] = useState(); const [snapPoints, setSnapPoints] = useState(false); @@ -350,12 +353,36 @@ export default function MasksAndZonesView({ ...globalObjectMasks, ...objectMasks, ]); - setEditingPolygons([ - ...zones, - ...motionMasks, - ...globalObjectMasks, - ...objectMasks, - ]); + // Don't overwrite editingPolygons during editing – layout shifts + // from switching to the edit pane can trigger a resize which + // recalculates scaledWidth/scaledHeight and would discard the + // newly-added polygon. Instead, rescale existing points + // proportionally. + if (editPaneRef.current === undefined) { + setEditingPolygons([ + ...zones, + ...motionMasks, + ...globalObjectMasks, + ...objectMasks, + ]); + } else if ( + prevScaledRef.current && + (prevScaledRef.current.w !== scaledWidth || + prevScaledRef.current.h !== scaledHeight) + ) { + const prevW = prevScaledRef.current.w; + const prevH = prevScaledRef.current.h; + setEditingPolygons((prev) => + prev.map((poly) => ({ + ...poly, + points: poly.points.map(([x, y]) => [ + (x / prevW) * scaledWidth, + (y / prevH) * scaledHeight, + ]), + })), + ); + } + prevScaledRef.current = { w: scaledWidth, h: scaledHeight }; } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps @@ -431,7 +458,7 @@ export default function MasksAndZonesView({ {cameraConfig && editingPolygons && (
-
+
{editPane == "zone" && ( From c3c27d036f12ae54ebac19d5747b82354f02091f Mon Sep 17 00:00:00 2001 From: Michal Srb Date: Tue, 3 Mar 2026 14:29:57 +0100 Subject: [PATCH 2/9] Hide hidden camera alerts (#22226) Cameras that have `ui.dashboard = false` config are hidden from the All Cameras "default" group, but their alerts still appear in the top row. This hides the alerts as well. One can still view the hidden cameras and their alerts by making a custom camera group. --- web/src/views/live/LiveDashboardView.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index dd8bb83f5..a25741f63 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -92,10 +92,17 @@ export default function LiveDashboardView({ const eventUpdate = useFrigateReviews(); const alertCameras = useMemo(() => { - if (!config || cameraGroup == "default") { + if (!config) { return null; } + if (cameraGroup == "default") { + return Object.values(config.cameras) + .filter((cam) => cam.ui.dashboard) + .map((cam) => cam.name) + .join(","); + } + if (includeBirdseye && cameras.length == 0) { return Object.values(config.cameras) .filter((cam) => cam.birdseye.enabled) From 5e7d4267687ee4d212b4fb6153e21125ffe7d9a3 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 4 Mar 2026 08:59:12 -0600 Subject: [PATCH 3/9] Add fullscreen controls to tracking details videos (#22252) --- web/src/components/overlay/detail/TrackingDetails.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/components/overlay/detail/TrackingDetails.tsx b/web/src/components/overlay/detail/TrackingDetails.tsx index 03b6dd6c4..3dad95621 100644 --- a/web/src/components/overlay/detail/TrackingDetails.tsx +++ b/web/src/components/overlay/detail/TrackingDetails.tsx @@ -1,6 +1,7 @@ import useSWR from "swr"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useResizeObserver } from "@/hooks/resize-observer"; +import { useFullscreen } from "@/hooks/use-fullscreen"; import { Event } from "@/types/event"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { TrackingDetailsSequence } from "@/types/timeline"; @@ -243,6 +244,8 @@ export function TrackingDetails({ }, [manualOverride, currentTime, annotationOffset]); const containerRef = useRef(null); + const { fullscreen, toggleFullscreen, supportsFullScreen } = + useFullscreen(containerRef); const timelineContainerRef = useRef(null); const rowRefs = useRef<(HTMLDivElement | null)[]>([]); const [_selectedZone, setSelectedZone] = useState(""); @@ -559,14 +562,15 @@ export function TrackingDetails({ visible={true} currentSource={videoSource} hotKeys={false} - supportsFullscreen={false} - fullscreen={false} + supportsFullscreen={supportsFullScreen} + fullscreen={fullscreen} frigateControls={true} onTimeUpdate={handleTimeUpdate} onSeekToTime={handleSeekToTime} onUploadFrame={onUploadFrameToPlus} onPlaying={() => setIsVideoLoading(false)} setFullResolution={setFullResolution} + toggleFullscreen={toggleFullscreen} isDetailMode={true} camera={event.camera} currentTimeOverride={currentTime} From 95956a690b376d39e6db0f632aec7b3e687becd9 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:07:34 -0600 Subject: [PATCH 4/9] Debug replay (#22212) * debug replay implementation * fix masks after dev rebase * fix squash merge issues * fix * fix * fix * no need to write debug replay camera to config * camera and filter button and dropdown * add filters * add ability to edit motion and object config for debug replay * add debug draw overlay to debug replay * add guard to prevent crash when camera is no longer in camera_states * fix overflow due to radix absolutely positioned elements * increase number of messages * ensure deep_merge replaces existing list values when override is true * add back button * add debug replay to explore and review menus * clean up * clean up * update instructions to prevent exposing exception info * fix typing * refactor output logic * refactor with helper function * move init to function for consistency --- .github/copilot-instructions.md | 16 + docs/docs/configuration/genai/objects.md | 2 +- frigate/api/app.py | 94 ++- frigate/api/debug_replay.py | 176 +++++ frigate/api/defs/request/app_body.py | 1 + frigate/api/fastapi_app.py | 5 + frigate/app.py | 14 + frigate/camera/activity_manager.py | 18 +- frigate/camera/maintainer.py | 71 +- frigate/comms/config_updater.py | 8 +- frigate/comms/dispatcher.py | 15 +- frigate/comms/inter_process.py | 8 +- frigate/comms/zmq_proxy.py | 10 +- frigate/config/camera/updater.py | 4 +- frigate/const.py | 2 + frigate/debug_replay.py | 443 +++++++++++ frigate/embeddings/maintainer.py | 9 +- frigate/events/maintainer.py | 9 +- frigate/output/birdseye.py | 10 +- frigate/output/output.py | 74 +- frigate/record/maintainer.py | 6 +- frigate/review/maintainer.py | 3 + frigate/stats/util.py | 3 + frigate/storage.py | 10 +- frigate/test/http_api/base_http_test.py | 2 + frigate/test/http_api/test_http_app.py | 29 + frigate/test/test_config.py | 16 + frigate/timeline.py | 4 +- frigate/track/object_processing.py | 18 +- frigate/track/tracked_object.py | 5 +- frigate/util/builtin.py | 5 +- frigate/util/config.py | 76 ++ web/public/locales/en/common.json | 2 + web/public/locales/en/views/explore.json | 4 + web/public/locales/en/views/replay.json | 54 ++ web/public/locales/en/views/settings.json | 1 + web/public/locales/en/views/system.json | 32 +- web/src/App.tsx | 4 +- web/src/api/ws.tsx | 53 +- web/src/components/camera/CameraImage.tsx | 9 +- .../config-form/section-configs/motion.ts | 24 + .../config-form/section-configs/objects.ts | 22 + .../config-form/section-configs/types.ts | 1 + .../config-form/sections/BaseSection.tsx | 123 +-- .../widgets/AudioLabelSwitchesWidget.tsx | 2 +- .../widgets/ObjectLabelSwitchesWidget.tsx | 2 +- .../theme/widgets/ZoneSwitchesWidget.tsx | 2 +- .../components/menu/SearchResultActions.tsx | 76 +- .../components/overlay/ActionsDropdown.tsx | 46 ++ .../components/overlay/CustomTimeSelector.tsx | 240 ++++++ .../components/overlay/DebugReplayDialog.tsx | 367 +++++++++ web/src/components/overlay/ExportDialog.tsx | 294 +------ .../overlay/MobileReviewSettingsDrawer.tsx | 188 ++++- web/src/components/timeline/EventMenu.tsx | 74 +- web/src/components/ws/WsMessageFeed.tsx | 608 +++++++++++++++ web/src/components/ws/WsMessageRow.tsx | 433 +++++++++++ web/src/hooks/use-allowed-cameras.ts | 9 +- web/src/hooks/use-camera-activity.ts | 16 +- web/src/hooks/use-stats.ts | 32 +- web/src/hooks/use-ws-message-buffer.ts | 99 +++ web/src/pages/Exports.tsx | 2 +- web/src/pages/Logs.tsx | 218 +++--- web/src/pages/Replay.tsx | 725 ++++++++++++++++++ web/src/types/log.ts | 2 +- web/src/utils/cameraUtil.ts | 12 + web/src/utils/configUtil.ts | 9 +- web/src/utils/wsUtil.ts | 53 ++ web/src/views/recording/RecordingView.tsx | 87 ++- 68 files changed, 4572 insertions(+), 519 deletions(-) create mode 100644 frigate/api/debug_replay.py create mode 100644 frigate/debug_replay.py create mode 100644 web/public/locales/en/views/replay.json create mode 100644 web/src/components/overlay/ActionsDropdown.tsx create mode 100644 web/src/components/overlay/CustomTimeSelector.tsx create mode 100644 web/src/components/overlay/DebugReplayDialog.tsx create mode 100644 web/src/components/ws/WsMessageFeed.tsx create mode 100644 web/src/components/ws/WsMessageRow.tsx create mode 100644 web/src/hooks/use-ws-message-buffer.ts create mode 100644 web/src/pages/Replay.tsx create mode 100644 web/src/utils/wsUtil.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f053abe3f..0af9c249f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -324,6 +324,12 @@ try: value = await sensor.read() except Exception: # ❌ Too broad logger.error("Failed") + +# Returning exceptions in JSON responses +except ValueError as e: + return JSONResponse( + content={"success": False, "message": str(e)}, + ) ``` ### ✅ Use These Instead @@ -353,6 +359,16 @@ try: value = await sensor.read() except SensorException as err: # ✅ Specific logger.exception("Failed to read sensor") + +# Safe error responses +except ValueError: + logger.exception("Invalid parameters for API request") + return JSONResponse( + content={ + "success": False, + "message": "Invalid request parameters", + }, + ) ``` ## Project-Specific Conventions diff --git a/docs/docs/configuration/genai/objects.md b/docs/docs/configuration/genai/objects.md index c878f5ec8..3ed826d21 100644 --- a/docs/docs/configuration/genai/objects.md +++ b/docs/docs/configuration/genai/objects.md @@ -75,4 +75,4 @@ Many providers also have a public facing chat interface for their models. Downlo - OpenAI - [ChatGPT](https://chatgpt.com) - Gemini - [Google AI Studio](https://aistudio.google.com) -- Ollama - [Open WebUI](https://docs.openwebui.com/) \ No newline at end of file +- Ollama - [Open WebUI](https://docs.openwebui.com/) diff --git a/frigate/api/app.py b/frigate/api/app.py index 04d1c2238..a28f174de 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -49,12 +49,13 @@ from frigate.stats.prometheus import get_metrics, update_metrics from frigate.types import JobStatusTypesEnum from frigate.util.builtin import ( clean_camera_user_pass, + deep_merge, flatten_config_data, load_labels, process_config_query_string, update_yaml_file_bulk, ) -from frigate.util.config import find_config_file +from frigate.util.config import apply_section_update, find_config_file from frigate.util.schema import get_config_schema from frigate.util.services import ( get_nvidia_driver_info, @@ -422,9 +423,100 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")): ) +def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONResponse: + """Apply config changes in-memory only, without writing to YAML. + + Used for temporary config changes like debug replay camera tuning. + Updates the in-memory Pydantic config and publishes ZMQ updates, + bypassing YAML parsing entirely. + """ + try: + updates = {} + if body.config_data: + updates = flatten_config_data(body.config_data) + updates = {k: ("" if v is None else v) for k, v in updates.items()} + + if not updates: + return JSONResponse( + content={"success": False, "message": "No configuration data provided"}, + status_code=400, + ) + + config: FrigateConfig = request.app.frigate_config + + # Group flat key paths into nested per-camera, per-section dicts + grouped: dict[str, dict[str, dict]] = {} + for key_path, value in updates.items(): + parts = key_path.split(".") + if len(parts) < 3 or parts[0] != "cameras": + continue + + cam, section = parts[1], parts[2] + grouped.setdefault(cam, {}).setdefault(section, {}) + + # Build nested dict from remaining path (e.g. "filters.person.threshold") + target = grouped[cam][section] + for part in parts[3:-1]: + target = target.setdefault(part, {}) + if len(parts) > 3: + target[parts[-1]] = value + elif isinstance(value, dict): + grouped[cam][section] = deep_merge( + grouped[cam][section], value, override=True + ) + else: + grouped[cam][section] = value + + # Apply each section update + for cam_name, sections in grouped.items(): + camera_config = config.cameras.get(cam_name) + if not camera_config: + return JSONResponse( + content={ + "success": False, + "message": f"Camera '{cam_name}' not found", + }, + status_code=400, + ) + + for section_name, update in sections.items(): + err = apply_section_update(camera_config, section_name, update) + if err is not None: + return JSONResponse( + content={"success": False, "message": err}, + status_code=400, + ) + + # Publish ZMQ updates so processing threads pick up changes + if body.update_topic and body.update_topic.startswith("config/cameras/"): + _, _, camera, field = body.update_topic.split("/") + settings = getattr(config.cameras.get(camera, None), field, None) + + if settings is not None: + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum[field], camera), + settings, + ) + + return JSONResponse( + content={"success": True, "message": "Config applied in-memory"}, + status_code=200, + ) + except Exception as e: + logger.error(f"Error applying config in-memory: {e}") + return JSONResponse( + content={"success": False, "message": "Error applying config"}, + status_code=500, + ) + + @router.put("/config/set", dependencies=[Depends(require_role(["admin"]))]) def config_set(request: Request, body: AppConfigSetBody): config_file = find_config_file() + + if body.skip_save: + return _config_set_in_memory(request, body) + lock = FileLock(f"{config_file}.lock", timeout=5) try: diff --git a/frigate/api/debug_replay.py b/frigate/api/debug_replay.py new file mode 100644 index 000000000..027d4e50c --- /dev/null +++ b/frigate/api/debug_replay.py @@ -0,0 +1,176 @@ +"""Debug replay API endpoints.""" + +import asyncio +import logging +from datetime import datetime + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field + +from frigate.api.auth import require_role +from frigate.api.defs.tags import Tags + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.app]) + + +class DebugReplayStartBody(BaseModel): + """Request body for starting a debug replay session.""" + + camera: str = Field(title="Source camera name") + start_time: float = Field(title="Start timestamp") + end_time: float = Field(title="End timestamp") + + +class DebugReplayStartResponse(BaseModel): + """Response for starting a debug replay session.""" + + success: bool + replay_camera: str + + +class DebugReplayStatusResponse(BaseModel): + """Response for debug replay status.""" + + active: bool + replay_camera: str | None = None + source_camera: str | None = None + start_time: float | None = None + end_time: float | None = None + live_ready: bool = False + + +class DebugReplayStopResponse(BaseModel): + """Response for stopping a debug replay session.""" + + success: bool + + +@router.post( + "/debug_replay/start", + response_model=DebugReplayStartResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Start debug replay", + description="Start a debug replay session from camera recordings.", +) +async def start_debug_replay(request: Request, body: DebugReplayStartBody): + """Start a debug replay session.""" + replay_manager = request.app.replay_manager + + if replay_manager.active: + return JSONResponse( + content={ + "success": False, + "message": "A replay session is already active", + }, + status_code=409, + ) + + try: + replay_camera = await asyncio.to_thread( + replay_manager.start, + source_camera=body.camera, + start_ts=body.start_time, + end_ts=body.end_time, + frigate_config=request.app.frigate_config, + config_publisher=request.app.config_publisher, + ) + except ValueError: + logger.exception("Invalid parameters for debug replay start request") + return JSONResponse( + content={ + "success": False, + "message": "Invalid debug replay request parameters", + }, + status_code=400, + ) + except RuntimeError: + logger.exception("Error while starting debug replay session") + return JSONResponse( + content={ + "success": False, + "message": "An internal error occurred while starting debug replay", + }, + status_code=500, + ) + + return DebugReplayStartResponse( + success=True, + replay_camera=replay_camera, + ) + + +@router.get( + "/debug_replay/status", + response_model=DebugReplayStatusResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Get debug replay status", + description="Get the status of the current debug replay session.", +) +def get_debug_replay_status(request: Request): + """Get the current replay session status.""" + replay_manager = request.app.replay_manager + + live_ready = False + replay_camera = replay_manager.replay_camera_name + + if replay_manager.active and replay_camera: + frame_processor = request.app.detected_frames_processor + frame = frame_processor.get_current_frame(replay_camera) + + if frame is not None: + frame_time = frame_processor.get_current_frame_time(replay_camera) + camera_config = request.app.frigate_config.cameras.get(replay_camera) + retry_interval = 10 + + if camera_config is not None: + retry_interval = float(camera_config.ffmpeg.retry_interval or 10) + + live_ready = datetime.now().timestamp() <= frame_time + retry_interval + + return DebugReplayStatusResponse( + active=replay_manager.active, + replay_camera=replay_camera, + source_camera=replay_manager.source_camera, + start_time=replay_manager.start_ts, + end_time=replay_manager.end_ts, + live_ready=live_ready, + ) + + +@router.post( + "/debug_replay/stop", + response_model=DebugReplayStopResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Stop debug replay", + description="Stop the active debug replay session and clean up all artifacts.", +) +async def stop_debug_replay(request: Request): + """Stop the active replay session.""" + replay_manager = request.app.replay_manager + + if not replay_manager.active: + return JSONResponse( + content={"success": False, "message": "No active replay session"}, + status_code=400, + ) + + try: + await asyncio.to_thread( + replay_manager.stop, + frigate_config=request.app.frigate_config, + config_publisher=request.app.config_publisher, + ) + except (ValueError, RuntimeError, OSError) as e: + logger.error("Error stopping replay: %s", e) + return JSONResponse( + content={ + "success": False, + "message": "Failed to stop replay session due to an internal error.", + }, + status_code=500, + ) + + return DebugReplayStopResponse(success=True) diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py index 6059daf6e..3d2ab5961 100644 --- a/frigate/api/defs/request/app_body.py +++ b/frigate/api/defs/request/app_body.py @@ -7,6 +7,7 @@ class AppConfigSetBody(BaseModel): requires_restart: int = 1 update_topic: str | None = None config_data: Optional[Dict[str, Any]] = None + skip_save: bool = False class AppPutPasswordBody(BaseModel): diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 3206c7b4a..1e8c408e6 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -18,6 +18,7 @@ from frigate.api import ( camera, chat, classification, + debug_replay, event, export, media, @@ -32,6 +33,7 @@ from frigate.comms.event_metadata_updater import ( ) from frigate.config import FrigateConfig from frigate.config.camera.updater import CameraConfigUpdatePublisher +from frigate.debug_replay import DebugReplayManager from frigate.embeddings import EmbeddingsContext from frigate.genai import GenAIClientManager from frigate.ptz.onvif import OnvifController @@ -65,6 +67,7 @@ def create_fastapi_app( stats_emitter: StatsEmitter, event_metadata_updater: EventMetadataPublisher, config_publisher: CameraConfigUpdatePublisher, + replay_manager: DebugReplayManager, enforce_default_admin: bool = True, ): logger.info("Starting FastAPI app") @@ -133,6 +136,7 @@ def create_fastapi_app( app.include_router(event.router) app.include_router(media.router) app.include_router(record.router) + app.include_router(debug_replay.router) # App Properties app.frigate_config = frigate_config app.genai_manager = GenAIClientManager(frigate_config) @@ -144,6 +148,7 @@ def create_fastapi_app( app.stats_emitter = stats_emitter app.event_metadata_updater = event_metadata_updater app.config_publisher = config_publisher + app.replay_manager = replay_manager if frigate_config.auth.enabled: secret = get_jwt_secret() diff --git a/frigate/app.py b/frigate/app.py index fac7a08d9..7c8ac47e3 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -43,6 +43,10 @@ from frigate.const import ( ) from frigate.data_processing.types import DataProcessorMetrics from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.debug_replay import ( + DebugReplayManager, + cleanup_replay_cameras, +) from frigate.embeddings import EmbeddingProcess, EmbeddingsContext from frigate.events.audio import AudioProcessor from frigate.events.cleanup import EventCleanup @@ -139,6 +143,9 @@ class FrigateApp: else: logger.debug(f"Skipping directory: {d}") + def init_debug_replay_manager(self) -> None: + self.replay_manager = DebugReplayManager() + def init_camera_metrics(self) -> None: # create camera_metrics for camera_name in self.config.cameras.keys(): @@ -531,6 +538,7 @@ class FrigateApp: set_file_limit() # Start frigate services. + self.init_debug_replay_manager() self.init_camera_metrics() self.init_queues() self.init_database() @@ -541,6 +549,10 @@ class FrigateApp: self.init_embeddings_manager() self.bind_database() self.check_db_data_migrations() + + # Clean up any stale replay camera artifacts (filesystem + DB) + cleanup_replay_cameras() + self.init_inter_process_communicator() self.start_detectors() self.init_dispatcher() @@ -572,6 +584,7 @@ class FrigateApp: self.stats_emitter, self.event_metadata_updater, self.inter_config_updater, + self.replay_manager, ), host="127.0.0.1", port=5001, @@ -637,6 +650,7 @@ class FrigateApp: self.record_cleanup.join() self.stats_emitter.join() self.frigate_watchdog.join() + self.camera_maintainer.join() self.db.stop() # Save embeddings stats to disk diff --git a/frigate/camera/activity_manager.py b/frigate/camera/activity_manager.py index c2dfa891d..70c867073 100644 --- a/frigate/camera/activity_manager.py +++ b/frigate/camera/activity_manager.py @@ -57,6 +57,9 @@ class CameraActivityManager: all_objects: list[dict[str, Any]] = [] for camera in new_activity.keys(): + if camera not in self.config.cameras: + continue + # handle cameras that were added dynamically if camera not in self.camera_all_object_counts: self.__init_camera(self.config.cameras[camera]) @@ -124,7 +127,11 @@ class CameraActivityManager: any_changed = False # run through each object and check what topics need to be updated - for label in self.config.cameras[camera].objects.track: + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return + + for label in camera_config.objects.track: if label in self.config.model.non_logo_attributes: continue @@ -174,6 +181,9 @@ class AudioActivityManager: now = datetime.datetime.now().timestamp() for camera in new_activity.keys(): + if camera not in self.config.cameras: + continue + # handle cameras that were added dynamically if camera not in self.current_audio_detections: self.__init_camera(self.config.cameras[camera]) @@ -193,7 +203,11 @@ class AudioActivityManager: def compare_audio_activity( self, camera: str, new_detections: list[tuple[str, float]], now: float ) -> None: - max_not_heard = self.config.cameras[camera].audio.max_not_heard + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return False + + max_not_heard = camera_config.audio.max_not_heard current = self.current_audio_detections[camera] any_changed = False diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py index 815e650e9..9cfdcc7f3 100644 --- a/frigate/camera/maintainer.py +++ b/frigate/camera/maintainer.py @@ -55,8 +55,20 @@ class CameraMaintainer(threading.Thread): self.shm_count = self.__calculate_shm_frame_count() self.camera_processes: dict[str, mp.Process] = {} self.capture_processes: dict[str, mp.Process] = {} + self.camera_stop_events: dict[str, MpEvent] = {} self.metrics_manager = metrics_manager + def __ensure_camera_stop_event(self, camera: str) -> MpEvent: + camera_stop_event = self.camera_stop_events.get(camera) + + if camera_stop_event is None: + camera_stop_event = mp.Event() + self.camera_stop_events[camera] = camera_stop_event + else: + camera_stop_event.clear() + + return camera_stop_event + def __init_historical_regions(self) -> None: # delete region grids for removed or renamed cameras cameras = list(self.config.cameras.keys()) @@ -99,6 +111,8 @@ class CameraMaintainer(threading.Thread): logger.info(f"Camera processor not started for disabled camera {name}") return + camera_stop_event = self.__ensure_camera_stop_event(name) + if runtime: self.camera_metrics[name] = CameraMetrics(self.metrics_manager) self.ptz_metrics[name] = PTZMetrics(autotracker_enabled=False) @@ -135,7 +149,7 @@ class CameraMaintainer(threading.Thread): self.camera_metrics[name], self.ptz_metrics[name], self.region_grids[name], - self.stop_event, + camera_stop_event, self.config.logger, ) self.camera_processes[config.name] = camera_process @@ -150,6 +164,8 @@ class CameraMaintainer(threading.Thread): logger.info(f"Capture process not started for disabled camera {name}") return + camera_stop_event = self.__ensure_camera_stop_event(name) + # pre-create shms count = 10 if runtime else self.shm_count for i in range(count): @@ -160,7 +176,7 @@ class CameraMaintainer(threading.Thread): config, count, self.camera_metrics[name], - self.stop_event, + camera_stop_event, self.config.logger, ) capture_process.daemon = True @@ -170,18 +186,36 @@ class CameraMaintainer(threading.Thread): logger.info(f"Capture process started for {name}: {capture_process.pid}") def __stop_camera_capture_process(self, camera: str) -> None: - capture_process = self.capture_processes[camera] + capture_process = self.capture_processes.get(camera) if capture_process is not None: logger.info(f"Waiting for capture process for {camera} to stop") - capture_process.terminate() - capture_process.join() + camera_stop_event = self.camera_stop_events.get(camera) + + if camera_stop_event is not None: + camera_stop_event.set() + + capture_process.join(timeout=10) + if capture_process.is_alive(): + logger.warning( + f"Capture process for {camera} didn't exit, forcing termination" + ) + capture_process.terminate() + capture_process.join() def __stop_camera_process(self, camera: str) -> None: - camera_process = self.camera_processes[camera] + camera_process = self.camera_processes.get(camera) if camera_process is not None: logger.info(f"Waiting for process for {camera} to stop") - camera_process.terminate() - camera_process.join() + camera_stop_event = self.camera_stop_events.get(camera) + + if camera_stop_event is not None: + camera_stop_event.set() + + camera_process.join(timeout=10) + if camera_process.is_alive(): + logger.warning(f"Process for {camera} didn't exit, forcing termination") + camera_process.terminate() + camera_process.join() logger.info(f"Closing frame queue for {camera}") empty_and_close_queue(self.camera_metrics[camera].frame_queue) @@ -199,6 +233,12 @@ class CameraMaintainer(threading.Thread): for update_type, updated_cameras in updates.items(): if update_type == CameraConfigUpdateEnum.add.name: for camera in updated_cameras: + if ( + camera in self.camera_processes + or camera in self.capture_processes + ): + continue + self.__start_camera_processor( camera, self.update_subscriber.camera_configs[camera], @@ -210,15 +250,22 @@ class CameraMaintainer(threading.Thread): runtime=True, ) elif update_type == CameraConfigUpdateEnum.remove.name: - self.__stop_camera_capture_process(camera) - self.__stop_camera_process(camera) + for camera in updated_cameras: + self.__stop_camera_capture_process(camera) + self.__stop_camera_process(camera) + self.capture_processes.pop(camera, None) + self.camera_processes.pop(camera, None) + self.camera_stop_events.pop(camera, None) + self.region_grids.pop(camera, None) + self.camera_metrics.pop(camera, None) + self.ptz_metrics.pop(camera, None) # ensure the capture processes are done - for camera in self.camera_processes.keys(): + for camera in self.capture_processes.keys(): self.__stop_camera_capture_process(camera) # ensure the camera processors are done - for camera in self.capture_processes.keys(): + for camera in self.camera_processes.keys(): self.__stop_camera_process(camera) self.update_subscriber.stop() diff --git a/frigate/comms/config_updater.py b/frigate/comms/config_updater.py index 447089a94..4552abc11 100644 --- a/frigate/comms/config_updater.py +++ b/frigate/comms/config_updater.py @@ -26,8 +26,8 @@ class ConfigPublisher: def stop(self) -> None: self.stop_event.set() - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) class ConfigSubscriber: @@ -55,5 +55,5 @@ class ConfigSubscriber: return (None, None) def stop(self) -> None: - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 6da154814..490a829dc 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -110,6 +110,9 @@ class Dispatcher: payload: str, sub_command: str | None = None, ) -> None: + if camera_name not in self.config.cameras: + return + try: if command_type == "set": if sub_command: @@ -131,6 +134,9 @@ class Dispatcher: def handle_request_region_grid() -> Any: camera = payload + if camera not in self.config.cameras: + return None + grid = get_camera_regions_grid( camera, self.config.cameras[camera].detect, @@ -243,7 +249,11 @@ class Dispatcher: self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy())) def handle_on_connect() -> None: - camera_status = self.camera_activity.last_camera_activity.copy() + camera_status = { + camera: status + for camera, status in self.camera_activity.last_camera_activity.copy().items() + if camera in self.config.cameras + } audio_detections = self.audio_activity.current_audio_detections.copy() cameras_with_status = camera_status.keys() @@ -346,7 +356,8 @@ class Dispatcher: # example /cam_name/notifications/suspend payload=duration camera_name = parts[-3] command = parts[-2] - self._on_camera_notification_suspend(camera_name, payload) + if camera_name in self.config.cameras: + self._on_camera_notification_suspend(camera_name, payload) except IndexError: logger.error( f"Received invalid {topic.split('/')[-1]} command: {topic}" diff --git a/frigate/comms/inter_process.py b/frigate/comms/inter_process.py index e4aad9107..5e76da5eb 100644 --- a/frigate/comms/inter_process.py +++ b/frigate/comms/inter_process.py @@ -61,8 +61,8 @@ class InterProcessCommunicator(Communicator): def stop(self) -> None: self.stop_event.set() self.reader_thread.join() - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) class InterProcessRequestor: @@ -82,5 +82,5 @@ class InterProcessRequestor: return "" def stop(self) -> None: - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) diff --git a/frigate/comms/zmq_proxy.py b/frigate/comms/zmq_proxy.py index 29329ec59..4a4a0492a 100644 --- a/frigate/comms/zmq_proxy.py +++ b/frigate/comms/zmq_proxy.py @@ -43,7 +43,7 @@ class ZmqProxy: def stop(self) -> None: # destroying the context will tell the proxy to stop - self.context.destroy() + self.context.destroy(linger=0) self.runner.join() @@ -66,8 +66,8 @@ class Publisher(Generic[T]): self.socket.send_string(f"{self.topic}{sub_topic} {json.dumps(payload)}") def stop(self) -> None: - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) class Subscriber(Generic[T]): @@ -96,8 +96,8 @@ class Subscriber(Generic[T]): return self._return_object("", None) def stop(self) -> None: - self.socket.close() - self.context.destroy() + self.socket.close(linger=0) + self.context.destroy(linger=0) def _return_object(self, topic: str, payload: T | None) -> T | None: return payload diff --git a/frigate/config/camera/updater.py b/frigate/config/camera/updater.py index 125094f10..44aea527d 100644 --- a/frigate/config/camera/updater.py +++ b/frigate/config/camera/updater.py @@ -80,8 +80,8 @@ class CameraConfigUpdateSubscriber: self.camera_configs[camera] = updated_config return elif update_type == CameraConfigUpdateEnum.remove: - self.config.cameras.pop(camera) - self.camera_configs.pop(camera) + self.config.cameras.pop(camera, None) + self.camera_configs.pop(camera, None) return config = self.camera_configs.get(camera) diff --git a/frigate/const.py b/frigate/const.py index 87fdb8e70..6b1e227d5 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -14,6 +14,8 @@ RECORD_DIR = f"{BASE_DIR}/recordings" TRIGGER_DIR = f"{CLIPS_DIR}/triggers" BIRDSEYE_PIPE = "/tmp/cache/birdseye" CACHE_DIR = "/tmp/cache" +REPLAY_CAMERA_PREFIX = "_replay_" +REPLAY_DIR = os.path.join(CACHE_DIR, "replay") PLUS_ENV_VAR = "PLUS_API_KEY" PLUS_API_HOST = "https://api.frigate.video" diff --git a/frigate/debug_replay.py b/frigate/debug_replay.py new file mode 100644 index 000000000..504184667 --- /dev/null +++ b/frigate/debug_replay.py @@ -0,0 +1,443 @@ +"""Debug replay camera management for replaying recordings with detection overlays.""" + +import logging +import os +import shutil +import subprocess as sp +import threading + +from ruamel.yaml import YAML + +from frigate.config import FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdatePublisher, + CameraConfigUpdateTopic, +) +from frigate.const import ( + CLIPS_DIR, + RECORD_DIR, + REPLAY_CAMERA_PREFIX, + REPLAY_DIR, + THUMB_DIR, +) +from frigate.models import Event, Recordings, ReviewSegment, Timeline +from frigate.util.config import find_config_file + +logger = logging.getLogger(__name__) + + +class DebugReplayManager: + """Manages a single debug replay session.""" + + def __init__(self) -> None: + self._lock = threading.Lock() + self.replay_camera_name: str | None = None + self.source_camera: str | None = None + self.clip_path: str | None = None + self.start_ts: float | None = None + self.end_ts: float | None = None + + @property + def active(self) -> bool: + """Whether a replay session is currently active.""" + return self.replay_camera_name is not None + + def start( + self, + source_camera: str, + start_ts: float, + end_ts: float, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + ) -> str: + """Start a debug replay session. + + Args: + source_camera: Name of the source camera to replay + start_ts: Start timestamp + end_ts: End timestamp + frigate_config: Current Frigate configuration + config_publisher: Publisher for camera config updates + + Returns: + The replay camera name + + Raises: + ValueError: If a session is already active or parameters are invalid + RuntimeError: If clip generation fails + """ + with self._lock: + return self._start_locked( + source_camera, start_ts, end_ts, frigate_config, config_publisher + ) + + def _start_locked( + self, + source_camera: str, + start_ts: float, + end_ts: float, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + ) -> str: + if self.active: + raise ValueError("A replay session is already active") + + if source_camera not in frigate_config.cameras: + raise ValueError(f"Camera '{source_camera}' not found") + + if end_ts <= start_ts: + raise ValueError("End time must be after start time") + + # Query recordings for the source camera in the time range + recordings = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + Recordings.end_time, + ) + .where( + Recordings.start_time.between(start_ts, end_ts) + | Recordings.end_time.between(start_ts, end_ts) + | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) + ) + .where(Recordings.camera == source_camera) + .order_by(Recordings.start_time.asc()) + ) + + if not recordings.count(): + raise ValueError( + f"No recordings found for camera '{source_camera}' in the specified time range" + ) + + # Create replay directory + os.makedirs(REPLAY_DIR, exist_ok=True) + + # Generate replay camera name + replay_name = f"{REPLAY_CAMERA_PREFIX}{source_camera}" + + # Build concat file for ffmpeg + concat_file = os.path.join(REPLAY_DIR, f"{replay_name}_concat.txt") + clip_path = os.path.join(REPLAY_DIR, f"{replay_name}.mp4") + + with open(concat_file, "w") as f: + for recording in recordings: + f.write(f"file '{recording.path}'\n") + + # Concatenate recordings into a single clip with -c copy (fast) + ffmpeg_cmd = [ + frigate_config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-y", + "-f", + "concat", + "-safe", + "0", + "-i", + concat_file, + "-c", + "copy", + "-movflags", + "+faststart", + clip_path, + ] + + logger.info( + "Generating replay clip for %s (%.1f - %.1f)", + source_camera, + start_ts, + end_ts, + ) + + try: + result = sp.run( + ffmpeg_cmd, + capture_output=True, + text=True, + timeout=120, + ) + if result.returncode != 0: + logger.error("FFmpeg error: %s", result.stderr) + raise RuntimeError( + f"Failed to generate replay clip: {result.stderr[-500:]}" + ) + except sp.TimeoutExpired: + raise RuntimeError("Clip generation timed out") + finally: + # Clean up concat file + if os.path.exists(concat_file): + os.remove(concat_file) + + if not os.path.exists(clip_path): + raise RuntimeError("Clip file was not created") + + # Build camera config dict for the replay camera + source_config = frigate_config.cameras[source_camera] + camera_dict = self._build_camera_config_dict( + source_config, replay_name, clip_path + ) + + # Build an in-memory config with the replay camera added + config_file = find_config_file() + yaml_parser = YAML() + with open(config_file, "r") as f: + config_data = yaml_parser.load(f) + + if "cameras" not in config_data or config_data["cameras"] is None: + config_data["cameras"] = {} + config_data["cameras"][replay_name] = camera_dict + + try: + new_config = FrigateConfig.parse_object(config_data) + except Exception as e: + raise RuntimeError(f"Failed to validate replay camera config: {e}") + + # Update the running config + frigate_config.cameras[replay_name] = new_config.cameras[replay_name] + + # Publish the add event + config_publisher.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.add, replay_name), + new_config.cameras[replay_name], + ) + + # Store session state + self.replay_camera_name = replay_name + self.source_camera = source_camera + self.clip_path = clip_path + self.start_ts = start_ts + self.end_ts = end_ts + + logger.info("Debug replay started: %s -> %s", source_camera, replay_name) + return replay_name + + def stop( + self, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + ) -> None: + """Stop the active replay session and clean up all artifacts. + + Args: + frigate_config: Current Frigate configuration + config_publisher: Publisher for camera config updates + """ + with self._lock: + self._stop_locked(frigate_config, config_publisher) + + def _stop_locked( + self, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, + ) -> None: + if not self.active: + logger.warning("No active replay session to stop") + return + + replay_name = self.replay_camera_name + + # Publish remove event so subscribers stop and remove from their config + if replay_name in frigate_config.cameras: + config_publisher.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, replay_name), + frigate_config.cameras[replay_name], + ) + # Do NOT pop here — let subscribers handle removal from the shared + # config dict when they process the ZMQ message to avoid race conditions + + # Defensive DB cleanup + self._cleanup_db(replay_name) + + # Remove filesystem artifacts + self._cleanup_files(replay_name) + + # Reset state + self.replay_camera_name = None + self.source_camera = None + self.clip_path = None + self.start_ts = None + self.end_ts = None + + logger.info("Debug replay stopped and cleaned up: %s", replay_name) + + def _build_camera_config_dict( + self, + source_config, + replay_name: str, + clip_path: str, + ) -> dict: + """Build a camera config dictionary for the replay camera. + + Args: + source_config: Source camera's CameraConfig + replay_name: Name for the replay camera + clip_path: Path to the replay clip file + + Returns: + Camera config as a dictionary + """ + # Extract detect config (exclude computed fields) + detect_dict = source_config.detect.model_dump( + exclude={"min_initialized", "max_disappeared", "enabled_in_config"} + ) + + # Extract objects config, using .dict() on filters to convert + # RuntimeFilterConfig ndarray masks back to string coordinates + objects_dict = { + "track": source_config.objects.track, + "mask": { + mask_id: ( + mask_cfg.model_dump( + exclude={"raw_coordinates", "enabled_in_config"} + ) + if mask_cfg is not None + else None + ) + for mask_id, mask_cfg in source_config.objects.mask.items() + } + if source_config.objects.mask + else {}, + "filters": { + name: filt.dict() if hasattr(filt, "dict") else filt.model_dump() + for name, filt in source_config.objects.filters.items() + }, + } + + # Extract zones (exclude_defaults avoids serializing empty defaults + # like distances=[] that fail validation on re-parse) + zones_dict = {} + for zone_name, zone_config in source_config.zones.items(): + zone_dump = zone_config.model_dump( + exclude={"contour", "color"}, exclude_defaults=True + ) + # Always include required fields + zone_dump.setdefault("coordinates", zone_config.coordinates) + zones_dict[zone_name] = zone_dump + + # Extract motion config (exclude runtime fields) + motion_dict = {} + if source_config.motion is not None: + motion_dict = source_config.motion.model_dump( + exclude={ + "frame_shape", + "raw_mask", + "mask", + "improved_contrast_enabled", + "rasterized_mask", + } + ) + + return { + "enabled": True, + "ffmpeg": { + "inputs": [ + { + "path": clip_path, + "roles": ["detect"], + "input_args": "-re -stream_loop -1 -fflags +genpts", + } + ], + "hwaccel_args": [], + }, + "detect": detect_dict, + "objects": objects_dict, + "zones": zones_dict, + "motion": motion_dict, + "record": {"enabled": False}, + "snapshots": {"enabled": False}, + "review": { + "alerts": {"enabled": False}, + "detections": {"enabled": False}, + }, + "birdseye": {"enabled": False}, + "audio": {"enabled": False}, + "lpr": {"enabled": False}, + "face_recognition": {"enabled": False}, + } + + def _cleanup_db(self, camera_name: str) -> None: + """Defensively remove any database rows for the replay camera.""" + try: + Event.delete().where(Event.camera == camera_name).execute() + except Exception as e: + logger.error("Failed to delete replay events: %s", e) + + try: + Timeline.delete().where(Timeline.camera == camera_name).execute() + except Exception as e: + logger.error("Failed to delete replay timeline: %s", e) + + try: + Recordings.delete().where(Recordings.camera == camera_name).execute() + except Exception as e: + logger.error("Failed to delete replay recordings: %s", e) + + try: + ReviewSegment.delete().where(ReviewSegment.camera == camera_name).execute() + except Exception as e: + logger.error("Failed to delete replay review segments: %s", e) + + def _cleanup_files(self, camera_name: str) -> None: + """Remove filesystem artifacts for the replay camera.""" + dirs_to_clean = [ + os.path.join(RECORD_DIR, camera_name), + os.path.join(CLIPS_DIR, camera_name), + os.path.join(THUMB_DIR, camera_name), + ] + + for dir_path in dirs_to_clean: + if os.path.exists(dir_path): + try: + shutil.rmtree(dir_path) + logger.debug("Removed replay directory: %s", dir_path) + except Exception as e: + logger.error("Failed to remove %s: %s", dir_path, e) + + # Remove replay clip and any related files + if os.path.exists(REPLAY_DIR): + try: + shutil.rmtree(REPLAY_DIR) + logger.debug("Removed replay cache directory") + except Exception as e: + logger.error("Failed to remove replay cache: %s", e) + + +def cleanup_replay_cameras() -> None: + """Remove any stale replay camera artifacts on startup. + + Since replay cameras are memory-only and never written to YAML, they + won't appear in the config after a restart. This function cleans up + filesystem and database artifacts from any replay that was running when + the process stopped. + + Must be called AFTER the database is bound. + """ + stale_cameras: set[str] = set() + + # Scan filesystem for leftover replay artifacts to derive camera names + for dir_path in [RECORD_DIR, CLIPS_DIR, THUMB_DIR]: + if os.path.isdir(dir_path): + for entry in os.listdir(dir_path): + if entry.startswith(REPLAY_CAMERA_PREFIX): + stale_cameras.add(entry) + + if os.path.isdir(REPLAY_DIR): + for entry in os.listdir(REPLAY_DIR): + if entry.startswith(REPLAY_CAMERA_PREFIX) and entry.endswith(".mp4"): + stale_cameras.add(entry.removesuffix(".mp4")) + + if not stale_cameras: + return + + logger.info("Cleaning up stale replay camera artifacts: %s", list(stale_cameras)) + + manager = DebugReplayManager() + for camera_name in stale_cameras: + manager._cleanup_db(camera_name) + manager._cleanup_files(camera_name) + + if os.path.exists(REPLAY_DIR): + try: + shutil.rmtree(REPLAY_DIR) + except Exception as e: + logger.error("Failed to remove replay cache directory: %s", e) diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 54831942a..8e45af498 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -421,7 +421,9 @@ class EmbeddingMaintainer(threading.Thread): if self.config.semantic_search.enabled: self.embeddings.update_stats() - camera_config = self.config.cameras[camera] + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return # no need to process updated objects if no processors are active if len(self.realtime_processors) == 0 and len(self.post_processors) == 0: @@ -639,7 +641,10 @@ class EmbeddingMaintainer(threading.Thread): if not camera or camera not in self.config.cameras: return - camera_config = self.config.cameras[camera] + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return + dedicated_lpr_enabled = ( camera_config.type == CameraTypeEnum.lpr and "license_plate" not in camera_config.objects.track diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index f6ab777c1..77f6eee5f 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -7,6 +7,7 @@ from typing import Dict from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber from frigate.config import FrigateConfig from frigate.config.classification import ObjectClassificationType +from frigate.const import REPLAY_CAMERA_PREFIX from frigate.events.types import EventStateEnum, EventTypeEnum from frigate.models import Event from frigate.util.builtin import to_relative_box @@ -146,7 +147,9 @@ class EventProcessor(threading.Thread): if should_update_db(self.events_in_process[event_data["id"]], event_data): updated_db = True - camera_config = self.config.cameras[camera] + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return width = camera_config.detect.width height = camera_config.detect.height first_detector = list(self.config.detectors.values())[0] @@ -283,6 +286,10 @@ class EventProcessor(threading.Thread): def handle_external_detection( self, event_type: EventStateEnum, event_data: Event ) -> None: + # Skip replay cameras + if event_data.get("camera", "").startswith(REPLAY_CAMERA_PREFIX): + return + if event_type == EventStateEnum.start: event = { Event.id: event_data["id"], diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index eb23c2573..d3717d281 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -420,7 +420,8 @@ class BirdsEyeFrameManager: [ cam for cam, cam_data in self.cameras.items() - if self.config.cameras[cam].birdseye.enabled + if cam in self.config.cameras + and self.config.cameras[cam].birdseye.enabled and self.config.cameras[cam].enabled_in_config and self.config.cameras[cam].enabled and cam_data["last_active_frame"] > 0 @@ -723,8 +724,11 @@ class BirdsEyeFrameManager: Update birdseye for a specific camera with new frame data. Returns (frame_changed, layout_changed) to indicate if the frame or layout changed. """ - # don't process if birdseye is disabled for this camera - camera_config = self.config.cameras[camera] + # don't process if camera was removed or birdseye is disabled + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return False, False + force_update = False # disabling birdseye is a little tricky diff --git a/frigate/output/output.py b/frigate/output/output.py index a44415000..38b1ddc52 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -22,7 +22,12 @@ from frigate.config.camera.updater import ( CameraConfigUpdateEnum, CameraConfigUpdateSubscriber, ) -from frigate.const import CACHE_DIR, CLIPS_DIR, PROCESS_PRIORITY_MED +from frigate.const import ( + CACHE_DIR, + CLIPS_DIR, + PROCESS_PRIORITY_MED, + REPLAY_CAMERA_PREFIX, +) from frigate.output.birdseye import Birdseye from frigate.output.camera import JsmpegCamera from frigate.output.preview import PreviewRecorder @@ -79,6 +84,32 @@ class OutputProcess(FrigateProcess): ) self.config = config + def is_debug_replay_camera(self, camera: str) -> bool: + return camera.startswith(REPLAY_CAMERA_PREFIX) + + def add_camera( + self, + camera: str, + websocket_server: WSGIServer, + jsmpeg_cameras: dict[str, JsmpegCamera], + preview_recorders: dict[str, PreviewRecorder], + preview_write_times: dict[str, float], + birdseye: Birdseye | None, + ) -> None: + camera_config = self.config.cameras[camera] + jsmpeg_cameras[camera] = JsmpegCamera( + camera_config, self.stop_event, websocket_server + ) + preview_recorders[camera] = PreviewRecorder(camera_config) + preview_write_times[camera] = 0 + + if ( + birdseye is not None + and self.config.birdseye.enabled + and camera_config.birdseye.enabled + ): + birdseye.add_camera(camera) + def run(self) -> None: self.pre_run_setup(self.config.logger) @@ -118,14 +149,17 @@ class OutputProcess(FrigateProcess): move_preview_frames("cache") for camera, cam_config in self.config.cameras.items(): - if not cam_config.enabled_in_config: + if not cam_config.enabled_in_config or self.is_debug_replay_camera(camera): continue - jsmpeg_cameras[camera] = JsmpegCamera( - cam_config, self.stop_event, websocket_server + self.add_camera( + camera, + websocket_server, + jsmpeg_cameras, + preview_recorders, + preview_write_times, + birdseye, ) - preview_recorders[camera] = PreviewRecorder(cam_config) - preview_write_times[camera] = 0 if self.config.birdseye.enabled: birdseye = Birdseye(self.config, self.stop_event, websocket_server) @@ -138,19 +172,15 @@ class OutputProcess(FrigateProcess): if CameraConfigUpdateEnum.add in updates: for camera in updates["add"]: - jsmpeg_cameras[camera] = JsmpegCamera( - self.config.cameras[camera], self.stop_event, websocket_server - ) - preview_recorders[camera] = PreviewRecorder( - self.config.cameras[camera] - ) - preview_write_times[camera] = 0 - - if ( - self.config.birdseye.enabled - and self.config.cameras[camera].birdseye.enabled - ): - birdseye.add_camera(camera) + if not self.is_debug_replay_camera(camera): + self.add_camera( + camera, + websocket_server, + jsmpeg_cameras, + preview_recorders, + preview_write_times, + birdseye, + ) (topic, data) = detection_subscriber.check_for_update(timeout=1) now = datetime.datetime.now().timestamp() @@ -174,7 +204,11 @@ class OutputProcess(FrigateProcess): _, ) = data - if not self.config.cameras[camera].enabled: + if ( + camera not in self.config.cameras + or not self.config.cameras[camera].enabled + or self.is_debug_replay_camera(camera) + ): continue frame = frame_manager.get( diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index a90d1edc1..7b54d6bd1 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -287,11 +287,12 @@ class RecordingMaintainer(threading.Thread): ) # publish most recently available recording time and None if disabled + camera_cfg = self.config.cameras.get(camera) self.recordings_publisher.publish( ( camera, recordings[0]["start_time"].timestamp() - if self.config.cameras[camera].record.enabled + if camera_cfg and camera_cfg.record.enabled else None, None, ), @@ -315,9 +316,8 @@ class RecordingMaintainer(threading.Thread): ) -> Optional[Recordings]: cache_path: str = recording["cache_path"] start_time: datetime.datetime = recording["start_time"] - record_config = self.config.cameras[camera].record - # Just delete files if recordings are turned off + # Just delete files if camera removed or recordings are turned off if ( camera not in self.config.cameras or not self.config.cameras[camera].record.enabled diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index 6afdc8de9..a51c73f88 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -652,6 +652,9 @@ class ReviewSegmentMaintainer(threading.Thread): if camera not in self.indefinite_events: self.indefinite_events[camera] = {} + if camera not in self.config.cameras: + continue + if ( not self.config.cameras[camera].enabled or not self.config.cameras[camera].record.enabled diff --git a/frigate/stats/util.py b/frigate/stats/util.py index f4f91f83f..40337268e 100644 --- a/frigate/stats/util.py +++ b/frigate/stats/util.py @@ -340,6 +340,9 @@ def stats_snapshot( stats["cameras"] = {} for name, camera_stats in camera_metrics.items(): + if name not in config.cameras: + continue + total_camera_fps += camera_stats.camera_fps.value total_process_fps += camera_stats.process_fps.value total_skipped_fps += camera_stats.skipped_fps.value diff --git a/frigate/storage.py b/frigate/storage.py index feabe06ff..93463c542 100644 --- a/frigate/storage.py +++ b/frigate/storage.py @@ -8,7 +8,7 @@ from pathlib import Path from peewee import SQL, fn from frigate.config import FrigateConfig -from frigate.const import RECORD_DIR +from frigate.const import RECORD_DIR, REPLAY_CAMERA_PREFIX from frigate.models import Event, Recordings from frigate.util.builtin import clear_and_unlink @@ -32,6 +32,10 @@ class StorageMaintainer(threading.Thread): def calculate_camera_bandwidth(self) -> None: """Calculate an average MB/hr for each camera.""" for camera in self.config.cameras.keys(): + # Skip replay cameras + if camera.startswith(REPLAY_CAMERA_PREFIX): + continue + # cameras with < 50 segments should be refreshed to keep size accurate # when few segments are available if self.camera_storage_stats.get(camera, {}).get("needs_refresh", True): @@ -77,6 +81,10 @@ class StorageMaintainer(threading.Thread): usages: dict[str, dict] = {} for camera in self.config.cameras.keys(): + # Skip replay cameras + if camera.startswith(REPLAY_CAMERA_PREFIX): + continue + camera_storage = ( Recordings.select(fn.SUM(Recordings.segment_size)) .where(Recordings.camera == camera, Recordings.segment_size != 0) diff --git a/frigate/test/http_api/base_http_test.py b/frigate/test/http_api/base_http_test.py index 16ded63f8..2ca4aafd0 100644 --- a/frigate/test/http_api/base_http_test.py +++ b/frigate/test/http_api/base_http_test.py @@ -13,6 +13,7 @@ from pydantic import Json from frigate.api.fastapi_app import create_fastapi_app from frigate.config import FrigateConfig from frigate.const import BASE_DIR, CACHE_DIR +from frigate.debug_replay import DebugReplayManager from frigate.models import Event, Recordings, ReviewSegment from frigate.review.types import SeverityEnum from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS @@ -141,6 +142,7 @@ class BaseTestHttp(unittest.TestCase): stats, event_metadata_publisher, None, + DebugReplayManager(), enforce_default_admin=False, ) diff --git a/frigate/test/http_api/test_http_app.py b/frigate/test/http_api/test_http_app.py index b04b1cf55..bf8e9c72a 100644 --- a/frigate/test/http_api/test_http_app.py +++ b/frigate/test/http_api/test_http_app.py @@ -22,3 +22,32 @@ class TestHttpApp(BaseTestHttp): response = client.get("/stats") response_json = response.json() assert response_json == self.test_stats + + def test_config_set_in_memory_replaces_objects_track_list(self): + self.minimal_config["cameras"]["front_door"]["objects"] = { + "track": ["person", "car"], + } + app = super().create_app() + app.config_publisher = Mock() + + with AuthTestClient(app) as client: + response = client.put( + "/config/set", + json={ + "requires_restart": 0, + "skip_save": True, + "update_topic": "config/cameras/front_door/objects", + "config_data": { + "cameras": { + "front_door": { + "objects": { + "track": ["person"], + } + } + } + }, + }, + ) + + assert response.status_code == 200 + assert app.frigate_config.cameras["front_door"].objects.track == ["person"] diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index 98799fcf0..e903c2ac3 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -151,6 +151,22 @@ class TestConfig(unittest.TestCase): frigate_config = FrigateConfig(**config) assert "dog" in frigate_config.cameras["back"].objects.track + def test_deep_merge_override_replaces_list_values(self): + base = {"objects": {"track": ["person", "face"]}} + update = {"objects": {"track": ["person"]}} + + merged = deep_merge(base, update, override=True) + + assert merged["objects"]["track"] == ["person"] + + def test_deep_merge_merge_lists_still_appends(self): + base = {"track": ["person"]} + update = {"track": ["face"]} + + merged = deep_merge(base, update, override=True, merge_lists=True) + + assert merged["track"] == ["person", "face"] + def test_override_birdseye(self): config = { "mqtt": {"host": "mqtt"}, diff --git a/frigate/timeline.py b/frigate/timeline.py index cf2f5e8c7..3ec866176 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -86,7 +86,9 @@ class TimelineProcessor(threading.Thread): event_data: dict[Any, Any], ) -> bool: """Handle object detection.""" - camera_config = self.config.cameras[camera] + camera_config = self.config.cameras.get(camera) + if camera_config is None: + return False event_id = event_data["id"] # Base timeline entry data that all entries will share diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index 9ac04b42a..a699fab23 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -690,9 +690,13 @@ class TrackedObjectProcessor(threading.Thread): self.create_camera_state(camera) elif "remove" in updated_topics: for camera in updated_topics["remove"]: - camera_state = self.camera_states[camera] - camera_state.shutdown() + removed_camera_state = self.camera_states[camera] + removed_camera_state.shutdown() self.camera_states.pop(camera) + self.camera_activity.pop(camera, None) + self.last_motion_detected.pop(camera, None) + + self.requestor.send_data(UPDATE_CAMERA_ACTIVITY, self.camera_activity) # manage camera disabled state for camera, config in self.config.cameras.items(): @@ -700,6 +704,10 @@ class TrackedObjectProcessor(threading.Thread): continue current_enabled = config.enabled + camera_state = self.camera_states.get(camera) + if camera_state is None: + continue + camera_state = self.camera_states[camera] if camera_state.prev_enabled and not current_enabled: @@ -752,7 +760,11 @@ class TrackedObjectProcessor(threading.Thread): except queue.Empty: continue - if not self.config.cameras[camera].enabled: + camera_config = self.config.cameras.get(camera) + if camera_config is None: + continue + + if not camera_config.enabled: logger.debug(f"Camera {camera} disabled, skipping update") continue diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py index c4398dec6..4eb600fb8 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -16,7 +16,7 @@ from frigate.config import ( SnapshotsConfig, UIConfig, ) -from frigate.const import CLIPS_DIR, THUMB_DIR +from frigate.const import CLIPS_DIR, REPLAY_CAMERA_PREFIX, THUMB_DIR from frigate.detectors.detector_config import ModelConfig from frigate.review.types import SeverityEnum from frigate.util.builtin import sanitize_float @@ -621,6 +621,9 @@ class TrackedObject: if not self.camera_config.name: return + if self.camera_config.name.startswith(REPLAY_CAMERA_PREFIX): + return + directory = os.path.join(THUMB_DIR, self.camera_config.name) if not os.path.exists(directory): diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py index bcdc2feda..aa2417a5c 100644 --- a/frigate/util/builtin.py +++ b/frigate/util/builtin.py @@ -84,7 +84,8 @@ def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dic """ :param dct1: First dict to merge :param dct2: Second dict to merge - :param override: if same key exists in both dictionaries, should override? otherwise ignore. (default=True) + :param override: if same key exists in both dictionaries, should override? otherwise ignore. + :param merge_lists: if True, lists will be merged. :return: The merge dictionary """ merged = copy.deepcopy(dct1) @@ -96,6 +97,8 @@ def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dic elif isinstance(v1, list) and isinstance(v2, list): if merge_lists: merged[k] = v1 + v2 + elif override: + merged[k] = copy.deepcopy(v2) else: if override: merged[k] = copy.deepcopy(v2) diff --git a/frigate/util/config.py b/frigate/util/config.py index c689d16e4..238671563 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -9,6 +9,7 @@ from typing import Any, Optional, Union from ruamel.yaml import YAML from frigate.const import CONFIG_DIR, EXPORT_DIR +from frigate.util.builtin import deep_merge from frigate.util.services import get_video_properties logger = logging.getLogger(__name__) @@ -688,3 +689,78 @@ class StreamInfoRetriever: info = asyncio.run(get_video_properties(ffmpeg, path)) self.stream_cache[path] = info return info + + +def apply_section_update(camera_config, section: str, update: dict) -> Optional[str]: + """Merge an update dict into a camera config section and rebuild runtime variants. + + For motion and object filter sections, the plain Pydantic models are rebuilt + as RuntimeMotionConfig / RuntimeFilterConfig so that rasterized numpy masks + are recomputed. This mirrors the logic in FrigateConfig.post_validation. + + Args: + camera_config: The CameraConfig instance to update. + section: Config section name (e.g. "motion", "objects"). + update: Nested dict of field updates to merge. + + Returns: + None on success, or an error message string on failure. + """ + from frigate.config.config import RuntimeFilterConfig, RuntimeMotionConfig + + current = getattr(camera_config, section, None) + if current is None: + return f"Section '{section}' not found on camera '{camera_config.name}'" + + try: + frame_shape = camera_config.frame_shape + + if section == "motion": + merged = deep_merge( + current.model_dump(exclude_unset=True, exclude={"rasterized_mask"}), + update, + override=True, + ) + camera_config.motion = RuntimeMotionConfig( + frame_shape=frame_shape, **merged + ) + + elif section == "objects": + merged = deep_merge( + current.model_dump( + exclude={"filters": {"__all__": {"rasterized_mask"}}} + ), + update, + override=True, + ) + new_objects = current.__class__.model_validate(merged) + + # Preserve private _all_objects from original config + try: + new_objects._all_objects = current._all_objects + except AttributeError: + pass + + # Rebuild RuntimeFilterConfig with merged global + per-object masks + for obj_name, filt in new_objects.filters.items(): + merged_mask = dict(filt.mask) + if new_objects.mask: + for gid, gmask in new_objects.mask.items(): + merged_mask[f"global_{gid}"] = gmask + + new_objects.filters[obj_name] = RuntimeFilterConfig( + frame_shape=frame_shape, + mask=merged_mask, + **filt.model_dump(exclude_unset=True, exclude={"mask", "raw_mask"}), + ) + camera_config.objects = new_objects + + else: + merged = deep_merge(current.model_dump(), update, override=True) + setattr(camera_config, section, current.__class__.model_validate(merged)) + + except Exception: + logger.exception("Config validation error") + return "Validation error. Check logs for details." + + return None diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index 1181554af..37566117a 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -117,6 +117,7 @@ "button": { "add": "Add", "apply": "Apply", + "applying": "Applying…", "reset": "Reset", "undo": "Undo", "done": "Done", @@ -252,6 +253,7 @@ "review": "Review", "explore": "Explore", "export": "Export", + "actions": "Actions", "uiPlayground": "UI Playground", "faceLibrary": "Face Library", "classification": "Classification", diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index 53b04e6c4..661a9a5e9 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -216,6 +216,10 @@ }, "hideObjectDetails": { "label": "Hide object path" + }, + "debugReplay": { + "label": "Debug replay", + "aria": "View this tracked object in the debug replay view" } }, "dialog": { diff --git a/web/public/locales/en/views/replay.json b/web/public/locales/en/views/replay.json new file mode 100644 index 000000000..a966626f5 --- /dev/null +++ b/web/public/locales/en/views/replay.json @@ -0,0 +1,54 @@ +{ + "title": "Debug Replay", + "description": "Replay camera recordings for debugging. The object list shows a time-delayed summary of detected objects and the Messages tab shows a stream of Frigate's internal messages from the replay footage.", + "websocket_messages": "Messages", + "dialog": { + "title": "Start Debug Replay", + "description": "Create a temporary replay camera that loops historical footage for debugging object detection and tracking issues. The replay camera will have the same detection configuration as the source camera. Choose a time range to begin.", + "camera": "Source Camera", + "timeRange": "Time Range", + "preset": { + "1m": "Last 1 Minute", + "5m": "Last 5 Minutes", + "timeline": "From Timeline", + "custom": "Custom" + }, + "startButton": "Start Replay", + "selectFromTimeline": "Select", + "starting": "Starting replay...", + "startLabel": "Start", + "endLabel": "End", + "toast": { + "success": "Debug replay started successfully", + "error": "Failed to start debug replay: {{error}}", + "alreadyActive": "A replay session is already active", + "stopped": "Debug replay stopped", + "stopError": "Failed to stop debug replay: {{error}}", + "goToReplay": "Go to Replay" + } + }, + "page": { + "noSession": "No Active Replay Session", + "noSessionDesc": "Start a debug replay from the History view by clicking the Debug Replay button in the toolbar.", + "goToRecordings": "Go to History", + "sourceCamera": "Source Camera", + "replayCamera": "Replay Camera", + "initializingReplay": "Initializing replay...", + "stoppingReplay": "Stopping replay...", + "stopReplay": "Stop Replay", + "confirmStop": { + "title": "Stop Debug Replay?", + "description": "This will stop the replay session and clean up all temporary data. Are you sure?", + "confirm": "Stop Replay", + "cancel": "Cancel" + }, + "activity": "Activity", + "objects": "Object List", + "audioDetections": "Audio Detections", + "noActivity": "No activity detected", + "activeTracking": "Active tracking", + "noActiveTracking": "No active tracking", + "configuration": "Configuration", + "configurationDesc": "Fine tune motion detection and object tracking settings for the debug replay camera. No changes are saved to your Frigate configuration file." + } +} diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index ca772377b..0b61b278b 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1392,6 +1392,7 @@ }, "toast": { "success": "Settings saved successfully", + "applied": "Settings applied successfully", "successRestartRequired": "Settings saved successfully. Restart Frigate to apply your changes.", "error": "Failed to save settings", "validationError": "Validation failed: {{message}}", diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json index 8ddbc03e1..faaff31c9 100644 --- a/web/public/locales/en/views/system.json +++ b/web/public/locales/en/views/system.json @@ -7,12 +7,39 @@ "logs": { "frigate": "Frigate Logs - Frigate", "go2rtc": "Go2RTC Logs - Frigate", - "nginx": "Nginx Logs - Frigate" + "nginx": "Nginx Logs - Frigate", + "websocket": "Messages Logs - Frigate" } }, "title": "System", "metrics": "System metrics", "logs": { + "websocket": { + "label": "Messages", + "pause": "Pause", + "resume": "Resume", + "clear": "Clear", + "filter": { + "all": "All topics", + "topics": "Topics", + "events": "Events", + "reviews": "Reviews", + "classification": "Classification", + "face_recognition": "Face Recognition", + "lpr": "LPR", + "camera_activity": "Camera activity", + "system": "System", + "camera": "Camera", + "all_cameras": "All cameras", + "cameras_count_one": "{{count}} Camera", + "cameras_count_other": "{{count}} Cameras" + }, + "empty": "No messages captured yet", + "count": "{{count}} messages", + "expanded": { + "payload": "Payload" + } + }, "download": { "label": "Download Logs" }, @@ -189,7 +216,8 @@ "cameraIsOffline": "{{camera}} is offline", "detectIsSlow": "{{detect}} is slow ({{speed}} ms)", "detectIsVerySlow": "{{detect}} is very slow ({{speed}} ms)", - "shmTooLow": "/dev/shm allocation ({{total}} MB) should be increased to at least {{min}} MB." + "shmTooLow": "/dev/shm allocation ({{total}} MB) should be increased to at least {{min}} MB.", + "debugReplayActive": "Debug replay session is active" }, "enrichments": { "title": "Enrichments", diff --git a/web/src/App.tsx b/web/src/App.tsx index 82ca2b1e0..21babc2b9 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -30,6 +30,7 @@ const Classification = lazy(() => import("@/pages/ClassificationModel")); const Chat = lazy(() => import("@/pages/Chat")); const Logs = lazy(() => import("@/pages/Logs")); const AccessDenied = lazy(() => import("@/pages/AccessDenied")); +const Replay = lazy(() => import("@/pages/Replay")); function App() { const { data: config } = useSWR("config", { @@ -108,7 +109,8 @@ function DefaultAppView() { } /> } /> } /> - } /> + } />{" "} + } />{" "} } /> } /> diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index f5b7b57a3..07d44d67a 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -1,5 +1,5 @@ import { baseUrl } from "./baseUrl"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import useWebSocket, { ReadyState } from "react-use-websocket"; import { EmbeddingsReindexProgressType, @@ -17,6 +17,13 @@ import { FrigateStats } from "@/types/stats"; import { createContainer } from "react-tracked"; import useDeepMemo from "@/hooks/use-deep-memo"; +export type WsFeedMessage = { + topic: string; + payload: unknown; + timestamp: number; + id: string; +}; + type Update = { topic: string; payload: unknown; @@ -29,6 +36,9 @@ type WsState = { type useValueReturn = [WsState, (update: Update) => void]; +const wsMessageSubscribers = new Set<(msg: WsFeedMessage) => void>(); +let wsMessageIdCounter = 0; + function useValue(): useValueReturn { const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`; @@ -43,8 +53,13 @@ function useValue(): useValueReturn { return; } - const cameraActivity: { [key: string]: FrigateCameraState } = - JSON.parse(activityValue); + let cameraActivity: { [key: string]: Partial }; + + try { + cameraActivity = JSON.parse(activityValue); + } catch { + return; + } if (Object.keys(cameraActivity).length === 0) { return; @@ -53,6 +68,12 @@ function useValue(): useValueReturn { const cameraStates: WsState = {}; Object.entries(cameraActivity).forEach(([name, state]) => { + const cameraConfig = state?.config; + + if (!cameraConfig) { + return; + } + const { record, detect, @@ -67,7 +88,7 @@ function useValue(): useValueReturn { detections, object_descriptions, review_descriptions, - } = state["config"]; + } = cameraConfig; cameraStates[`${name}/recordings/state`] = record ? "ON" : "OFF"; cameraStates[`${name}/enabled/state`] = enabled ? "ON" : "OFF"; cameraStates[`${name}/detect/state`] = detect ? "ON" : "OFF"; @@ -115,6 +136,17 @@ function useValue(): useValueReturn { ...prevState, [data.topic]: data.payload, })); + + // Notify feed subscribers + if (wsMessageSubscribers.size > 0) { + const feedMsg: WsFeedMessage = { + topic: data.topic, + payload: data.payload, + timestamp: Date.now(), + id: String(wsMessageIdCounter++), + }; + wsMessageSubscribers.forEach((cb) => cb(feedMsg)); + } } }, onOpen: () => { @@ -740,3 +772,16 @@ export function useJobStatus( return { payload: currentJob as Job | null }; } + +export function useWsMessageSubscribe(callback: (msg: WsFeedMessage) => void) { + const callbackRef = useRef(callback); + callbackRef.current = callback; + + useEffect(() => { + const handler = (msg: WsFeedMessage) => callbackRef.current(msg); + wsMessageSubscribers.add(handler); + return () => { + wsMessageSubscribers.delete(handler); + }; + }, []); +} diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx index 716e63f57..f0c05995e 100644 --- a/web/src/components/camera/CameraImage.tsx +++ b/web/src/components/camera/CameraImage.tsx @@ -26,7 +26,8 @@ export default function CameraImage({ const containerRef = useRef(null); const imgRef = useRef(null); - const { name } = config ? config.cameras[camera] : ""; + const cameraConfig = config?.cameras?.[camera]; + const { name } = cameraConfig ?? { name: camera }; const { payload: enabledState } = useEnabledState(camera); const enabled = enabledState ? enabledState === "ON" : true; @@ -34,15 +35,15 @@ export default function CameraImage({ useResizeObserver(containerRef); const requestHeight = useMemo(() => { - if (!config || containerHeight == 0) { + if (!cameraConfig || containerHeight == 0) { return 360; } return Math.min( - config.cameras[camera].detect.height, + cameraConfig.detect.height, Math.round(containerHeight * (isDesktop ? 1.1 : 1.25)), ); - }, [config, camera, containerHeight]); + }, [cameraConfig, containerHeight]); const [isPortraitImage, setIsPortraitImage] = useState(false); diff --git a/web/src/components/config-form/section-configs/motion.ts b/web/src/components/config-form/section-configs/motion.ts index 0acdc0d99..41b5738d3 100644 --- a/web/src/components/config-form/section-configs/motion.ts +++ b/web/src/components/config-form/section-configs/motion.ts @@ -44,6 +44,30 @@ const motion: SectionConfigOverrides = { camera: { restartRequired: ["frame_height"], }, + replay: { + restartRequired: [], + fieldOrder: [ + "threshold", + "contour_area", + "lightning_threshold", + "improve_contrast", + ], + fieldGroups: { + sensitivity: ["threshold", "contour_area"], + algorithm: ["improve_contrast"], + }, + hiddenFields: [ + "enabled", + "enabled_in_config", + "mask", + "raw_mask", + "mqtt_off_delay", + "delta_alpha", + "frame_alpha", + "frame_height", + ], + advancedFields: ["lightning_threshold"], + }, }; export default motion; diff --git a/web/src/components/config-form/section-configs/objects.ts b/web/src/components/config-form/section-configs/objects.ts index 1dfb31053..a70746c49 100644 --- a/web/src/components/config-form/section-configs/objects.ts +++ b/web/src/components/config-form/section-configs/objects.ts @@ -99,6 +99,28 @@ const objects: SectionConfigOverrides = { camera: { restartRequired: [], }, + replay: { + restartRequired: [], + fieldOrder: ["track", "filters"], + fieldGroups: { + tracking: ["track"], + filtering: ["filters"], + }, + hiddenFields: [ + "enabled_in_config", + "alert", + "detect", + "mask", + "raw_mask", + "genai", + "genai.enabled_in_config", + "filters.*.mask", + "filters.*.raw_mask", + "filters.mask", + "filters.raw_mask", + ], + advancedFields: [], + }, }; export default objects; diff --git a/web/src/components/config-form/section-configs/types.ts b/web/src/components/config-form/section-configs/types.ts index 600a3ca50..e2b308e08 100644 --- a/web/src/components/config-form/section-configs/types.ts +++ b/web/src/components/config-form/section-configs/types.ts @@ -4,4 +4,5 @@ export type SectionConfigOverrides = { base?: SectionConfig; global?: Partial; camera?: Partial; + replay?: Partial; }; diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 606919dcc..821857ae6 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -95,9 +95,9 @@ export interface SectionConfig { } export interface BaseSectionProps { - /** Whether this is at global or camera level */ - level: "global" | "camera"; - /** Camera name (required if level is "camera") */ + /** Whether this is at global, camera, or replay level */ + level: "global" | "camera" | "replay"; + /** Camera name (required if level is "camera" or "replay") */ cameraName?: string; /** Whether to show override indicator badge */ showOverrideIndicator?: boolean; @@ -117,6 +117,10 @@ export interface BaseSectionProps { defaultCollapsed?: boolean; /** Whether to show the section title (default: false for global, true for camera) */ showTitle?: boolean; + /** If true, apply config in-memory only without writing to YAML */ + skipSave?: boolean; + /** If true, buttons are not sticky at the bottom */ + noStickyButtons?: boolean; /** Callback when section status changes */ onStatusChange?: (status: { hasChanges: boolean; @@ -156,12 +160,16 @@ export function ConfigSection({ collapsible = false, defaultCollapsed = true, showTitle, + skipSave = false, + noStickyButtons = false, onStatusChange, pendingDataBySection, onPendingDataChange, }: ConfigSectionProps) { + // For replay level, treat as camera-level config access + const effectiveLevel = level === "replay" ? "camera" : level; const { t, i18n } = useTranslation([ - level === "camera" ? "config/cameras" : "config/global", + effectiveLevel === "camera" ? "config/cameras" : "config/global", "config/cameras", "views/settings", "common", @@ -174,10 +182,10 @@ export function ConfigSection({ // Create a key for this section's pending data const pendingDataKey = useMemo( () => - level === "camera" && cameraName + effectiveLevel === "camera" && cameraName ? `${cameraName}::${sectionPath}` : sectionPath, - [level, cameraName, sectionPath], + [effectiveLevel, cameraName, sectionPath], ); // Use pending data from parent if available, otherwise use local state @@ -222,20 +230,20 @@ export function ConfigSection({ const lastPendingDataKeyRef = useRef(null); const updateTopic = - level === "camera" && cameraName + effectiveLevel === "camera" && cameraName ? cameraUpdateTopicMap[sectionPath] ? `config/cameras/${cameraName}/${cameraUpdateTopicMap[sectionPath]}` : undefined : `config/${sectionPath}`; // Default: show title for camera level (since it might be collapsible), hide for global - const shouldShowTitle = showTitle ?? level === "camera"; + const shouldShowTitle = showTitle ?? effectiveLevel === "camera"; // Fetch config const { data: config, mutate: refreshConfig } = useSWR("config"); // Get section schema using cached hook - const sectionSchema = useSectionSchema(sectionPath, level); + const sectionSchema = useSectionSchema(sectionPath, effectiveLevel); // Apply special case handling for sections with problematic schema defaults const modifiedSchema = useMemo( @@ -247,7 +255,7 @@ export function ConfigSection({ // Get override status const { isOverridden, globalValue, cameraValue } = useConfigOverride({ config, - cameraName: level === "camera" ? cameraName : undefined, + cameraName: effectiveLevel === "camera" ? cameraName : undefined, sectionPath, compareFields: sectionConfig.overrideFields, }); @@ -256,12 +264,12 @@ export function ConfigSection({ const rawSectionValue = useMemo(() => { if (!config) return undefined; - if (level === "camera" && cameraName) { + if (effectiveLevel === "camera" && cameraName) { return get(config.cameras?.[cameraName], sectionPath); } return get(config, sectionPath); - }, [config, level, cameraName, sectionPath]); + }, [config, cameraName, sectionPath, effectiveLevel]); const rawFormData = useMemo(() => { if (!config) return {}; @@ -328,9 +336,10 @@ export function ConfigSection({ [rawFormData, sanitizeSectionData], ); - // Clear pendingData whenever formData changes (e.g., from server refresh) - // This prevents RJSF's initial onChange call from being treated as a user edit - // Only clear if pendingData is managed locally (not by parent) + // Clear pendingData whenever the section/camera key changes (e.g., switching + // cameras) or when there is no pending data yet (initialization). + // This prevents RJSF's initial onChange call from being treated as a user edit. + // Only clear if pendingData is managed locally (not by parent). useEffect(() => { const pendingKeyChanged = lastPendingDataKeyRef.current !== pendingDataKey; @@ -339,15 +348,16 @@ export function ConfigSection({ isInitializingRef.current = true; setPendingOverrides(undefined); setDirtyOverrides(undefined); + + // Reset local pending data when switching sections/cameras + if (onPendingDataChange === undefined) { + setPendingData(null); + } } else if (!pendingData) { isInitializingRef.current = true; setPendingOverrides(undefined); setDirtyOverrides(undefined); } - - if (onPendingDataChange === undefined) { - setPendingData(null); - } }, [ onPendingDataChange, pendingData, @@ -484,7 +494,7 @@ export function ConfigSection({ setIsSaving(true); try { const basePath = - level === "camera" && cameraName + effectiveLevel === "camera" && cameraName ? `cameras.${cameraName}.${sectionPath}` : sectionPath; const rawData = sanitizeSectionData(rawFormData); @@ -495,7 +505,7 @@ export function ConfigSection({ ); const sanitizedOverrides = sanitizeOverridesForSection( sectionPath, - level, + effectiveLevel, overrides, ); @@ -508,16 +518,26 @@ export function ConfigSection({ return; } - const needsRestart = requiresRestartForOverrides(sanitizedOverrides); + const needsRestart = skipSave + ? false + : requiresRestartForOverrides(sanitizedOverrides); const configData = buildConfigDataForPath(basePath, sanitizedOverrides); await axios.put("config/set", { requires_restart: needsRestart ? 1 : 0, update_topic: updateTopic, config_data: configData, + ...(skipSave ? { skip_save: true } : {}), }); - if (needsRestart) { + if (skipSave) { + toast.success( + t("toast.applied", { + ns: "views/settings", + defaultValue: "Settings applied successfully", + }), + ); + } else if (needsRestart) { statusBar?.addMessage( "config_restart_required", t("configForm.restartRequiredFooter", { @@ -596,7 +616,7 @@ export function ConfigSection({ }, [ sectionPath, pendingData, - level, + effectiveLevel, cameraName, t, refreshConfig, @@ -608,15 +628,16 @@ export function ConfigSection({ updateTopic, setPendingData, requiresRestartForOverrides, + skipSave, ]); // Handle reset to global/defaults - removes camera-level override or resets global to defaults const handleResetToGlobal = useCallback(async () => { - if (level === "camera" && !cameraName) return; + if (effectiveLevel === "camera" && !cameraName) return; try { const basePath = - level === "camera" && cameraName + effectiveLevel === "camera" && cameraName ? `cameras.${cameraName}.${sectionPath}` : sectionPath; @@ -632,7 +653,7 @@ export function ConfigSection({ t("toast.resetSuccess", { ns: "views/settings", defaultValue: - level === "global" + effectiveLevel === "global" ? "Reset to defaults" : "Reset to global defaults", }), @@ -651,7 +672,7 @@ export function ConfigSection({ } }, [ sectionPath, - level, + effectiveLevel, cameraName, requiresRestart, t, @@ -661,8 +682,8 @@ export function ConfigSection({ ]); const sectionValidation = useMemo( - () => getSectionValidation({ sectionPath, level, t }), - [sectionPath, level, t], + () => getSectionValidation({ sectionPath, level: effectiveLevel, t }), + [sectionPath, effectiveLevel, t], ); const customValidate = useMemo(() => { @@ -733,7 +754,7 @@ export function ConfigSection({ // nested under the section name (e.g., `audio.label`). For global-level // sections, keys are nested under the section name in `config/global`. const configNamespace = - level === "camera" ? "config/cameras" : "config/global"; + effectiveLevel === "camera" ? "config/cameras" : "config/global"; const title = t(`${sectionPath}.label`, { ns: configNamespace, defaultValue: defaultTitle, @@ -769,7 +790,7 @@ export function ConfigSection({ i18nNamespace={configNamespace} customValidate={customValidate} formContext={{ - level, + level: effectiveLevel, cameraName, globalValue, cameraValue, @@ -784,7 +805,7 @@ export function ConfigSection({ onFormDataChange: (data: ConfigSectionData) => handleChange(data), // For widgets that need access to full camera config (e.g., zone names) fullCameraConfig: - level === "camera" && cameraName + effectiveLevel === "camera" && cameraName ? config?.cameras?.[cameraName] : undefined, fullConfig: config, @@ -804,7 +825,12 @@ export function ConfigSection({ }} /> -
+
)}
- {((level === "camera" && isOverridden) || level === "global") && - !hasChanges && ( + {((effectiveLevel === "camera" && isOverridden) || + effectiveLevel === "global") && + !hasChanges && + !skipSave && ( + + ), + }, + ); + } else { + toast.error(t("dialog.toast.error", { error: errorMessage }), { + position: "top-center", + }); + } + }) + .finally(() => { + setIsStarting(false); + }); + }, + [navigate, t], + ); + const MenuItem = isContextMenu ? ContextMenuItem : DropdownMenuItem; const menuItems = ( @@ -149,6 +205,20 @@ export default function SearchResultActions({ {t("itemMenu.addTrigger.label")} )} + {searchResult.has_clip && ( + { + handleDebugReplay(searchResult); + }} + > + {isStarting + ? t("dialog.starting", { ns: "views/replay" }) + : t("itemMenu.debugReplay.label")} + + )} {isAdmin && ( void; + onExportClick: () => void; +}; + +export default function ActionsDropdown({ + onDebugReplayClick, + onExportClick, +}: ActionsDropdownProps) { + const { t } = useTranslation(["components/dialog", "views/replay", "common"]); + + return ( + + + + + + + {t("menu.export", { ns: "common" })} + + + {t("title", { ns: "views/replay" })} + + + + ); +} diff --git a/web/src/components/overlay/CustomTimeSelector.tsx b/web/src/components/overlay/CustomTimeSelector.tsx new file mode 100644 index 000000000..0d9a4d052 --- /dev/null +++ b/web/src/components/overlay/CustomTimeSelector.tsx @@ -0,0 +1,240 @@ +import { useMemo, useState } from "react"; +import { Button } from "../ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { SelectSeparator } from "../ui/select"; +import { TimeRange } from "@/types/timeline"; +import { useFormattedTimestamp } from "@/hooks/use-date-utils"; +import { getUTCOffset } from "@/utils/dateUtil"; +import { TimezoneAwareCalendar } from "./ReviewActivityCalendar"; +import { FaArrowRight, FaCalendarAlt } from "react-icons/fa"; +import { isDesktop, isIOS } from "react-device-detect"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useTranslation } from "react-i18next"; + +type CustomTimeSelectorProps = { + latestTime: number; + range?: TimeRange; + setRange: (range: TimeRange | undefined) => void; + startLabel: string; + endLabel: string; +}; + +export function CustomTimeSelector({ + latestTime, + range, + setRange, + startLabel, + endLabel, +}: CustomTimeSelectorProps) { + const { t } = useTranslation(["common"]); + const { data: config } = useSWR("config"); + + // times + const timezoneOffset = useMemo( + () => + config?.ui.timezone + ? Math.round(getUTCOffset(new Date(), config.ui.timezone)) + : undefined, + [config?.ui.timezone], + ); + const localTimeOffset = useMemo( + () => + Math.round( + getUTCOffset( + new Date(), + Intl.DateTimeFormat().resolvedOptions().timeZone, + ), + ), + [], + ); + + const startTime = useMemo(() => { + let time = range?.after || latestTime - 3600; + + if (timezoneOffset) { + time = time + (timezoneOffset - localTimeOffset) * 60; + } + + return time; + }, [range, latestTime, timezoneOffset, localTimeOffset]); + + const endTime = useMemo(() => { + let time = range?.before || latestTime; + + if (timezoneOffset) { + time = time + (timezoneOffset - localTimeOffset) * 60; + } + + return time; + }, [range, latestTime, timezoneOffset, localTimeOffset]); + + const formattedStart = useFormattedTimestamp( + startTime, + config?.ui.time_format == "24hour" + ? t("time.formattedTimestamp.24hour") + : t("time.formattedTimestamp.12hour"), + ); + + const formattedEnd = useFormattedTimestamp( + endTime, + config?.ui.time_format == "24hour" + ? t("time.formattedTimestamp.24hour") + : t("time.formattedTimestamp.12hour"), + ); + + const startClock = useMemo(() => { + const date = new Date(startTime * 1000); + return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; + }, [startTime]); + + const endClock = useMemo(() => { + const date = new Date(endTime * 1000); + return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; + }, [endTime]); + + // calendars + const [startOpen, setStartOpen] = useState(false); + const [endOpen, setEndOpen] = useState(false); + + return ( +
+ +
+ { + if (!open) { + setStartOpen(false); + } + }} + > + + + + + { + if (!day) { + return; + } + + setRange({ + before: endTime, + after: day.getTime() / 1000 + 1, + }); + }} + /> + + { + const clock = e.target.value; + const [hour, minute, second] = isIOS + ? [...clock.split(":"), "00"] + : clock.split(":"); + + const start = new Date(startTime * 1000); + start.setHours( + parseInt(hour), + parseInt(minute), + parseInt(second ?? 0), + 0, + ); + setRange({ + before: endTime, + after: start.getTime() / 1000, + }); + }} + /> + + + + { + if (!open) { + setEndOpen(false); + } + }} + > + + + + + { + if (!day) { + return; + } + + setRange({ + after: startTime, + before: day.getTime() / 1000, + }); + }} + /> + + { + const clock = e.target.value; + const [hour, minute, second] = isIOS + ? [...clock.split(":"), "00"] + : clock.split(":"); + + const end = new Date(endTime * 1000); + end.setHours( + parseInt(hour), + parseInt(minute), + parseInt(second ?? 0), + 0, + ); + setRange({ + before: end.getTime() / 1000, + after: startTime, + }); + }} + /> + + +
+
+ ); +} diff --git a/web/src/components/overlay/DebugReplayDialog.tsx b/web/src/components/overlay/DebugReplayDialog.tsx new file mode 100644 index 000000000..9c3efb4f5 --- /dev/null +++ b/web/src/components/overlay/DebugReplayDialog.tsx @@ -0,0 +1,367 @@ +import { useCallback, useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../ui/dialog"; +import { Label } from "../ui/label"; +import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; +import { Button } from "../ui/button"; +import axios from "axios"; +import { toast } from "sonner"; +import { isDesktop } from "react-device-detect"; +import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { SelectSeparator } from "../ui/select"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { LuBug, LuPlay, LuX } from "react-icons/lu"; +import { ExportMode } from "@/types/filter"; +import { TimeRange } from "@/types/timeline"; +import { cn } from "@/lib/utils"; +import { CustomTimeSelector } from "./CustomTimeSelector"; + +const REPLAY_TIME_OPTIONS = ["1", "5", "timeline", "custom"] as const; +type ReplayTimeOption = (typeof REPLAY_TIME_OPTIONS)[number]; + +type DebugReplayContentProps = { + currentTime: number; + latestTime: number; + range?: TimeRange; + selectedOption: ReplayTimeOption; + isStarting: boolean; + onSelectedOptionChange: (option: ReplayTimeOption) => void; + onStart: () => void; + onCancel: () => void; + setRange: (range: TimeRange | undefined) => void; + setMode: (mode: ExportMode) => void; +}; + +export function DebugReplayContent({ + currentTime, + latestTime, + range, + selectedOption, + isStarting, + onSelectedOptionChange, + onStart, + onCancel, + setRange, + setMode, +}: DebugReplayContentProps) { + const { t } = useTranslation(["views/replay"]); + + return ( +
+ {isDesktop && ( + <> + + {t("dialog.title")} + {t("dialog.description")} + + + + )} + + {/* Time range */} +
+ + onSelectedOptionChange(value as ReplayTimeOption) + } + > + {REPLAY_TIME_OPTIONS.map((opt) => ( +
+ + +
+ ))} +
+
+ + {/* Custom time inputs */} + {selectedOption === "custom" && ( + + )} + + {isDesktop && } + + +
+ {t("button.cancel", { ns: "common" })} +
+ +
+
+ ); +} + +type DebugReplayDialogProps = { + camera: string; + currentTime: number; + latestTime: number; + range?: TimeRange; + mode: ExportMode; + setRange: (range: TimeRange | undefined) => void; + setMode: (mode: ExportMode) => void; +}; + +export default function DebugReplayDialog({ + camera, + currentTime, + latestTime, + range, + mode, + setRange, + setMode, +}: DebugReplayDialogProps) { + const { t } = useTranslation(["views/replay"]); + const navigate = useNavigate(); + + const [selectedOption, setSelectedOption] = useState("1"); + const [isStarting, setIsStarting] = useState(false); + + const handleTimeOptionChange = useCallback( + (option: ReplayTimeOption) => { + setSelectedOption(option); + + if (option === "custom" || option === "timeline") { + return; + } + + const minutes = parseInt(option, 10); + const end = latestTime; + setRange({ after: end - minutes * 60, before: end }); + }, + [latestTime, setRange], + ); + + const handleStart = useCallback(() => { + if (!range || range.before <= range.after) { + toast.error( + t("dialog.toast.error", { error: "End time must be after start time" }), + { position: "top-center" }, + ); + return; + } + + setIsStarting(true); + + axios + .post("debug_replay/start", { + camera: camera, + start_time: range.after, + end_time: range.before, + }) + .then((response) => { + if (response.status === 200) { + toast.success(t("dialog.toast.success"), { + position: "top-center", + }); + setMode("none"); + setRange(undefined); + navigate("/replay"); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + + if (error.response?.status === 409) { + toast.error(t("dialog.toast.alreadyActive"), { + position: "top-center", + closeButton: true, + dismissible: false, + action: ( + + + + ), + }); + } else { + toast.error(t("dialog.toast.error", { error: errorMessage }), { + position: "top-center", + }); + } + }) + .finally(() => { + setIsStarting(false); + }); + }, [camera, range, navigate, setMode, setRange, t]); + + const handleCancel = useCallback(() => { + setMode("none"); + setRange(undefined); + }, [setMode, setRange]); + + const Overlay = isDesktop ? Dialog : Drawer; + const Trigger = isDesktop ? DialogTrigger : DrawerTrigger; + const Content = isDesktop ? DialogContent : DrawerContent; + + return ( + <> + + { + if (!open) { + setMode("none"); + } + }} + > + {!isDesktop && ( + + + + )} + + + + + + ); +} + +type SaveDebugReplayOverlayProps = { + className: string; + show: boolean; + isStarting: boolean; + onSave: () => void; + onCancel: () => void; +}; + +export function SaveDebugReplayOverlay({ + className, + show, + isStarting, + onSave, + onCancel, +}: SaveDebugReplayOverlayProps) { + const { t } = useTranslation(["views/replay"]); + + return ( +
+
+ + +
+
+ ); +} diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index 738aa689e..6912ebf46 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import { Dialog, DialogContent, @@ -12,16 +12,12 @@ import { Label } from "../ui/label"; import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import { Button } from "../ui/button"; import { ExportMode } from "@/types/filter"; -import { FaArrowDown, FaArrowRight, FaCalendarAlt } from "react-icons/fa"; +import { FaArrowDown } from "react-icons/fa"; import axios from "axios"; import { toast } from "sonner"; import { Input } from "../ui/input"; import { TimeRange } from "@/types/timeline"; -import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import useSWR from "swr"; -import { FrigateConfig } from "@/types/frigateConfig"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -import { TimezoneAwareCalendar } from "./ReviewActivityCalendar"; import { Select, SelectContent, @@ -30,15 +26,15 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; -import { isDesktop, isIOS, isMobile } from "react-device-detect"; +import { isDesktop, isMobile } from "react-device-detect"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import SaveExportOverlay from "./SaveExportOverlay"; -import { getUTCOffset } from "@/utils/dateUtil"; import { baseUrl } from "@/api/baseUrl"; import { cn } from "@/lib/utils"; import { GenericVideoPlayer } from "../player/GenericVideoPlayer"; import { useTranslation } from "react-i18next"; import { ExportCase } from "@/types/export"; +import { CustomTimeSelector } from "./CustomTimeSelector"; const EXPORT_OPTIONS = [ "1", @@ -167,31 +163,33 @@ export default function ExportDialog({ } }} > - - - + {!isDesktop && ( + + + + )} )} void; -}; -function CustomTimeSelector({ - latestTime, - range, - setRange, -}: CustomTimeSelectorProps) { - const { t } = useTranslation(["components/dialog"]); - const { data: config } = useSWR("config"); - - // times - - const timezoneOffset = useMemo( - () => - config?.ui.timezone - ? Math.round(getUTCOffset(new Date(), config.ui.timezone)) - : undefined, - [config?.ui.timezone], - ); - const localTimeOffset = useMemo( - () => - Math.round( - getUTCOffset( - new Date(), - Intl.DateTimeFormat().resolvedOptions().timeZone, - ), - ), - [], - ); - - const startTime = useMemo(() => { - let time = range?.after || latestTime - 3600; - - if (timezoneOffset) { - time = time + (timezoneOffset - localTimeOffset) * 60; - } - - return time; - }, [range, latestTime, timezoneOffset, localTimeOffset]); - const endTime = useMemo(() => { - let time = range?.before || latestTime; - - if (timezoneOffset) { - time = time + (timezoneOffset - localTimeOffset) * 60; - } - - return time; - }, [range, latestTime, timezoneOffset, localTimeOffset]); - const formattedStart = useFormattedTimestamp( - startTime, - config?.ui.time_format == "24hour" - ? t("time.formattedTimestamp.24hour", { ns: "common" }) - : t("time.formattedTimestamp.12hour", { ns: "common" }), - ); - const formattedEnd = useFormattedTimestamp( - endTime, - config?.ui.time_format == "24hour" - ? t("time.formattedTimestamp.24hour", { ns: "common" }) - : t("time.formattedTimestamp.12hour", { ns: "common" }), - ); - - const startClock = useMemo(() => { - const date = new Date(startTime * 1000); - return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; - }, [startTime]); - const endClock = useMemo(() => { - const date = new Date(endTime * 1000); - return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; - }, [endTime]); - - // calendars - - const [startOpen, setStartOpen] = useState(false); - const [endOpen, setEndOpen] = useState(false); - - return ( -
- -
- { - if (!open) { - setStartOpen(false); - } - }} - > - - - - - { - if (!day) { - return; - } - - setRange({ - before: endTime, - after: day.getTime() / 1000 + 1, - }); - }} - /> - - { - const clock = e.target.value; - const [hour, minute, second] = isIOS - ? [...clock.split(":"), "00"] - : clock.split(":"); - - const start = new Date(startTime * 1000); - start.setHours( - parseInt(hour), - parseInt(minute), - parseInt(second ?? 0), - 0, - ); - setRange({ - before: endTime, - after: start.getTime() / 1000, - }); - }} - /> - - - - { - if (!open) { - setEndOpen(false); - } - }} - > - - - - - { - if (!day) { - return; - } - - setRange({ - after: startTime, - before: day.getTime() / 1000, - }); - }} - /> - - { - const clock = e.target.value; - const [hour, minute, second] = isIOS - ? [...clock.split(":"), "00"] - : clock.split(":"); - - const end = new Date(endTime * 1000); - end.setHours( - parseInt(hour), - parseInt(minute), - parseInt(second ?? 0), - 0, - ); - setRange({ - before: end.getTime() / 1000, - after: startTime, - }); - }} - /> - - -
-
- ); -} - type ExportPreviewDialogProps = { camera: string; range?: TimeRange; diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index 84f9051ea..77cb8e3f4 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -2,8 +2,13 @@ import { useCallback, useState } from "react"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Button } from "../ui/button"; import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa"; +import { LuBug } from "react-icons/lu"; import { TimeRange } from "@/types/timeline"; import { ExportContent, ExportPreviewDialog } from "./ExportDialog"; +import { + DebugReplayContent, + SaveDebugReplayOverlay, +} from "./DebugReplayDialog"; import { ExportMode, GeneralFilter } from "@/types/filter"; import ReviewActivityCalendar from "./ReviewActivityCalendar"; import { SelectSeparator } from "../ui/select"; @@ -16,19 +21,32 @@ import { import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import { GeneralFilterContent } from "../filter/ReviewFilterGroup"; import { toast } from "sonner"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; import SaveExportOverlay from "./SaveExportOverlay"; import { isIOS, isMobile } from "react-device-detect"; import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; -type DrawerMode = "none" | "select" | "export" | "calendar" | "filter"; +type DrawerMode = + | "none" + | "select" + | "export" + | "calendar" + | "filter" + | "debug-replay"; -const DRAWER_FEATURES = ["export", "calendar", "filter"] as const; +const DRAWER_FEATURES = [ + "export", + "calendar", + "filter", + "debug-replay", +] as const; export type DrawerFeatures = (typeof DRAWER_FEATURES)[number]; const DEFAULT_DRAWER_FEATURES: DrawerFeatures[] = [ "export", "calendar", "filter", + "debug-replay", ]; type MobileReviewSettingsDrawerProps = { @@ -45,6 +63,10 @@ type MobileReviewSettingsDrawerProps = { recordingsSummary?: RecordingsSummary; allLabels: string[]; allZones: string[]; + debugReplayMode?: ExportMode; + debugReplayRange?: TimeRange; + setDebugReplayMode?: (mode: ExportMode) => void; + setDebugReplayRange?: (range: TimeRange | undefined) => void; onUpdateFilter: (filter: ReviewFilter) => void; setRange: (range: TimeRange | undefined) => void; setMode: (mode: ExportMode) => void; @@ -64,13 +86,26 @@ export default function MobileReviewSettingsDrawer({ recordingsSummary, allLabels, allZones, + debugReplayMode = "none", + debugReplayRange, + setDebugReplayMode = () => {}, + setDebugReplayRange = () => {}, onUpdateFilter, setRange, setMode, setShowExportPreview, }: MobileReviewSettingsDrawerProps) { - const { t } = useTranslation(["views/recording", "components/dialog"]); + const { t } = useTranslation([ + "views/recording", + "components/dialog", + "views/replay", + ]); + const navigate = useNavigate(); const [drawerMode, setDrawerMode] = useState("none"); + const [selectedReplayOption, setSelectedReplayOption] = useState< + "1" | "5" | "custom" | "timeline" + >("1"); + const [isDebugReplayStarting, setIsDebugReplayStarting] = useState(false); // exports @@ -140,6 +175,76 @@ export default function MobileReviewSettingsDrawer({ }); }, [camera, name, range, selectedCaseId, setRange, setName, setMode, t]); + const onStartDebugReplay = useCallback(async () => { + if ( + !debugReplayRange || + debugReplayRange.before <= debugReplayRange.after + ) { + toast.error( + t("dialog.toast.error", { + error: "End time must be after start time", + ns: "views/replay", + }), + { position: "top-center" }, + ); + return; + } + + setIsDebugReplayStarting(true); + + try { + const response = await axios.post("debug_replay/start", { + camera: camera, + start_time: debugReplayRange.after, + end_time: debugReplayRange.before, + }); + + if (response.status === 200) { + toast.success(t("dialog.toast.success", { ns: "views/replay" }), { + position: "top-center", + }); + setDebugReplayMode("none"); + setDebugReplayRange(undefined); + setDrawerMode("none"); + navigate("/replay"); + } + } catch (error) { + const axiosError = error as AxiosError<{ + message?: string; + detail?: string; + }>; + const errorMessage = + axiosError.response?.data?.message || + axiosError.response?.data?.detail || + "Unknown error"; + + if (axiosError.response?.status === 409) { + toast.error(t("dialog.toast.alreadyActive", { ns: "views/replay" }), { + position: "top-center", + }); + } else { + toast.error( + t("dialog.toast.error", { + error: errorMessage, + ns: "views/replay", + }), + { + position: "top-center", + }, + ); + } + } finally { + setIsDebugReplayStarting(false); + } + }, [ + camera, + debugReplayRange, + navigate, + setDebugReplayMode, + setDebugReplayRange, + t, + ]); + // filters const [currentFilter, setCurrentFilter] = useState({ @@ -196,6 +301,26 @@ export default function MobileReviewSettingsDrawer({ {t("filter")} )} + {features.includes("debug-replay") && ( + + )}
); } else if (drawerMode == "export") { @@ -311,6 +436,47 @@ export default function MobileReviewSettingsDrawer({ />
); + } else if (drawerMode == "debug-replay") { + const handleTimeOptionChange = ( + option: "1" | "5" | "custom" | "timeline", + ) => { + setSelectedReplayOption(option); + + if (option === "custom" || option === "timeline") { + return; + } + + const hours = parseInt(option); + const end = latestTime; + const now = new Date(end * 1000); + now.setHours(now.getHours() - hours); + setDebugReplayRange({ after: now.getTime() / 1000, before: end }); + }; + + content = ( + { + setDebugReplayMode("none"); + setDebugReplayRange(undefined); + setDrawerMode("select"); + }} + setRange={setDebugReplayRange} + setMode={(mode) => { + setDebugReplayMode(mode); + + if (mode == "timeline") { + setDrawerMode("none"); + } + }} + /> + ); } return ( @@ -322,6 +488,16 @@ export default function MobileReviewSettingsDrawer({ onCancel={() => setMode("none")} onPreview={() => setShowExportPreview(true)} /> + { + setDebugReplayMode("none"); + setDebugReplayRange(undefined); + }} + /> - + {content} diff --git a/web/src/components/timeline/EventMenu.tsx b/web/src/components/timeline/EventMenu.tsx index e6ad8eba4..98c514945 100644 --- a/web/src/components/timeline/EventMenu.tsx +++ b/web/src/components/timeline/EventMenu.tsx @@ -12,8 +12,11 @@ import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Event } from "@/types/event"; import { FrigateConfig } from "@/types/frigateConfig"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { useIsAdmin } from "@/hooks/use-is-admin"; +import axios from "axios"; +import { toast } from "sonner"; +import { Button } from "../ui/button"; type EventMenuProps = { event: Event; @@ -34,9 +37,10 @@ export default function EventMenu({ }: EventMenuProps) { const apiHost = useApiHost(); const navigate = useNavigate(); - const { t } = useTranslation("views/explore"); + const { t } = useTranslation(["views/explore", "views/replay"]); const [isOpen, setIsOpen] = useState(false); const isAdmin = useIsAdmin(); + const [isStarting, setIsStarting] = useState(false); const handleObjectSelect = () => { if (isSelected) { @@ -46,6 +50,59 @@ export default function EventMenu({ } }; + const handleDebugReplay = useCallback( + (event: Event) => { + setIsStarting(true); + + axios + .post("debug_replay/start", { + camera: event.camera, + start_time: event.start_time, + end_time: event.end_time, + }) + .then((response) => { + if (response.status === 200) { + toast.success(t("dialog.toast.success", { ns: "views/replay" }), { + position: "top-center", + }); + navigate("/replay"); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + + if (error.response?.status === 409) { + toast.error( + t("dialog.toast.alreadyActive", { ns: "views/replay" }), + { + position: "top-center", + closeButton: true, + dismissible: false, + action: ( + + + + ), + }, + ); + } else { + toast.error(t("dialog.toast.error", { error: errorMessage }), { + position: "top-center", + }); + } + }) + .finally(() => { + setIsStarting(false); + }); + }, + [navigate, t], + ); + return ( <> @@ -117,6 +174,19 @@ export default function EventMenu({ {t("itemMenu.findSimilar.label")} )} + {event.has_clip && ( + { + handleDebugReplay(event); + }} + > + {isStarting + ? t("dialog.starting", { ns: "views/replay" }) + : t("itemMenu.debugReplay.label")} + + )} diff --git a/web/src/components/ws/WsMessageFeed.tsx b/web/src/components/ws/WsMessageFeed.tsx new file mode 100644 index 000000000..0da86a108 --- /dev/null +++ b/web/src/components/ws/WsMessageFeed.tsx @@ -0,0 +1,608 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { WsFeedMessage } from "@/api/ws"; +import { useWsMessageBuffer } from "@/hooks/use-ws-message-buffer"; +import WsMessageRow from "./WsMessageRow"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { FaEraser, FaFilter, FaPause, FaPlay, FaVideo } from "react-icons/fa"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; +import FilterSwitch from "@/components/filter/FilterSwitch"; +import { isMobile } from "react-device-detect"; +import { isReplayCamera } from "@/utils/cameraUtil"; + +type TopicCategory = + | "events" + | "camera_activity" + | "system" + | "reviews" + | "classification" + | "face_recognition" + | "lpr"; + +const ALL_TOPIC_CATEGORIES: TopicCategory[] = [ + "events", + "reviews", + "classification", + "face_recognition", + "lpr", + "camera_activity", + "system", +]; + +const PRESET_TOPICS: Record> = { + events: new Set(["events", "triggers"]), + reviews: new Set(["reviews"]), + classification: new Set(["tracked_object_update"]), + face_recognition: new Set(["tracked_object_update"]), + lpr: new Set(["tracked_object_update"]), + camera_activity: new Set(["camera_activity", "audio_detections"]), + system: new Set([ + "stats", + "model_state", + "job_state", + "embeddings_reindex_progress", + "audio_transcription_state", + "birdseye_layout", + ]), +}; + +// Maps tracked_object_update payload type to TopicCategory +const TRACKED_UPDATE_TYPE_MAP: Record = { + classification: "classification", + face: "face_recognition", + lpr: "lpr", +}; + +// camera_activity preset also matches topics with camera prefix patterns +const CAMERA_ACTIVITY_TOPIC_PATTERNS = [ + "/motion", + "/audio", + "/detect", + "/recordings", + "/enabled", + "/snapshots", + "/ptz", +]; + +function matchesCategories( + msg: WsFeedMessage, + categories: TopicCategory[] | undefined, +): boolean { + // undefined means all topics + if (!categories) return true; + + const { topic, payload } = msg; + + // Handle tracked_object_update with payload-based sub-categories + if (topic === "tracked_object_update") { + // payload might be a JSON string or a parsed object + let data: unknown = payload; + if (typeof data === "string") { + try { + data = JSON.parse(data); + } catch { + // not valid JSON, fall through + } + } + + const updateType = + data && typeof data === "object" && "type" in data + ? (data as { type: string }).type + : undefined; + + if (updateType && updateType in TRACKED_UPDATE_TYPE_MAP) { + const mappedCategory = TRACKED_UPDATE_TYPE_MAP[updateType]; + return categories.includes(mappedCategory); + } + + // tracked_object_update with other types (e.g. "description") falls under "events" + return categories.includes("events"); + } + + for (const cat of categories) { + const topicSet = PRESET_TOPICS[cat]; + if (topicSet.has(topic)) return true; + + if (cat === "camera_activity") { + if ( + CAMERA_ACTIVITY_TOPIC_PATTERNS.some((pattern) => + topic.includes(pattern), + ) + ) { + return true; + } + } + } + + return false; +} + +type WsMessageFeedProps = { + maxSize?: number; + defaultCamera?: string; + lockedCamera?: string; + showCameraBadge?: boolean; +}; + +export default function WsMessageFeed({ + maxSize = 500, + defaultCamera, + lockedCamera, + showCameraBadge = true, +}: WsMessageFeedProps) { + const { t } = useTranslation(["views/system"]); + const [paused, setPaused] = useState(false); + // undefined = all topics + const [selectedTopics, setSelectedTopics] = useState< + TopicCategory[] | undefined + >(undefined); + // undefined = all cameras + const [selectedCameras, setSelectedCameras] = useState( + () => { + if (lockedCamera) return [lockedCamera]; + if (defaultCamera) return [defaultCamera]; + return undefined; + }, + ); + + const { messages, clear } = useWsMessageBuffer(maxSize, paused, { + cameraFilter: selectedCameras, + }); + + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const availableCameras = useMemo(() => { + if (!config?.cameras) return []; + return Object.keys(config.cameras) + .filter((name) => { + const cam = config.cameras[name]; + return !isReplayCamera(name) && cam.enabled_in_config; + }) + .sort(); + }, [config]); + + const filteredMessages = useMemo(() => { + return messages.filter((msg: WsFeedMessage) => { + if (!matchesCategories(msg, selectedTopics)) return false; + return true; + }); + }, [messages, selectedTopics]); + + // Auto-scroll logic + const scrollContainerRef = useRef(null); + const autoScrollRef = useRef(true); + + const handleScroll = useCallback(() => { + const el = scrollContainerRef.current; + if (!el) return; + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40; + autoScrollRef.current = atBottom; + }, []); + + useEffect(() => { + const el = scrollContainerRef.current; + if (!el || !autoScrollRef.current) return; + el.scrollTop = el.scrollHeight; + }, [filteredMessages.length]); + + return ( +
+ {/* Toolbar */} +
+
+ + + {!lockedCamera && ( + + )} +
+ +
+ + {t("logs.websocket.count", { + count: filteredMessages.length, + })} + + +
+ + + +
+
+
+ + {/* Feed area */} +
+ {filteredMessages.length === 0 ? ( +
+ {t("logs.websocket.empty")} +
+ ) : ( + filteredMessages.map((msg: WsFeedMessage) => ( + + )) + )} +
+
+ ); +} + +// Topic Filter Button + +type TopicFilterButtonProps = { + selectedTopics: TopicCategory[] | undefined; + updateTopicFilter: (topics: TopicCategory[] | undefined) => void; +}; + +function TopicFilterButton({ + selectedTopics, + updateTopicFilter, +}: TopicFilterButtonProps) { + const { t } = useTranslation(["views/system"]); + const [open, setOpen] = useState(false); + const [currentTopics, setCurrentTopics] = useState< + TopicCategory[] | undefined + >(selectedTopics); + + useEffect(() => { + setCurrentTopics(selectedTopics); + }, [selectedTopics]); + + const isFiltered = selectedTopics !== undefined; + + const trigger = ( + + ); + + const content = ( + { + updateTopicFilter(currentTopics); + setOpen(false); + }} + onReset={() => { + setCurrentTopics(undefined); + updateTopicFilter(undefined); + }} + /> + ); + + if (isMobile) { + return ( + { + if (!open) setCurrentTopics(selectedTopics); + setOpen(open); + }} + > + {trigger} + + {content} + + + ); + } + + return ( + { + if (!open) setCurrentTopics(selectedTopics); + setOpen(open); + }} + > + {trigger} + {content} + + ); +} + +type TopicFilterContentProps = { + currentTopics: TopicCategory[] | undefined; + setCurrentTopics: (topics: TopicCategory[] | undefined) => void; + onApply: () => void; + onReset: () => void; +}; + +function TopicFilterContent({ + currentTopics, + setCurrentTopics, + onApply, + onReset, +}: TopicFilterContentProps) { + const { t } = useTranslation(["views/system", "common"]); + + return ( + <> +
+ { + if (isChecked) { + setCurrentTopics(undefined); + } + }} + /> + + {ALL_TOPIC_CATEGORIES.map((cat) => ( + { + if (isChecked) { + const updated = currentTopics ? [...currentTopics, cat] : [cat]; + setCurrentTopics(updated); + } else { + const updated = currentTopics + ? currentTopics.filter((c) => c !== cat) + : []; + if (updated.length === 0) { + setCurrentTopics(undefined); + } else { + setCurrentTopics(updated); + } + } + }} + /> + ))} +
+ +
+ + +
+ + ); +} + +// Camera Filter Button + +type WsCamerasFilterButtonProps = { + allCameras: string[]; + selectedCameras: string[] | undefined; + updateCameraFilter: (cameras: string[] | undefined) => void; +}; + +function WsCamerasFilterButton({ + allCameras, + selectedCameras, + updateCameraFilter, +}: WsCamerasFilterButtonProps) { + const { t } = useTranslation(["views/system", "common"]); + const [open, setOpen] = useState(false); + const [currentCameras, setCurrentCameras] = useState( + selectedCameras, + ); + + useEffect(() => { + setCurrentCameras(selectedCameras); + }, [selectedCameras]); + + const isFiltered = selectedCameras !== undefined; + + const trigger = ( + + ); + + const content = ( + { + updateCameraFilter(currentCameras); + setOpen(false); + }} + onReset={() => { + setCurrentCameras(undefined); + updateCameraFilter(undefined); + }} + /> + ); + + if (isMobile) { + return ( + { + if (!open) setCurrentCameras(selectedCameras); + setOpen(open); + }} + > + {trigger} + + {content} + + + ); + } + + return ( + { + if (!open) setCurrentCameras(selectedCameras); + setOpen(open); + }} + > + {trigger} + {content} + + ); +} + +type WsCamerasFilterContentProps = { + allCameras: string[]; + currentCameras: string[] | undefined; + setCurrentCameras: (cameras: string[] | undefined) => void; + onApply: () => void; + onReset: () => void; +}; + +function WsCamerasFilterContent({ + allCameras, + currentCameras, + setCurrentCameras, + onApply, + onReset, +}: WsCamerasFilterContentProps) { + const { t } = useTranslation(["views/system", "common"]); + + return ( + <> +
+ { + if (isChecked) { + setCurrentCameras(undefined); + } + }} + /> + + {allCameras.map((cam) => ( + { + if (isChecked) { + const updated = currentCameras ? [...currentCameras] : []; + if (!updated.includes(cam)) { + updated.push(cam); + } + setCurrentCameras(updated); + } else { + const updated = currentCameras ? [...currentCameras] : []; + if (updated.length > 1) { + updated.splice(updated.indexOf(cam), 1); + setCurrentCameras(updated); + } + } + }} + /> + ))} +
+ +
+ + +
+ + ); +} diff --git a/web/src/components/ws/WsMessageRow.tsx b/web/src/components/ws/WsMessageRow.tsx new file mode 100644 index 000000000..aa7c89522 --- /dev/null +++ b/web/src/components/ws/WsMessageRow.tsx @@ -0,0 +1,433 @@ +import { memo, useCallback, useState } from "react"; +import { WsFeedMessage } from "@/api/ws"; +import { cn } from "@/lib/utils"; +import { ChevronRight } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { extractCameraName } from "@/utils/wsUtil"; +import { getIconForLabel } from "@/utils/iconUtil"; +import { LuCheck, LuCopy } from "react-icons/lu"; + +type TopicCategory = "events" | "camera_activity" | "system" | "other"; + +const TOPIC_CATEGORY_COLORS: Record = { + events: "bg-blue-500/20 text-blue-700 dark:text-blue-300 border-blue-500/30", + camera_activity: + "bg-green-500/20 text-green-700 dark:text-green-300 border-green-500/30", + system: + "bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-500/30", + other: "bg-gray-500/20 text-gray-700 dark:text-gray-300 border-gray-500/30", +}; + +const EVENT_TYPE_COLORS: Record = { + start: + "bg-green-500/20 text-green-700 dark:text-green-300 border-green-500/30", + update: "bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 border-cyan-500/30", + end: "bg-red-500/20 text-red-700 dark:text-red-300 border-red-500/30", +}; + +const TRACKED_OBJECT_UPDATE_COLORS: Record = { + description: + "bg-amber-500/20 text-amber-700 dark:text-amber-300 border-amber-500/30", + face: "bg-pink-500/20 text-pink-700 dark:text-pink-300 border-pink-500/30", + lpr: "bg-yellow-500/20 text-yellow-700 dark:text-yellow-300 border-yellow-500/30", + classification: + "bg-violet-500/20 text-violet-700 dark:text-violet-300 border-violet-500/30", +}; + +function getEventTypeColor(eventType: string): string { + return ( + EVENT_TYPE_COLORS[eventType] || + "bg-orange-500/20 text-orange-700 dark:text-orange-300 border-orange-500/30" + ); +} + +function getTrackedObjectTypeColor(objectType: string): string { + return ( + TRACKED_OBJECT_UPDATE_COLORS[objectType] || + "bg-orange-500/20 text-orange-700 dark:text-orange-300 border-orange-500/30" + ); +} + +const EVENT_TOPICS = new Set([ + "events", + "reviews", + "tracked_object_update", + "triggers", +]); + +const SYSTEM_TOPICS = new Set([ + "stats", + "model_state", + "job_state", + "embeddings_reindex_progress", + "audio_transcription_state", + "birdseye_layout", +]); + +function getTopicCategory(topic: string): TopicCategory { + if (EVENT_TOPICS.has(topic)) return "events"; + if (SYSTEM_TOPICS.has(topic)) return "system"; + if ( + topic === "camera_activity" || + topic === "audio_detections" || + topic.includes("/motion") || + topic.includes("/audio") || + topic.includes("/detect") || + topic.includes("/recordings") || + topic.includes("/enabled") || + topic.includes("/snapshots") || + topic.includes("/ptz") + ) { + return "camera_activity"; + } + return "other"; +} + +function formatTimestamp(ts: number): string { + const d = new Date(ts); + const hh = String(d.getHours()).padStart(2, "0"); + const mm = String(d.getMinutes()).padStart(2, "0"); + const ss = String(d.getSeconds()).padStart(2, "0"); + const ms = String(d.getMilliseconds()).padStart(3, "0"); + return `${hh}:${mm}:${ss}.${ms}`; +} + +function getPayloadSummary( + topic: string, + payload: unknown, + hideType: boolean = false, +): string { + if (payload === null || payload === undefined) return ""; + + try { + const data = typeof payload === "string" ? JSON.parse(payload) : payload; + + if (typeof data === "object" && data !== null) { + // Topic-specific summary handlers + if (topic === "tracked_object_update") { + return getTrackedObjectUpdateSummary(data); + } + + if ("type" in data && "label" in (data.after || data)) { + const after = data.after || data; + const parts: string[] = []; + + if (!hideType) { + parts.push(`type: ${data.type}`); + } + parts.push(`label: ${after.label || "?"}`); + + // Add sub_label for events topic if present + if (topic === "events" && after.sub_label) { + parts.push(`sub_label: ${after.sub_label}`); + } + + return parts.join(", "); + } + if ("type" in data && "camera" in data) { + if (hideType) { + return `camera: ${data.camera}`; + } + return `type: ${data.type}, camera: ${data.camera}`; + } + const keys = Object.keys(data); + if (keys.length <= 3) { + return keys + .map((k) => { + const v = data[k]; + if (typeof v === "string" || typeof v === "number") { + return `${k}: ${v}`; + } + return k; + }) + .join(", "); + } + return `{${keys.length} keys}`; + } + + const str = String(data); + return str.length > 80 ? str.slice(0, 80) + "…" : str; + } catch { + const str = String(payload); + return str.length > 80 ? str.slice(0, 80) + "…" : str; + } +} + +function getTrackedObjectUpdateSummary(data: unknown): string { + if (typeof data !== "object" || data === null) return ""; + + const obj = data as Record; + const type = obj.type as string; + + switch (type) { + case "description": + return obj.description ? `${obj.description}` : "no description"; + + case "face": { + const name = obj.name as string | undefined; + return name || "unknown"; + } + + case "lpr": { + const name = obj.name as string | undefined; + const plate = obj.plate as string | undefined; + return name || plate || "unknown"; + } + + case "classification": { + const parts: string[] = []; + const model = obj.model as string | undefined; + const subLabel = obj.sub_label as string | undefined; + const attribute = obj.attribute as string | undefined; + + if (model) parts.push(`model: ${model}`); + if (subLabel) parts.push(`sub_label: ${subLabel}`); + if (attribute) parts.push(`attribute: ${attribute}`); + + return parts.length > 0 ? parts.join(", ") : "classification"; + } + + default: + return type || "unknown"; + } +} + +function extractTypeForBadge(payload: unknown): string | null { + if (payload === null || payload === undefined) return null; + + try { + const data = typeof payload === "string" ? JSON.parse(payload) : payload; + + if (typeof data === "object" && data !== null && "type" in data) { + return data.type as string; + } + } catch { + // ignore + } + return null; +} + +function shouldShowTypeBadge(type: string | null): boolean { + if (!type) return false; + return true; +} + +function shouldShowSummary(topic: string): boolean { + // Hide summary for reviews topic + return topic !== "reviews"; +} + +function escapeHtml(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +function highlightJson(value: unknown): string { + // Try to auto-parse JSON strings + if (typeof value === "string") { + try { + const parsed = JSON.parse(value); + if (typeof parsed === "object" && parsed !== null) { + value = parsed; + } + } catch { + // not JSON + } + } + + const raw = JSON.stringify(value, null, 2) ?? String(value); + + // Single regex pass to colorize JSON tokens + return raw.replace( + /("(?:[^"\\]|\\.)*")\s*:|("(?:[^"\\]|\\.)*")|(true|false|null)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, + (match, key: string, str: string, keyword: string, num: string) => { + if (key) { + return `${escapeHtml(key)}:`; + } + if (str) { + const content = escapeHtml(str); + return `${content}`; + } + if (keyword) { + return `${keyword}`; + } + if (num) { + return `${num}`; + } + return match; + }, + ); +} + +function CopyJsonButton({ payload }: { payload: unknown }) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + const text = + typeof payload === "string" + ? payload + : JSON.stringify(payload, null, 2); + navigator.clipboard.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, + [payload], + ); + + return ( + + ); +} + +type WsMessageRowProps = { + message: WsFeedMessage; + showCameraBadge?: boolean; +}; + +const WsMessageRow = memo(function WsMessageRow({ + message, + showCameraBadge = true, +}: WsMessageRowProps) { + const { t } = useTranslation(["views/system"]); + const [expanded, setExpanded] = useState(false); + const category = getTopicCategory(message.topic); + + const cameraName = extractCameraName(message); + + const messageType = extractTypeForBadge(message.payload); + const showTypeBadge = shouldShowTypeBadge(messageType); + + const summary = getPayloadSummary(message.topic, message.payload); + + const eventLabel = (() => { + try { + const data = + typeof message.payload === "string" + ? JSON.parse(message.payload) + : message.payload; + if (typeof data === "object" && data !== null) { + return (data.after?.label as string) || (data.label as string) || null; + } + } catch { + // ignore + } + return null; + })(); + + const parsedPayload = (() => { + try { + return typeof message.payload === "string" + ? JSON.parse(message.payload) + : message.payload; + } catch { + return message.payload; + } + })(); + + const handleToggle = useCallback(() => { + setExpanded((prev) => !prev); + }, []); + + // Determine which color function to use based on topic + const getTypeBadgeColor = (type: string | null) => { + if (!type) return ""; + if (message.topic === "tracked_object_update") { + return getTrackedObjectTypeColor(type); + } + return getEventTypeColor(type); + }; + + return ( +
+
+ + + + {formatTimestamp(message.timestamp)} + + + + {message.topic} + + + {showTypeBadge && messageType && ( + + {messageType} + + )} + + {showCameraBadge && cameraName && ( + + {cameraName} + + )} + + {eventLabel && ( + + {getIconForLabel( + eventLabel, + "object", + "size-3.5 text-primary-variant", + )} + + )} + + {shouldShowSummary(message.topic) && ( + + {summary} + + )} +
+ + {expanded && ( +
+
+ + {t("logs.websocket.expanded.payload")} + + +
+
+        
+ )} +
+ ); +}); + +export default WsMessageRow; diff --git a/web/src/hooks/use-allowed-cameras.ts b/web/src/hooks/use-allowed-cameras.ts index 05941922a..add52fed7 100644 --- a/web/src/hooks/use-allowed-cameras.ts +++ b/web/src/hooks/use-allowed-cameras.ts @@ -2,6 +2,7 @@ import { useContext } from "react"; import { AuthContext } from "@/context/auth-context"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; +import { isReplayCamera } from "@/utils/cameraUtil"; export function useAllowedCameras() { const { auth } = useContext(AuthContext); @@ -14,9 +15,11 @@ export function useAllowedCameras() { auth.user?.role === "admin" || !auth.isAuthenticated // anonymous internal port ) { - // return all cameras - return config?.cameras ? Object.keys(config.cameras) : []; + // return all cameras, excluding replay cameras + return config?.cameras + ? Object.keys(config.cameras).filter((name) => !isReplayCamera(name)) + : []; } - return auth.allowedCameras || []; + return (auth.allowedCameras || []).filter((name) => !isReplayCamera(name)); } diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index 328811a9d..cf5bf4653 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -26,7 +26,7 @@ type useCameraActivityReturn = { }; export function useCameraActivity( - camera: CameraConfig, + camera: CameraConfig | undefined, revalidateOnFocus: boolean = true, ): useCameraActivityReturn { const { data: config } = useSWR("config", { @@ -47,7 +47,7 @@ export function useCameraActivity( // init camera activity const { payload: updatedCameraState } = useInitialCameraState( - camera.name, + camera?.name ?? "", revalidateOnFocus, ); useEffect(() => { @@ -60,7 +60,7 @@ export function useCameraActivity( const memoizedAudioState = useDeepMemo(updatedAudioState); useEffect(() => { - if (memoizedAudioState) { + if (memoizedAudioState && camera?.name) { setAudioDetections(memoizedAudioState[camera.name]); } }, [memoizedAudioState, camera]); @@ -72,8 +72,8 @@ export function useCameraActivity( [objects], ); - const { payload: cameraEnabled } = useEnabledState(camera.name); - const { payload: detectingMotion } = useMotionActivity(camera.name); + const { payload: cameraEnabled } = useEnabledState(camera?.name ?? ""); + const { payload: detectingMotion } = useMotionActivity(camera?.name ?? ""); const { payload: event } = useFrigateEvents(); const updatedEvent = useDeepMemo(event); @@ -91,7 +91,7 @@ export function useCameraActivity( return; } - if (updatedEvent.after.camera !== camera.name) { + if (!camera?.name || updatedEvent.after.camera !== camera.name) { return; } @@ -158,6 +158,10 @@ export function useCameraActivity( return false; } + if (!camera?.name) { + return false; + } + return ( cameras[camera.name]?.camera_fps == 0 && stats["service"].uptime > 60 ); diff --git a/web/src/hooks/use-stats.ts b/web/src/hooks/use-stats.ts index 040dd7b1a..5bddb75ac 100644 --- a/web/src/hooks/use-stats.ts +++ b/web/src/hooks/use-stats.ts @@ -9,6 +9,7 @@ import { useMemo } from "react"; import useSWR from "swr"; import useDeepMemo from "./use-deep-memo"; import { capitalizeAll, capitalizeFirstLetter } from "@/utils/stringUtil"; +import { isReplayCamera } from "@/utils/cameraUtil"; import { useFrigateStats } from "@/api/ws"; import { useTranslation } from "react-i18next"; @@ -16,6 +17,9 @@ import { useTranslation } from "react-i18next"; export default function useStats(stats: FrigateStats | undefined) { const { t } = useTranslation(["views/system"]); const { data: config } = useSWR("config"); + const { data: debugReplayStatus } = useSWR("debug_replay/status", { + revalidateOnFocus: false, + }); const memoizedStats = useDeepMemo(stats); @@ -74,6 +78,11 @@ export default function useStats(stats: FrigateStats | undefined) { return; } + // Skip replay cameras + if (isReplayCamera(name)) { + return; + } + const cameraName = config.cameras?.[name]?.friendly_name ?? name; if (config.cameras[name].enabled && cam["camera_fps"] == 0) { problems.push({ @@ -96,7 +105,15 @@ export default function useStats(stats: FrigateStats | undefined) { ); const cameraName = config?.cameras?.[name]?.friendly_name ?? name; - if (!isNaN(ffmpegAvg) && ffmpegAvg >= CameraFfmpegThreshold.error) { + + // Skip ffmpeg warnings for replay cameras when debug replay is active + if ( + !isNaN(ffmpegAvg) && + ffmpegAvg >= CameraFfmpegThreshold.error && + !( + debugReplayStatus?.active && debugReplayStatus?.replay_camera === name + ) + ) { problems.push({ text: t("stats.ffmpegHighCpuUsage", { camera: capitalizeFirstLetter(capitalizeAll(cameraName)), @@ -119,8 +136,19 @@ export default function useStats(stats: FrigateStats | undefined) { } }); + // Add message if debug replay is active + if (debugReplayStatus?.active) { + problems.push({ + text: t("stats.debugReplayActive", { + defaultValue: "Debug replay session is active", + }), + color: "text-selected", + relevantLink: "/replay", + }); + } + return problems; - }, [config, memoizedStats, t]); + }, [config, memoizedStats, t, debugReplayStatus]); return { potentialProblems }; } diff --git a/web/src/hooks/use-ws-message-buffer.ts b/web/src/hooks/use-ws-message-buffer.ts new file mode 100644 index 000000000..6f7f06662 --- /dev/null +++ b/web/src/hooks/use-ws-message-buffer.ts @@ -0,0 +1,99 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useWsMessageSubscribe, WsFeedMessage } from "@/api/ws"; +import { extractCameraName } from "@/utils/wsUtil"; + +type UseWsMessageBufferReturn = { + messages: WsFeedMessage[]; + clear: () => void; +}; + +type MessageFilter = { + cameraFilter?: string | string[]; // "all", specific camera name, or array of camera names (undefined in array = all) +}; + +export function useWsMessageBuffer( + maxSize: number = 2000, + paused: boolean = false, + filter?: MessageFilter, +): UseWsMessageBufferReturn { + const bufferRef = useRef([]); + const [version, setVersion] = useState(0); + const pausedRef = useRef(paused); + const filterRef = useRef(filter); + + pausedRef.current = paused; + filterRef.current = filter; + + const batchTimerRef = useRef | null>(null); + const dirtyRef = useRef(false); + + useEffect(() => { + batchTimerRef.current = setInterval(() => { + if (dirtyRef.current) { + dirtyRef.current = false; + setVersion((v) => v + 1); + } + }, 200); + + return () => { + if (batchTimerRef.current) { + clearInterval(batchTimerRef.current); + } + }; + }, []); + + const shouldIncludeMessage = useCallback((msg: WsFeedMessage): boolean => { + const currentFilter = filterRef.current; + if (!currentFilter) return true; + + // Check camera filter + const cf = currentFilter.cameraFilter; + if (cf !== undefined) { + if (Array.isArray(cf)) { + // Array of cameras: include messages matching any camera in the list + const msgCamera = extractCameraName(msg); + if (msgCamera && !cf.includes(msgCamera)) { + return false; + } + } else if (cf !== "all") { + // Single string camera filter + const msgCamera = extractCameraName(msg); + if (msgCamera !== cf) { + return false; + } + } + } + + return true; + }, []); + + useWsMessageSubscribe( + useCallback( + (msg: WsFeedMessage) => { + if (pausedRef.current) return; + if (!shouldIncludeMessage(msg)) return; + + const buf = bufferRef.current; + buf.push(msg); + if (buf.length > maxSize) { + buf.splice(0, buf.length - maxSize); + } + dirtyRef.current = true; + }, + [shouldIncludeMessage, maxSize], + ), + ); + + const clear = useCallback(() => { + bufferRef.current = []; + setVersion((v) => v + 1); + }, []); + + // version is used to trigger re-renders; we spread the buffer + // into a new array so that downstream useMemo dependencies + // see a new reference and recompute. + // eslint-disable-next-line react-hooks/exhaustive-deps + const messages = useMemo(() => [...bufferRef.current], [version]); + + return { messages, clear }; +} diff --git a/web/src/pages/Exports.tsx b/web/src/pages/Exports.tsx index 5b05439c6..a1b5953c6 100644 --- a/web/src/pages/Exports.tsx +++ b/web/src/pages/Exports.tsx @@ -642,4 +642,4 @@ function CaseAssignmentDialog({ ); } -export default Exports; \ No newline at end of file +export default Exports; diff --git a/web/src/pages/Logs.tsx b/web/src/pages/Logs.tsx index 03b52acc9..b01b4c712 100644 --- a/web/src/pages/Logs.tsx +++ b/web/src/pages/Logs.tsx @@ -35,10 +35,12 @@ import { isIOS, isMobile } from "react-device-detect"; import { isPWA } from "@/utils/isPWA"; import { isInIframe } from "@/utils/isIFrame"; import { useTranslation } from "react-i18next"; +import WsMessageFeed from "@/components/ws/WsMessageFeed"; function Logs() { const { t } = useTranslation(["views/system"]); const [logService, setLogService] = useState("frigate"); + const isWebsocket = logService === "websocket"; const tabsRef = useRef(null); const lazyLogWrapperRef = useRef(null); const [logs, setLogs] = useState([]); @@ -216,6 +218,12 @@ function Logs() { }, [logService, filterSeverity, t]); useEffect(() => { + if (isWebsocket) { + setIsLoading(false); + setLogs([]); + return; + } + setIsLoading(true); setLogs([]); lastFetchedIndexRef.current = -1; @@ -494,116 +502,128 @@ function Logs() { data-nav-item={item} aria-label={`Select ${item}`} > -
{item}
+
+ {item === "websocket" ? t("logs.websocket.label") : item} +
))}
-
- - - -
-
- -
-
-
-
-
- {t("logs.type.label")} + {!isWebsocket && ( +
+ + + +
+ )} +
+ + {isWebsocket ? ( +
+ +
+ ) : ( +
+
+
+
+
+ {t("logs.type.label")} +
+
{t("logs.type.timestamp")}
+
+
+
+ {t("logs.type.tag")} +
+
+
{t("logs.type.message")}
-
- {t("logs.type.tag")} -
-
-
{t("logs.type.message")}
-
-
-
- {isLoading ? ( - - ) : ( - ( - <> - {follow && !logSettings.disableStreaming && ( -
- - - - - {t("logs.tips")} - -
- )} - - } - loading={isLoading} - /> - - )} - /> - )} +
+ {isLoading ? ( + + ) : ( + ( + <> + {follow && !logSettings.disableStreaming && ( +
+ + + + + {t("logs.tips")} + +
+ )} + + } + loading={isLoading} + /> + + )} + /> + )} +
-
+ )}
); } diff --git a/web/src/pages/Replay.tsx b/web/src/pages/Replay.tsx new file mode 100644 index 000000000..187a9a76b --- /dev/null +++ b/web/src/pages/Replay.tsx @@ -0,0 +1,725 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { Trans, useTranslation } from "react-i18next"; +import useSWR from "swr"; +import axios from "axios"; +import { toast } from "sonner"; +import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { useCameraActivity } from "@/hooks/use-camera-activity"; +import { cn } from "@/lib/utils"; +import Heading from "@/components/ui/heading"; +import { Toaster } from "@/components/ui/sonner"; +import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; +import { getIconForLabel } from "@/utils/iconUtil"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { ObjectType } from "@/types/ws"; +import WsMessageFeed from "@/components/ws/WsMessageFeed"; +import { ConfigSectionTemplate } from "@/components/config-form/sections/ConfigSectionTemplate"; + +import { LuExternalLink, LuInfo, LuSettings } from "react-icons/lu"; +import { LuSquare } from "react-icons/lu"; +import { MdReplay } from "react-icons/md"; +import { isDesktop, isMobile } from "react-device-detect"; +import Logo from "@/components/Logo"; +import { Separator } from "@/components/ui/separator"; +import { useDocDomain } from "@/hooks/use-doc-domain"; +import DebugDrawingLayer from "@/components/overlay/DebugDrawingLayer"; +import { IoMdArrowRoundBack } from "react-icons/io"; + +type DebugReplayStatus = { + active: boolean; + replay_camera: string | null; + source_camera: string | null; + start_time: number | null; + end_time: number | null; + live_ready: boolean; +}; + +type DebugOptions = { + bbox: boolean; + timestamp: boolean; + zones: boolean; + mask: boolean; + motion: boolean; + regions: boolean; + paths: boolean; +}; + +const DEFAULT_OPTIONS: DebugOptions = { + bbox: true, + timestamp: false, + zones: false, + mask: false, + motion: true, + regions: false, + paths: false, +}; + +const DEBUG_OPTION_KEYS: (keyof DebugOptions)[] = [ + "bbox", + "timestamp", + "zones", + "mask", + "motion", + "regions", + "paths", +]; + +const DEBUG_OPTION_I18N_KEY: Record = { + bbox: "boundingBoxes", + timestamp: "timestamp", + zones: "zones", + mask: "mask", + motion: "motion", + regions: "regions", + paths: "paths", +}; + +const REPLAY_INIT_SKELETON_TIMEOUT_MS = 8000; + +export default function Replay() { + const { t } = useTranslation(["views/replay", "views/settings", "common"]); + const navigate = useNavigate(); + const { getLocaleDocUrl } = useDocDomain(); + + const { + data: status, + mutate: refreshStatus, + isLoading, + } = useSWR("debug_replay/status", { + refreshInterval: 1000, + }); + const [isInitializing, setIsInitializing] = useState(true); + + // Refresh status immediately on mount to avoid showing "no session" briefly + useEffect(() => { + const initializeStatus = async () => { + await refreshStatus(); + setIsInitializing(false); + }; + initializeStatus(); + }, [refreshStatus]); + + useEffect(() => { + if (status?.live_ready) { + setShowReplayInitSkeleton(false); + } + }, [status?.live_ready]); + + const [options, setOptions] = useState(DEFAULT_OPTIONS); + const [isStopping, setIsStopping] = useState(false); + const [configDialogOpen, setConfigDialogOpen] = useState(false); + + const searchParams = useMemo(() => { + const params = new URLSearchParams(); + for (const key of DEBUG_OPTION_KEYS) { + params.set(key, options[key] ? "1" : "0"); + } + return params; + }, [options]); + + const handleSetOption = useCallback( + (key: keyof DebugOptions, value: boolean) => { + setOptions((prev) => ({ ...prev, [key]: value })); + }, + [], + ); + + const handleStop = useCallback(() => { + setIsStopping(true); + axios + .post("debug_replay/stop") + .then(() => { + toast.success(t("dialog.toast.stopped"), { + position: "top-center", + }); + refreshStatus(); + navigate("/review"); + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("dialog.toast.stopError", { error: errorMessage }), { + position: "top-center", + }); + }) + .finally(() => { + setIsStopping(false); + }); + }, [navigate, refreshStatus, t]); + + // Camera activity for the replay camera + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const replayCameraName = status?.replay_camera ?? ""; + const replayCameraConfig = replayCameraName + ? config?.cameras?.[replayCameraName] + : undefined; + + const { objects } = useCameraActivity(replayCameraConfig); + + const [showReplayInitSkeleton, setShowReplayInitSkeleton] = useState(false); + + // debug draw + const containerRef = useRef(null); + const [debugDraw, setDebugDraw] = useState(false); + + useEffect(() => { + if (!status?.active || !status.replay_camera) { + setShowReplayInitSkeleton(false); + return; + } + + setShowReplayInitSkeleton(true); + + const timeout = window.setTimeout(() => { + setShowReplayInitSkeleton(false); + }, REPLAY_INIT_SKELETON_TIMEOUT_MS); + + return () => { + window.clearTimeout(timeout); + }; + }, [status?.active, status?.replay_camera]); + + useEffect(() => { + if (status?.live_ready) { + setShowReplayInitSkeleton(false); + } + }, [status?.live_ready]); + + // Format time range for display + const timeRangeDisplay = useMemo(() => { + if (!status?.start_time || !status?.end_time) return ""; + const start = new Date(status.start_time * 1000).toLocaleString(); + const end = new Date(status.end_time * 1000).toLocaleString(); + return `${start} — ${end}`; + }, [status]); + + // Show loading state + if (isInitializing || (isLoading && !status?.active)) { + return ( +
+ +
+ ); + } + + // No active session + if (!status?.active) { + return ( +
+ + + {t("page.noSession")} + +

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

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

{t("description")}

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

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

+
    + + debug.boundingBoxes.colors.info + +
+ + ) : ( + + {`debug.${i18nKey}.tips`} + + )} +
+
+ )} +
+
+ {t(`debug.${i18nKey}.desc`, { + ns: "views/settings", + })} +
+
+ + handleSetOption(key, checked) + } + /> +
+ ); + })} + {isDesktop && ( + <> + +
+
+
+ + + + +
+ + + {t("button.info", { ns: "common" })} + +
+
+ + {t("debug.objectShapeFilterDrawing.tips", { + ns: "views/settings", + })} +
+ + {t("readTheDocumentation", { + ns: "common", + })} + + +
+
+
+
+
+ {t("debug.objectShapeFilterDrawing.desc", { + ns: "views/settings", + })} +
+
+ { + setDebugDraw(isChecked); + }} + /> +
+ + )} +
+
+
+ + + + +
+ +
+
+
+
+
+ + + + + {t("page.configuration")} + + {t("page.configurationDesc")} + + +
+ + +
+
+
+
+ ); +} + +type ObjectListProps = { + cameraConfig?: CameraConfig; + objects?: ObjectType[]; + config?: FrigateConfig; +}; + +function ObjectList({ cameraConfig, objects, config }: ObjectListProps) { + const { t } = useTranslation(["views/settings"]); + + const colormap = useMemo(() => { + if (!config) { + return; + } + return config.model?.colormap; + }, [config]); + + const getColorForObjectName = useCallback( + (objectName: string) => { + return colormap && colormap[objectName] + ? `rgb(${colormap[objectName][2]}, ${colormap[objectName][1]}, ${colormap[objectName][0]})` + : "rgb(128, 128, 128)"; + }, + [colormap], + ); + + if (!objects || objects.length === 0) { + return ( +
+ {t("debug.noObjects", { ns: "views/settings" })} +
+ ); + } + + return ( +
+ {objects.map((obj: ObjectType) => { + return ( +
+
+
+ {getIconForLabel(obj.label, "object", "size-4 text-white")} +
+
+ {getTranslatedLabel(obj.label)} +
+
+
+
+ + {t("debug.objectShapeFilterDrawing.score", { + ns: "views/settings", + })} + : + + + {obj.score ? (obj.score * 100).toFixed(1) : "-"}% + +
+ {obj.ratio && ( +
+ + {t("debug.objectShapeFilterDrawing.ratio", { + ns: "views/settings", + })} + : + + {obj.ratio.toFixed(2)} +
+ )} + {obj.area && cameraConfig && ( +
+ + {t("debug.objectShapeFilterDrawing.area", { + ns: "views/settings", + })} + : + + + {obj.area} px ( + {( + (obj.area / + (cameraConfig.detect.width * + cameraConfig.detect.height)) * + 100 + ).toFixed(2)} + %) + +
+ )} +
+
+ ); + })} +
+ ); +} diff --git a/web/src/types/log.ts b/web/src/types/log.ts index 2e856f574..bc0555e16 100644 --- a/web/src/types/log.ts +++ b/web/src/types/log.ts @@ -12,7 +12,7 @@ export type LogLine = { content: string; }; -export const logTypes = ["frigate", "go2rtc", "nginx"] as const; +export const logTypes = ["frigate", "go2rtc", "nginx", "websocket"] as const; export type LogType = (typeof logTypes)[number]; export type LogSettingsType = { diff --git a/web/src/utils/cameraUtil.ts b/web/src/utils/cameraUtil.ts index 543605ad0..c47c46f8f 100644 --- a/web/src/utils/cameraUtil.ts +++ b/web/src/utils/cameraUtil.ts @@ -148,3 +148,15 @@ export function detectCameraAudioFeatures( audioOutput: !!audioOutput, }; } + +const REPLAY_CAMERA_PREFIX = "_replay_"; + +/** + * Check if a camera name is a debug replay camera. + * + * @param name - The camera name to check + * @returns true if the camera is a replay camera + */ +export function isReplayCamera(name: string): boolean { + return name.startsWith(REPLAY_CAMERA_PREFIX); +} diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index c4c5afad1..9d2327cb3 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -514,13 +514,18 @@ const mergeSectionConfig = ( export function getSectionConfig( sectionKey: string, - level: "global" | "camera", + level: "global" | "camera" | "replay", ): SectionConfig { const entry = sectionConfigs[sectionKey]; if (!entry) { return {}; } - const overrides = level === "global" ? entry.global : entry.camera; + const overrides = + level === "global" + ? entry.global + : level === "replay" + ? entry.replay + : entry.camera; return mergeSectionConfig(entry.base, overrides); } diff --git a/web/src/utils/wsUtil.ts b/web/src/utils/wsUtil.ts new file mode 100644 index 000000000..411ff1645 --- /dev/null +++ b/web/src/utils/wsUtil.ts @@ -0,0 +1,53 @@ +import { WsFeedMessage } from "@/api/ws"; + +const EVENT_TOPICS = new Set([ + "events", + "reviews", + "tracked_object_update", + "triggers", +]); + +const SYSTEM_TOPICS = new Set([ + "stats", + "model_state", + "job_state", + "embeddings_reindex_progress", + "audio_transcription_state", + "birdseye_layout", +]); + +export function extractCameraName(message: WsFeedMessage): string | null { + // Try extracting from topic pattern: {camera}/motion, {camera}/audio/rms, etc. + const topicParts = message.topic.split("/"); + if ( + topicParts.length >= 2 && + !EVENT_TOPICS.has(message.topic) && + !SYSTEM_TOPICS.has(message.topic) && + message.topic !== "camera_activity" && + message.topic !== "audio_detections" && + message.topic !== "restart" && + message.topic !== "notification_test" + ) { + return topicParts[0]; + } + + // Try extracting from payload + try { + const data = + typeof message.payload === "string" + ? JSON.parse(message.payload) + : message.payload; + + if (typeof data === "object" && data !== null) { + if ("camera" in data) return data.camera as string; + if ("after" in data && data.after?.camera) + return data.after.camera as string; + if ("before" in data && data.before?.camera) + return data.before.camera as string; + } + } catch { + // ignore parse errors + } + + return null; +} diff --git a/web/src/views/recording/RecordingView.tsx b/web/src/views/recording/RecordingView.tsx index 75463b1fd..5758728dc 100644 --- a/web/src/views/recording/RecordingView.tsx +++ b/web/src/views/recording/RecordingView.tsx @@ -1,6 +1,8 @@ import ReviewCard from "@/components/card/ReviewCard"; import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup"; +import DebugReplayDialog from "@/components/overlay/DebugReplayDialog"; import ExportDialog from "@/components/overlay/ExportDialog"; +import ActionsDropdown from "@/components/overlay/ActionsDropdown"; import PreviewPlayer, { PreviewController, } from "@/components/player/PreviewPlayer"; @@ -199,6 +201,11 @@ export function RecordingView({ const [exportRange, setExportRange] = useState(); const [showExportPreview, setShowExportPreview] = useState(false); + // debug replay + + const [debugReplayMode, setDebugReplayMode] = useState("none"); + const [debugReplayRange, setDebugReplayRange] = useState(); + // move to next clip const onClipEnded = useCallback(() => { @@ -269,7 +276,7 @@ export function RecordingView({ ); useEffect(() => { - if (scrubbing || exportRange) { + if (scrubbing || exportRange || debugReplayRange) { if ( currentTime > currentTimeRange.before + 60 || currentTime < currentTimeRange.after - 60 @@ -591,6 +598,23 @@ export function RecordingView({ selected={mainCamera} onSelectCamera={onSelectCamera} /> + {isDesktop && ( + { + setDebugReplayRange(range); + + if (range != undefined) { + mainControllerRef.current?.pause(); + } + }} + setMode={setDebugReplayMode} + /> + )} {isDesktop && ( {}} /> )} + {isDesktop && ( + { + const now = new Date(timeRange.before * 1000); + now.setHours(now.getHours() - 1); + setDebugReplayRange({ + after: now.getTime() / 1000, + before: timeRange.before, + }); + setDebugReplayMode("select"); + }} + onExportClick={() => { + const now = new Date(timeRange.before * 1000); + now.setHours(now.getHours() - 1); + setExportRange({ + before: timeRange.before, + after: now.getTime() / 1000, + }); + setExportMode("select"); + }} + /> + )} {isDesktop ? ( { + setDebugReplayRange(range); + + if (range != undefined) { + mainControllerRef.current?.pause(); + } + }} onUpdateFilter={updateFilter} setRange={setExportRange} setMode={setExportMode} @@ -758,7 +814,9 @@ export function RecordingView({ timeRange={currentTimeRange} cameraPreviews={allPreviews ?? []} startTimestamp={playbackStart} - hotKeys={exportMode != "select"} + hotKeys={ + exportMode != "select" && debugReplayMode != "select" + } fullscreen={fullscreen} onTimestampUpdate={(timestamp) => { setPlayerTime(timestamp); @@ -772,7 +830,11 @@ export function RecordingView({ onControllerReady={(controller) => { mainControllerRef.current = controller; }} - isScrubbing={scrubbing || exportMode == "timeline"} + isScrubbing={ + scrubbing || + exportMode == "timeline" || + debugReplayMode == "timeline" + } supportsFullscreen={supportsFullScreen} setFullResolution={setFullResolution} toggleFullscreen={toggleFullscreen} @@ -840,18 +902,29 @@ export function RecordingView({ contentRef={contentRef} mainCamera={mainCamera} timelineType={ - (exportRange == undefined ? timelineType : "timeline") ?? - "timeline" + (exportRange == undefined && debugReplayRange == undefined + ? timelineType + : "timeline") ?? "timeline" } timeRange={timeRange} mainCameraReviewItems={mainCameraReviewItems} activeReviewItem={activeReviewItem} currentTime={currentTime} - exportRange={exportMode == "timeline" ? exportRange : undefined} + exportRange={ + exportMode == "timeline" + ? exportRange + : debugReplayMode == "timeline" + ? debugReplayRange + : undefined + } setCurrentTime={setCurrentTime} manuallySetCurrentTime={manuallySetCurrentTime} setScrubbing={setScrubbing} - setExportRange={setExportRange} + setExportRange={ + debugReplayMode == "timeline" + ? setDebugReplayRange + : setExportRange + } onAnalysisOpen={onAnalysisOpen} isPlaying={mainControllerRef?.current?.isPlaying() ?? false} /> From b2118382cb3a7cabc1947c738289458c79bf7da7 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 4 Mar 2026 15:53:20 -0700 Subject: [PATCH 5/9] Various Fixes (#22263) --- frigate/camera/activity_manager.py | 1 + frigate/genai/ollama.py | 39 ++++++++++++++---------------- 2 files changed, 19 insertions(+), 21 deletions(-) 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/genai/ollama.py b/frigate/genai/ollama.py index 73ba2171e..eb63f7fdb 100644 --- a/frigate/genai/ollama.py +++ b/frigate/genai/ollama.py @@ -222,27 +222,24 @@ 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 + 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 if final_message is not None: yield ("message", final_message) From 2782931c7239bd03a9f34be0362a5df221a4698d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 5 Mar 2026 08:42:38 -0600 Subject: [PATCH 6/9] Update frontend to React 19 (#22275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove unused RecoilRoot and fix implicit ref callback Remove the vestigial recoil dependency (zero consumers) and convert the implicit-return ref callback in SearchView to block form to prevent React 19 interpreting it as a cleanup function. * replace react-transition-group with framer-motion in Chip Replace CSSTransition with framer-motion AnimatePresence + motion.div for React 19 compatibility (react-transition-group uses findDOMNode). framer-motion is already a project dependency. * migrate react-grid-layout v1 to v2 - Replace WidthProvider(Responsive) HOC with useContainerWidth hook - Update types: Layout (single item) → LayoutItem, Layout[] → Layout - Replace isDraggable/isResizable/resizeHandles with dragConfig/resizeConfig - Update EventCallback signature for v2 API - Remove @types/react-grid-layout (v2 includes its own types) * upgrade vaul, next-themes, framer-motion, react-zoom-pan-pinch - vaul: ^0.9.1 → ^1.1.2 - next-themes: ^0.3.0 → ^0.4.6 - framer-motion: ^11.5.4 → ^12.35.0 (React 19 native support) - react-zoom-pan-pinch: 3.4.4 → latest * upgrade to React 19, react-konva v19, eslint-plugin-react-hooks v5 Core React 19 upgrade with all necessary type fixes: - Update RefObject types to accept T | null (React 19 refs always nullable) - Add JSX namespace imports (no longer global in React 19) - Add initial values to useRef calls (required in React 19) - Fix ReactElement.props unknown type in config-form components - Fix IconWrapper interface to use HTMLAttributes instead of index signature - Add monaco-editor as dev dependency for type declarations - Upgrade react-konva to v19, eslint-plugin-react-hooks to v5 * upgrade typescript to 5.9.3 * modernize Context.Provider to React 19 shorthand Replace with across all project-owned context providers. External library contexts (react-icons IconContext, radix TooltipPrimitive) left unchanged. * add runtime patches for React 19 compatibility - Patch @radix-ui/react-compose-refs@1.1.2: stabilize useComposedRefs to prevent infinite render loops from unstable ref callbacks https://github.com/radix-ui/primitives/issues/3799 - Patch @radix-ui/react-slot@1.2.4: use useComposedRefs hook in SlotClone instead of inline composeRefs to prevent re-render cycles https://github.com/radix-ui/primitives/pull/3804 - Patch react-use-websocket@4.8.1: remove flushSync wrappers that cause "Maximum update depth exceeded" with React 19 auto-batching https://github.com/facebook/react/issues/27613 - Add npm overrides to ensure single hoisted copies of compose-refs and react-slot across all Radix packages - Add postinstall script for patch-package - Remove leftover react-transition-group dependency * formatting * use availableWidth instead of useContainerWidth for grid layout The useContainerWidth hook from react-grid-layout v2 returns raw container width without accounting for scrollbar width, causing the grid to not fill the full available space. Use the existing availableWidth value from useResizeObserver which already compensates for scrollbar width, matching the working implementation. * remove unused carousel component and fix React 19 peer deps Remove embla-carousel-react and its unused Carousel UI component. Upgrade sonner v1 → v2 for native React 19 support. Remove @types/react-icons stub (react-icons bundles its own types). These changes eliminate all peer dependency conflicts, so npm install works without --legacy-peer-deps. * fix React 19 infinite re-render loop on live dashboard The "Maximum update depth exceeded" error was caused by two issues: 1. useDeferredStreamMetadata returned a new `{}` default on every render when SWR data was undefined, creating an unstable reference that triggered the useEffect in useCameraLiveMode on every render cycle. Fixed by using a stable module-level EMPTY_METADATA constant. 2. useResizeObserver's rest parameter `...refs` created a new array on every render, causing its useEffect to re-run and re-observe elements continuously. Fixed by stabilizing refs with useRef and only reconnecting the observer when actual DOM elements change. --- web/package-lock.json | 4529 ++++++++--------- web/package.json | 46 +- .../@radix-ui+react-compose-refs+1.1.2.patch | 75 + web/patches/@radix-ui+react-slot+1.2.4.patch | 46 + web/patches/react-use-websocket+4.8.1.patch | 23 + .../theme/fields/LayoutGridField.tsx | 24 +- .../theme/templates/FieldTemplate.tsx | 7 +- .../templates/MultiSchemaFieldTemplate.tsx | 1 + .../theme/templates/ObjectFieldTemplate.tsx | 24 +- web/src/components/dynamic/TimeAgo.tsx | 1 + web/src/components/indicators/Chip.tsx | 58 +- web/src/components/mobile/MobilePage.tsx | 6 +- .../components/overlay/DebugDrawingLayer.tsx | 2 +- .../components/overlay/detail/ObjectPath.tsx | 2 +- .../overlay/dialog/PlatformAwareDialog.tsx | 1 + web/src/components/player/HlsVideoPlayer.tsx | 2 +- web/src/components/player/WebRTCPlayer.tsx | 4 +- web/src/components/settings/PolygonCanvas.tsx | 2 +- web/src/components/settings/PolygonDrawer.tsx | 2 +- .../timeline/EventReviewTimeline.tsx | 4 +- web/src/components/timeline/EventSegment.tsx | 2 +- .../timeline/MotionReviewTimeline.tsx | 4 +- .../components/timeline/ReviewTimeline.tsx | 4 +- .../components/timeline/SummaryTimeline.tsx | 2 +- .../timeline/VirtualizedEventSegments.tsx | 4 +- .../timeline/VirtualizedMotionSegments.tsx | 4 +- web/src/components/ui/calendar-range.tsx | 5 +- web/src/components/ui/carousel.tsx | 265 - web/src/components/ui/form.tsx | 8 +- web/src/components/ui/icon-wrapper.tsx | 4 +- web/src/components/ui/sidebar.tsx | 4 +- web/src/components/ui/toggle-group.tsx | 4 +- web/src/context/auth-context.tsx | 6 +- web/src/context/detail-stream-context.tsx | 6 +- web/src/context/language-provider.tsx | 4 +- web/src/context/providers.tsx | 37 +- web/src/context/statusbar-provider.tsx | 4 +- .../context/streaming-settings-provider.tsx | 4 +- web/src/context/theme-provider.tsx | 4 +- web/src/hooks/resize-observer.ts | 27 +- web/src/hooks/use-deferred-stream-metadata.ts | 3 +- web/src/hooks/use-draggable-element.ts | 8 +- web/src/hooks/use-fullscreen.ts | 2 +- web/src/hooks/use-image-loaded.ts | 2 +- web/src/hooks/use-timeline-utils.ts | 2 +- web/src/hooks/use-timeline-zoom.ts | 2 +- web/src/hooks/use-user-interaction.ts | 4 +- web/src/hooks/use-video-dimensions.ts | 2 +- web/src/views/live/DraggableGridLayout.tsx | 49 +- web/src/views/search/SearchView.tsx | 6 +- 50 files changed, 2513 insertions(+), 2828 deletions(-) create mode 100644 web/patches/@radix-ui+react-compose-refs+1.1.2.patch create mode 100644 web/patches/@radix-ui+react-slot+1.2.4.patch create mode 100644 web/patches/react-use-websocket+4.8.1.patch delete mode 100644 web/src/components/ui/carousel.tsx diff --git a/web/package-lock.json b/web/package-lock.json index 9f7382839..a767b3eff 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", @@ -26,7 +27,7 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "1.2.4", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toggle": "^1.1.2", @@ -44,8 +45,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 +55,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 +84,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 +93,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 +105,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 +762,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 +794,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 +1290,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 +1385,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 +1454,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 +1483,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 +1654,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 +1794,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 +1894,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 +1909,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 +1924,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 +1942,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 +1989,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 +2008,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 +2039,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 +2065,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 +2154,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 +2178,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 +2247,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 +2344,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 +2429,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 +2456,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 +2505,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 +2922,22 @@ } } }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-radio-group": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz", - "integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2407,6 +2954,246 @@ } } }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", @@ -2470,30 +3257,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 +3299,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 +3337,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 +3423,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 +3510,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 +3676,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 +3691,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 +3802,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 +3855,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 +3983,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 +3998,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 +4016,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 +4063,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 +4082,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 +4097,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 +4164,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 +4181,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 +4227,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 +4263,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 +4309,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 +4388,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 +5034,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 +5052,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 +5091,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 +5513,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 +5942,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 +5974,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 +6160,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 +6238,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 +6381,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 +6561,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 +6677,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 +6705,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 +6988,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 +7285,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 +7383,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 +7409,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 +7595,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 +7617,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 +7626,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 +8133,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 +8201,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 +8359,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 +8390,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 +8442,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 +8624,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 +9581,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 +9617,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 +9693,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 +9817,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 +9947,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 +10117,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 +10666,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 +10731,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 +10757,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 +10775,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 +10847,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 +10864,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 +10905,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 +10967,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 +11066,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 +11076,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 +11108,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 +11483,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 +11510,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 +11579,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 +12169,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12444,6 +12219,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 +12343,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 +12632,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 +12833,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..7762a7a56 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 .", @@ -32,7 +33,7 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "1.2.4", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toggle": "^1.1.2", @@ -50,8 +51,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 +61,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 +90,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 +99,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 +111,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/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..bef53519e 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -91,7 +91,7 @@ export default function HlsVideoPlayer({ // playback - const hlsRef = useRef(); + const hlsRef = useRef(undefined); const [useHlsCompat, setUseHlsCompat] = useState(false); const [loadedMetadata, setLoadedMetadata] = useState(false); const [bufferTimeout, setBufferTimeout] = useState(); diff --git a/web/src/components/player/WebRTCPlayer.tsx b/web/src/components/player/WebRTCPlayer.tsx index 37770acc0..147af43ea 100644 --- a/web/src/components/player/WebRTCPlayer.tsx +++ b/web/src/components/player/WebRTCPlayer.tsx @@ -51,10 +51,10 @@ export default function WebRtcPlayer({ // camera states - const pcRef = useRef(); + const pcRef = useRef(undefined); const videoRef = useRef(null); const [bufferTimeout, setBufferTimeout] = useState(); - const videoLoadTimeoutRef = useRef(); + const videoLoadTimeoutRef = useRef(undefined); const PeerConnection = useCallback( async (media: string) => { diff --git a/web/src/components/settings/PolygonCanvas.tsx b/web/src/components/settings/PolygonCanvas.tsx index 0db8db258..b2241ab84 100644 --- a/web/src/components/settings/PolygonCanvas.tsx +++ b/web/src/components/settings/PolygonCanvas.tsx @@ -10,7 +10,7 @@ import { snapPointToLines } from "@/utils/canvasUtil"; import { usePolygonStates } from "@/hooks/use-polygon-states"; type PolygonCanvasProps = { - containerRef: RefObject; + containerRef: RefObject; camera: string; width: number; height: number; 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..c5531b14a 100644 --- a/web/src/components/timeline/MotionReviewTimeline.tsx +++ b/web/src/components/timeline/MotionReviewTimeline.tsx @@ -41,8 +41,8 @@ export type MotionReviewTimelineProps = { events: ReviewSegment[]; motion_events: MotionData[]; noRecordingRanges?: RecordingSegment[]; - contentRef: RefObject; - timelineRef?: RefObject; + contentRef: RefObject; + timelineRef?: RefObject; onHandlebarDraggingChange?: (isDragging: boolean) => void; dense?: boolean; isZooming: boolean; diff --git a/web/src/components/timeline/ReviewTimeline.tsx b/web/src/components/timeline/ReviewTimeline.tsx index 8a3b6d2da..b96758493 100644 --- a/web/src/components/timeline/ReviewTimeline.tsx +++ b/web/src/components/timeline/ReviewTimeline.tsx @@ -20,8 +20,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip"; export type ReviewTimelineProps = { - timelineRef: RefObject; - contentRef: RefObject; + timelineRef: RefObject; + contentRef: RefObject; segmentDuration: number; timelineDuration: number; timelineStartAligned: number; diff --git a/web/src/components/timeline/SummaryTimeline.tsx b/web/src/components/timeline/SummaryTimeline.tsx index d17709bdf..91d3104de 100644 --- a/web/src/components/timeline/SummaryTimeline.tsx +++ b/web/src/components/timeline/SummaryTimeline.tsx @@ -14,7 +14,7 @@ import { import { useTimelineUtils } from "@/hooks/use-timeline-utils"; export type SummaryTimelineProps = { - reviewTimelineRef: React.RefObject; + reviewTimelineRef: React.RefObject; timelineStart: number; timelineEnd: number; segmentDuration: number; diff --git a/web/src/components/timeline/VirtualizedEventSegments.tsx b/web/src/components/timeline/VirtualizedEventSegments.tsx index 5b8b73633..23ad49ffc 100644 --- a/web/src/components/timeline/VirtualizedEventSegments.tsx +++ b/web/src/components/timeline/VirtualizedEventSegments.tsx @@ -10,7 +10,7 @@ import { EventSegment } from "./EventSegment"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; type VirtualizedEventSegmentsProps = { - timelineRef: React.RefObject; + timelineRef: React.RefObject; segments: number[]; events: ReviewSegment[]; segmentDuration: number; @@ -19,7 +19,7 @@ type VirtualizedEventSegmentsProps = { minimapStartTime?: number; minimapEndTime?: number; severityType: ReviewSeverity; - contentRef: React.RefObject; + contentRef: React.RefObject; setHandlebarTime?: React.Dispatch>; dense: boolean; alignStartDateToTimeline: (timestamp: number) => number; diff --git a/web/src/components/timeline/VirtualizedMotionSegments.tsx b/web/src/components/timeline/VirtualizedMotionSegments.tsx index f868e158f..240192911 100644 --- a/web/src/components/timeline/VirtualizedMotionSegments.tsx +++ b/web/src/components/timeline/VirtualizedMotionSegments.tsx @@ -10,7 +10,7 @@ import MotionSegment from "./MotionSegment"; import { ReviewSegment, MotionData } from "@/types/review"; type VirtualizedMotionSegmentsProps = { - timelineRef: React.RefObject; + timelineRef: React.RefObject; segments: number[]; events: ReviewSegment[]; motion_events: MotionData[]; @@ -19,7 +19,7 @@ type VirtualizedMotionSegmentsProps = { showMinimap: boolean; minimapStartTime?: number; minimapEndTime?: number; - contentRef: React.RefObject; + contentRef: React.RefObject; setHandlebarTime?: React.Dispatch>; dense: boolean; motionOnly: boolean; diff --git a/web/src/components/ui/calendar-range.tsx b/web/src/components/ui/calendar-range.tsx index 09641926a..f8ed6c886 100644 --- a/web/src/components/ui/calendar-range.tsx +++ b/web/src/components/ui/calendar-range.tsx @@ -1,3 +1,4 @@ +import type { JSX } from "react"; import { useState, useEffect, useRef } from "react"; import { Button } from "./button"; import { Calendar } from "./calendar"; @@ -124,8 +125,8 @@ export function DateRangePicker({ ); // Refs to store the values of range and rangeCompare when the date picker is opened - const openedRangeRef = useRef(); - const openedRangeCompareRef = useRef(); + const openedRangeRef = useRef(undefined); + const openedRangeCompareRef = useRef(undefined); const [selectedPreset, setSelectedPreset] = useState( undefined, diff --git a/web/src/components/ui/carousel.tsx b/web/src/components/ui/carousel.tsx deleted file mode 100644 index 98b7d6cbd..000000000 --- a/web/src/components/ui/carousel.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import * as React from "react"; -import useEmblaCarousel, { - type UseEmblaCarouselType, -} from "embla-carousel-react"; -import { ArrowLeft, ArrowRight } from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { useTranslation } from "react-i18next"; - -type CarouselApi = UseEmblaCarouselType[1]; -type UseCarouselParameters = Parameters; -type CarouselOptions = UseCarouselParameters[0]; -type CarouselPlugin = UseCarouselParameters[1]; - -type CarouselProps = { - opts?: CarouselOptions; - plugins?: CarouselPlugin; - orientation?: "horizontal" | "vertical"; - setApi?: (api: CarouselApi) => void; -}; - -type CarouselContextProps = { - carouselRef: ReturnType[0]; - api: ReturnType[1]; - scrollPrev: () => void; - scrollNext: () => void; - canScrollPrev: boolean; - canScrollNext: boolean; -} & CarouselProps; - -const CarouselContext = React.createContext(null); - -function useCarousel() { - const context = React.useContext(CarouselContext); - - if (!context) { - throw new Error("useCarousel must be used within a "); - } - - return context; -} - -const Carousel = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & CarouselProps ->( - ( - { - orientation = "horizontal", - opts, - setApi, - plugins, - className, - children, - ...props - }, - ref, - ) => { - const [carouselRef, api] = useEmblaCarousel( - { - ...opts, - axis: orientation === "horizontal" ? "x" : "y", - }, - plugins, - ); - const [canScrollPrev, setCanScrollPrev] = React.useState(false); - const [canScrollNext, setCanScrollNext] = React.useState(false); - - const onSelect = React.useCallback((api: CarouselApi) => { - if (!api) { - return; - } - - setCanScrollPrev(api.canScrollPrev()); - setCanScrollNext(api.canScrollNext()); - }, []); - - const scrollPrev = React.useCallback(() => { - api?.scrollPrev(); - }, [api]); - - const scrollNext = React.useCallback(() => { - api?.scrollNext(); - }, [api]); - - const handleKeyDown = React.useCallback( - (event: React.KeyboardEvent) => { - if (event.key === "ArrowLeft") { - event.preventDefault(); - scrollPrev(); - } else if (event.key === "ArrowRight") { - event.preventDefault(); - scrollNext(); - } - }, - [scrollPrev, scrollNext], - ); - - React.useEffect(() => { - if (!api || !setApi) { - return; - } - - setApi(api); - }, [api, setApi]); - - React.useEffect(() => { - if (!api) { - return; - } - - onSelect(api); - api.on("reInit", onSelect); - api.on("select", onSelect); - - return () => { - api?.off("select", onSelect); - }; - }, [api, onSelect]); - - return ( - -
- {children} -
-
- ); - }, -); -Carousel.displayName = "Carousel"; - -const CarouselContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const { carouselRef, orientation } = useCarousel(); - - return ( -
-
-
- ); -}); -CarouselContent.displayName = "CarouselContent"; - -const CarouselItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const { orientation } = useCarousel(); - - return ( -
- ); -}); -CarouselItem.displayName = "CarouselItem"; - -const CarouselPrevious = React.forwardRef< - HTMLButtonElement, - React.ComponentProps ->(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { t } = useTranslation(["views/explore"]); - const { orientation, scrollPrev, canScrollPrev } = useCarousel(); - - return ( - - ); -}); -CarouselPrevious.displayName = "CarouselPrevious"; - -const CarouselNext = React.forwardRef< - HTMLButtonElement, - React.ComponentProps ->(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { t } = useTranslation(["views/explore"]); - const { orientation, scrollNext, canScrollNext } = useCarousel(); - - return ( - - ); -}); -CarouselNext.displayName = "CarouselNext"; - -export { - type CarouselApi, - Carousel, - CarouselContent, - CarouselItem, - CarouselPrevious, - CarouselNext, -}; diff --git a/web/src/components/ui/form.tsx b/web/src/components/ui/form.tsx index dc6102ea2..aa4db1f36 100644 --- a/web/src/components/ui/form.tsx +++ b/web/src/components/ui/form.tsx @@ -33,9 +33,9 @@ const FormField = < ...props }: ControllerProps) => { return ( - + - + ); }; @@ -77,9 +77,9 @@ const FormItem = React.forwardRef< const id = React.useId(); return ( - +
- + ); }); FormItem.displayName = "FormItem"; diff --git a/web/src/components/ui/icon-wrapper.tsx b/web/src/components/ui/icon-wrapper.tsx index f87d18c62..f9504e0a1 100644 --- a/web/src/components/ui/icon-wrapper.tsx +++ b/web/src/components/ui/icon-wrapper.tsx @@ -1,10 +1,10 @@ import { ForwardedRef, forwardRef } from "react"; import { IconType } from "react-icons"; -interface IconWrapperProps { +interface IconWrapperProps extends React.HTMLAttributes { icon: IconType; className?: string; - [key: string]: any; + disabled?: boolean; } const IconWrapper = forwardRef( diff --git a/web/src/components/ui/sidebar.tsx b/web/src/components/ui/sidebar.tsx index 5a21a6cc1..e7dc78de7 100644 --- a/web/src/components/ui/sidebar.tsx +++ b/web/src/components/ui/sidebar.tsx @@ -141,7 +141,7 @@ const SidebarProvider = React.forwardRef< ); return ( - +
- + ); }, ); diff --git a/web/src/components/ui/toggle-group.tsx b/web/src/components/ui/toggle-group.tsx index 19505f9a4..85710b4a3 100644 --- a/web/src/components/ui/toggle-group.tsx +++ b/web/src/components/ui/toggle-group.tsx @@ -22,9 +22,9 @@ const ToggleGroup = React.forwardRef< className={cn("flex items-center justify-center gap-1", className)} {...props} > - + {children} - + )) diff --git a/web/src/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-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/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 && (
(); // 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" > From 65db9b0aec87b2957156ce2afe4415916a6ffe9c Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:11:32 -0600 Subject: [PATCH 7/9] Fixes (#22280) * fix ollama chat tool calling handle dict arguments, streaming fallback, and message format * pin setuptools<81 to ensure pkg_resources remains available When ensure_torch_dependencies() installs torch/torchvision via pip, it can upgrade setuptools to >=81.0.0, which removed the pkg_resources module. rknn-toolkit2 depends on pkg_resources internally, so subsequent RKNN conversion fails with No module named 'pkg_resources'. --- frigate/genai/ollama.py | 70 +++++++++++++++++++++++++++------- frigate/genai/utils.py | 27 +++++++------ frigate/util/rknn_converter.py | 1 + 3 files changed, 74 insertions(+), 24 deletions(-) diff --git a/frigate/genai/ollama.py b/frigate/genai/ollama.py index eb63f7fdb..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 ) @@ -233,11 +278,10 @@ class OllamaClient(GenAIClient): 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", + "tool_calls": None, + "finish_reason": "stop", } break 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/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", ], From 02678f4a097dcffe00a4bbbdc01ebc9c190a5b4c Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:17:41 -0600 Subject: [PATCH 8/9] show log when anonymous users log in (#22254) based on a cache key built from remote_addr and user agent, expires after 7 days by default --- frigate/api/auth.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) 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", From 229436c94afe4ee642d9280d0380e808a0bb819f Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:19:30 -0600 Subject: [PATCH 9/9] Add ability to clear region grids from the frontend (#22277) * backend * frontend * i18n * tweaks --- frigate/api/media.py | 18 +++ web/public/locales/en/views/settings.json | 13 +- web/src/pages/Settings.tsx | 15 ++- ...ingsView.tsx => MediaSyncSettingsView.tsx} | 4 +- .../views/settings/RegionGridSettingsView.tsx | 124 ++++++++++++++++++ 5 files changed, 167 insertions(+), 7 deletions(-) rename web/src/views/settings/{MaintenanceSettingsView.tsx => MediaSyncSettingsView.tsx} (99%) create mode 100644 web/src/views/settings/RegionGridSettingsView.tsx diff --git a/frigate/api/media.py b/frigate/api/media.py index 3cfd97674..2ddabc631 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -24,6 +24,7 @@ from tzlocal import get_localzone_name from frigate.api.auth import ( allow_any_authenticated, require_camera_access, + require_role, ) from frigate.api.defs.query.media_query_parameters import ( Extension, @@ -1005,6 +1006,23 @@ def grid_snapshot( ) +@router.delete( + "/{camera_name}/region_grid", dependencies=[Depends(require_role("admin"))] +) +def clear_region_grid(request: Request, camera_name: str): + """Clear the region grid for a camera.""" + if camera_name not in request.app.frigate_config.cameras: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + Regions.delete().where(Regions.camera == camera_name).execute() + return JSONResponse( + content={"success": True, "message": "Region grid cleared"}, + ) + + @router.get( "/events/{event_id}/snapshot-clean.webp", dependencies=[Depends(require_camera_access)], diff --git a/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/pages/Settings.tsx b/web/src/pages/Settings.tsx index e686ea241..ea6c3d650 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -40,7 +40,8 @@ import UsersView from "@/views/settings/UsersView"; import RolesView from "@/views/settings/RolesView"; import UiSettingsView from "@/views/settings/UiSettingsView"; import FrigatePlusSettingsView from "@/views/settings/FrigatePlusSettingsView"; -import MaintenanceSettingsView from "@/views/settings/MaintenanceSettingsView"; +import MediaSyncSettingsView from "@/views/settings/MediaSyncSettingsView"; +import RegionGridSettingsView from "@/views/settings/RegionGridSettingsView"; import SystemDetectionModelSettingsView from "@/views/settings/SystemDetectionModelSettingsView"; import { SingleSectionPage, @@ -154,7 +155,8 @@ const allSettingsViews = [ "roles", "notifications", "frigateplus", - "maintenance", + "mediaSync", + "regionGrid", ] as const; type SettingsType = (typeof allSettingsViews)[number]; @@ -444,7 +446,10 @@ const settingsGroups = [ }, { label: "maintenance", - items: [{ key: "maintenance", component: MaintenanceSettingsView }], + items: [ + { key: "mediaSync", component: MediaSyncSettingsView }, + { key: "regionGrid", component: RegionGridSettingsView }, + ], }, ]; @@ -471,6 +476,7 @@ const CAMERA_SELECT_BUTTON_PAGES = [ "masksAndZones", "motionTuner", "triggers", + "regionGrid", ]; const ALLOWED_VIEWS_FOR_VIEWER = ["ui", "debug", "notifications"]; @@ -478,7 +484,8 @@ const ALLOWED_VIEWS_FOR_VIEWER = ["ui", "debug", "notifications"]; const LARGE_BOTTOM_MARGIN_PAGES = [ "masksAndZones", "motionTuner", - "maintenance", + "mediaSync", + "regionGrid", ]; // keys for camera sections diff --git a/web/src/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")} + + + + + + ); +}