diff --git a/.github/actions/install-frontend-deps/action.yml b/.github/actions/install-frontend-deps/action.yml
index 1e6d3e6be80..530ab0a722c 100644
--- a/.github/actions/install-frontend-deps/action.yml
+++ b/.github/actions/install-frontend-deps/action.yml
@@ -1,5 +1,10 @@
name: install frontend dependencies
description: Installs frontend dependencies with pnpm, with caching
+inputs:
+ working-directory:
+ description: Directory to install dependencies in
+ required: false
+ default: invokeai/frontend/web
runs:
using: 'composite'
steps:
@@ -30,4 +35,4 @@ runs:
- name: install frontend dependencies
run: pnpm install --prefer-frozen-lockfile
shell: bash
- working-directory: invokeai/frontend/web
+ working-directory: ${{ inputs.working-directory }}
diff --git a/.github/workflows/frontend-checks.yml b/.github/workflows/frontend-checks.yml
index b36fbeb650b..c287d45529d 100644
--- a/.github/workflows/frontend-checks.yml
+++ b/.github/workflows/frontend-checks.yml
@@ -30,14 +30,13 @@ on:
type: boolean
default: true
-defaults:
- run:
- working-directory: invokeai/frontend/web
-
jobs:
frontend-checks:
runs-on: ubuntu-latest
timeout-minutes: 10 # expected run time: <2 min
+ defaults:
+ run:
+ working-directory: invokeai/frontend/web
steps:
- uses: actions/checkout@v6
@@ -94,3 +93,37 @@ jobs:
if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
run: 'pnpm lint:knip'
shell: bash
+
+ frontend-webv2-checks:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10 # expected run time: <2 min
+ defaults:
+ run:
+ working-directory: invokeai/frontend/webv2
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: check for changed webv2 files
+ if: ${{ inputs.always_run != true }}
+ id: changed-files
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96
+ with:
+ files_yaml: |
+ webv2:
+ - 'invokeai/frontend/webv2/**'
+
+ - name: install dependencies
+ if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
+ uses: ./.github/actions/install-frontend-deps
+ with:
+ working-directory: invokeai/frontend/webv2
+
+ - name: lint
+ if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm run lint'
+ shell: bash
+
+ - name: build
+ if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm run build'
+ shell: bash
diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml
index abb1fb8419f..824ec08203a 100644
--- a/.github/workflows/frontend-tests.yml
+++ b/.github/workflows/frontend-tests.yml
@@ -30,14 +30,13 @@ on:
type: boolean
default: true
-defaults:
- run:
- working-directory: invokeai/frontend/web
-
jobs:
frontend-tests:
runs-on: ubuntu-latest
timeout-minutes: 10 # expected run time: <2 min
+ defaults:
+ run:
+ working-directory: invokeai/frontend/web
steps:
- uses: actions/checkout@v6
@@ -63,3 +62,32 @@ jobs:
if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
run: 'pnpm test:no-watch'
shell: bash
+
+ frontend-webv2-tests:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10 # expected run time: <2 min
+ defaults:
+ run:
+ working-directory: invokeai/frontend/webv2
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: check for changed webv2 files
+ if: ${{ inputs.always_run != true }}
+ id: changed-files
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96
+ with:
+ files_yaml: |
+ webv2:
+ - 'invokeai/frontend/webv2/**'
+
+ - name: install dependencies
+ if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
+ uses: ./.github/actions/install-frontend-deps
+ with:
+ working-directory: invokeai/frontend/webv2
+
+ - name: vitest
+ if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm test'
+ shell: bash
diff --git a/.gitignore b/.gitignore
index cc037f09abd..cbb63f66c37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,6 +135,16 @@ env/
venv/
ENV/
+# Generated stuff
+invokeai.yaml
+invokeai.example.yaml
+
+/models/
+/databases/
+/configs/
+/nodes/
+/outputs/
+
# Spyder project settings
.spyderproject
.spyproject
diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py
index e7468c1bca4..2957bd70d97 100644
--- a/invokeai/app/api/dependencies.py
+++ b/invokeai/app/api/dependencies.py
@@ -41,6 +41,7 @@
from invokeai.app.services.names.names_default import SimpleNameService
from invokeai.app.services.object_serializer.object_serializer_disk import ObjectSerializerDisk
from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache
+from invokeai.app.services.project_records.project_records_sqlite import ProjectRecordsSqlite
from invokeai.app.services.session_processor.session_processor_default import (
DefaultSessionProcessor,
DefaultSessionRunner,
@@ -187,6 +188,7 @@ def initialize(
style_preset_image_files = StylePresetImageFileStorageDisk(style_presets_folder / "images")
workflow_thumbnails = WorkflowThumbnailFileStorageDisk(workflow_thumbnails_folder)
client_state_persistence = ClientStatePersistenceSqlite(db=db)
+ project_records = ProjectRecordsSqlite(db=db)
users = UserService(db=db)
services = InvocationServices(
@@ -220,6 +222,7 @@ def initialize(
style_preset_image_files=style_preset_image_files,
workflow_thumbnails=workflow_thumbnails,
client_state_persistence=client_state_persistence,
+ project_records=project_records,
users=users,
)
diff --git a/invokeai/app/api/routers/_access.py b/invokeai/app/api/routers/_access.py
index fae3971a144..9e442c6dc29 100644
--- a/invokeai/app/api/routers/_access.py
+++ b/invokeai/app/api/routers/_access.py
@@ -22,6 +22,8 @@ def assert_image_owner(image_name: str, current_user: CurrentUserOrDefault) -> N
"""
if current_user.is_admin:
return
+ if not ApiDependencies.invoker.services.image_records.exists(image_name):
+ raise HTTPException(status_code=404, detail="Image not found")
owner = ApiDependencies.invoker.services.image_records.get_user_id(image_name)
if owner is not None and owner == current_user.user_id:
return
@@ -50,6 +52,8 @@ def assert_image_read_access(image_name: str, current_user: CurrentUserOrDefault
"""
if current_user.is_admin:
return
+ if not ApiDependencies.invoker.services.image_records.exists(image_name):
+ raise HTTPException(status_code=404, detail="Image not found")
owner = ApiDependencies.invoker.services.image_records.get_user_id(image_name)
if owner is not None and owner == current_user.user_id:
diff --git a/invokeai/app/api/routers/app_info.py b/invokeai/app/api/routers/app_info.py
index 832e58f5e24..1ea8cbe494b 100644
--- a/invokeai/app/api/routers/app_info.py
+++ b/invokeai/app/api/routers/app_info.py
@@ -23,6 +23,7 @@
load_external_api_keys,
)
from invokeai.app.services.external_generation.external_generation_common import ExternalProviderStatus
+from invokeai.app.services.external_generation.startup import sync_configured_external_starter_models
from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
from invokeai.app.services.model_records.model_records_base import UnknownModelException
from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch
@@ -178,7 +179,7 @@ async def update_runtime_config(
status_code=200,
response_model=list[ExternalProviderStatusModel],
)
-async def get_external_provider_statuses() -> list[ExternalProviderStatusModel]:
+async def get_external_provider_statuses(_: AdminUserOrDefault) -> list[ExternalProviderStatusModel]:
statuses = ApiDependencies.invoker.services.external_generation.get_provider_statuses()
return [status_to_model(status) for status in statuses.values()]
@@ -189,7 +190,7 @@ async def get_external_provider_statuses() -> list[ExternalProviderStatusModel]:
status_code=200,
response_model=list[ExternalProviderConfigModel],
)
-async def get_external_provider_configs() -> list[ExternalProviderConfigModel]:
+async def get_external_provider_configs(_: AdminUserOrDefault) -> list[ExternalProviderConfigModel]:
config = get_config()
return [_build_external_provider_config(provider_id, config) for provider_id in EXTERNAL_PROVIDER_FIELDS]
@@ -219,9 +220,14 @@ async def set_external_provider_config(
raise HTTPException(status_code=400, detail="No external provider config fields provided")
api_key_removed = update.api_key is not None and updates.get(api_key_field) is None
+ api_key_set = update.api_key is not None and updates.get(api_key_field) is not None
_apply_external_provider_update(updates)
if api_key_removed:
_remove_external_models_for_provider(provider_id)
+ elif api_key_set:
+ # Configuring a key should make the provider's models usable without a
+ # restart; queue its external starter models the same way startup does.
+ _sync_external_starter_models_for_provider(provider_id)
return _build_external_provider_config(provider_id, get_config())
@@ -316,6 +322,19 @@ def _build_external_provider_config(provider_id: str, config: InvokeAIAppConfig)
)
+def _sync_external_starter_models_for_provider(provider_id: str) -> None:
+ invoker = ApiDependencies.invoker
+ try:
+ sync_configured_external_starter_models(
+ configured_provider_ids={provider_id},
+ model_manager=invoker.services.model_manager,
+ logger=invoker.services.logger,
+ )
+ except Exception as error:
+ # Queuing installs must never fail the config save; surface and move on.
+ invoker.services.logger.warning(f"Failed queueing external starter models for '{provider_id}': {error}")
+
+
def _remove_external_models_for_provider(provider_id: str) -> None:
model_manager = ApiDependencies.invoker.services.model_manager
external_models = model_manager.store.search_by_attr(
diff --git a/invokeai/app/api/routers/custom_nodes.py b/invokeai/app/api/routers/custom_nodes.py
index 3ee8c0ec99c..473d4c55f3b 100644
--- a/invokeai/app/api/routers/custom_nodes.py
+++ b/invokeai/app/api/routers/custom_nodes.py
@@ -1,8 +1,8 @@
"""FastAPI routes for custom node management."""
+import asyncio
import json
import shutil
-import subprocess
import sys
import traceback
from importlib.util import module_from_spec, spec_from_file_location
@@ -28,6 +28,7 @@
# were imported by that pack. Used on uninstall to delete only pack-imported workflows
# — deleting by tag alone is unsafe because users can edit tags on their own workflows.
PACK_MANIFEST_FILENAME = ".invokeai_pack_manifest.json"
+GIT_CLONE_TIMEOUT_SECONDS = 120
class NodePackInfo(BaseModel):
@@ -126,6 +127,26 @@ def _get_installed_packs() -> list[NodePackInfo]:
return packs
+async def _clone_node_pack(source: str, target_dir: Path) -> tuple[int, str]:
+ process = await asyncio.create_subprocess_exec(
+ "git",
+ "clone",
+ source,
+ str(target_dir),
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ )
+
+ try:
+ _stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=GIT_CLONE_TIMEOUT_SECONDS)
+ except asyncio.TimeoutError:
+ process.kill()
+ await process.communicate()
+ raise
+
+ return process.returncode or 0, stderr.decode(errors="replace").strip()
+
+
@custom_nodes_router.get(
"/",
operation_id="list_custom_node_packs",
@@ -172,21 +193,16 @@ async def install_custom_node_pack(
try:
# Clone the repository
- result = subprocess.run(
- ["git", "clone", source, str(target_dir)],
- capture_output=True,
- text=True,
- timeout=120,
- )
+ returncode, stderr = await _clone_node_pack(source, target_dir)
- if result.returncode != 0:
+ if returncode != 0:
# Clean up on failure
if target_dir.exists():
shutil.rmtree(target_dir)
return InstallNodePackResponse(
name=pack_name,
success=False,
- message=f"Git clone failed: {result.stderr.strip()}",
+ message=f"Git clone failed: {stderr}",
)
# Detect dependency manifests but do NOT install them automatically.
@@ -232,7 +248,7 @@ async def install_custom_node_pack(
dependency_file=dependency_file,
)
- except subprocess.TimeoutExpired:
+ except asyncio.TimeoutError:
if target_dir.exists():
shutil.rmtree(target_dir)
return InstallNodePackResponse(
diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py
index bdd2e406444..a1c47b4e722 100644
--- a/invokeai/app/api/routers/model_manager.py
+++ b/invokeai/app/api/routers/model_manager.py
@@ -11,7 +11,7 @@
from typing import List, Optional, Type
import huggingface_hub
-from fastapi import Body, Path, Query, Response, UploadFile
+from fastapi import Body, Header, Path, Query, Response, UploadFile
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.routing import APIRouter
from PIL import Image
@@ -68,6 +68,11 @@ class ModelsList(BaseModel):
model_config = ConfigDict(use_enum_values=True)
+class EmptyModelCacheResponse(BaseModel):
+ models_cleared: int
+ bytes_freed: int
+
+
class CacheType(str, Enum):
"""Cache type - one of vram or ram."""
@@ -218,6 +223,20 @@ async def list_missing_models() -> ModelsList:
return ModelsList(models=missing_models)
+@model_manager_router.get(
+ "/models_dir",
+ operation_id="get_models_dir",
+ responses={200: {"description": "The absolute path of the models directory"}},
+)
+async def get_models_dir() -> str:
+ """Get the absolute path of the directory managed models are stored in.
+
+ Model config `path` values are relative to this directory unless they are
+ absolute (in-place installs from outside it).
+ """
+ return ApiDependencies.invoker.services.configuration.models_path.resolve().as_posix()
+
+
@model_manager_router.get(
"/get_by_attrs",
operation_id="get_model_records_by_attrs",
@@ -747,6 +766,11 @@ async def install_model(
source: str = Query(description="Model source to install, can be a local path, repo_id, or remote URL"),
inplace: Optional[bool] = Query(description="Whether or not to install a local model in place", default=False),
access_token: Optional[str] = Query(description="access token for the remote resource", default=None),
+ source_access_token: Optional[str] = Header(
+ alias="X-Model-Source-Access-Token",
+ description="access token for the remote resource",
+ default=None,
+ ),
config: ModelRecordChanges = Body(
description="Object containing fields that override auto-probed values in the model config record, such as name, description and prediction_type ",
examples=[{"name": "string", "description": "string"}],
@@ -786,7 +810,7 @@ async def install_model(
result: ModelInstallJob = installer.heuristic_import(
source=source,
config=config,
- access_token=access_token,
+ access_token=source_access_token or access_token,
inplace=bool(inplace),
)
logger.info(f"Started installation of {source}")
@@ -1301,12 +1325,14 @@ async def get_stats() -> Optional[CacheStats]:
"/empty_model_cache",
operation_id="empty_model_cache",
status_code=200,
+ response_model=EmptyModelCacheResponse,
)
-async def empty_model_cache(current_admin: AdminUserOrDefault) -> None:
+async def empty_model_cache(current_admin: AdminUserOrDefault) -> EmptyModelCacheResponse:
"""Drop all models from the model cache to free RAM/VRAM. 'Locked' models that are in active use will not be dropped."""
# Request 1000GB of room in order to force the cache to drop all models.
ApiDependencies.invoker.services.logger.info("Emptying model cache.")
- ApiDependencies.invoker.services.model_manager.load.ram_cache.make_room(1000 * 2**30)
+ result = ApiDependencies.invoker.services.model_manager.load.ram_cache.make_room(1000 * 2**30)
+ return EmptyModelCacheResponse(models_cleared=result.models_cleared, bytes_freed=result.bytes_freed)
class HFTokenStatus(str, Enum):
@@ -1341,7 +1367,7 @@ def reset_token(cls) -> HFTokenStatus:
@model_manager_router.get("/hf_login", operation_id="get_hf_login_status", response_model=HFTokenStatus)
-async def get_hf_login_status() -> HFTokenStatus:
+async def get_hf_login_status(_: AdminUserOrDefault) -> HFTokenStatus:
token_status = HFTokenHelper.get_status()
if token_status is HFTokenStatus.UNKNOWN:
diff --git a/invokeai/app/api/routers/projects.py b/invokeai/app/api/routers/projects.py
new file mode 100644
index 00000000000..6609c091235
--- /dev/null
+++ b/invokeai/app/api/routers/projects.py
@@ -0,0 +1,105 @@
+from typing import Any
+
+from fastapi import Body, HTTPException, Path, status
+from fastapi.routing import APIRouter
+from pydantic import BaseModel, Field
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.project_records.project_records_common import (
+ ProjectRecordConflictError,
+ ProjectRecordDTO,
+ ProjectRecordExistsError,
+ ProjectRecordNotFoundError,
+ ProjectSummaryDTO,
+)
+
+projects_router = APIRouter(prefix="/v1/projects", tags=["projects"])
+
+
+class ProjectCreateRequest(BaseModel):
+ """Request body for creating a project."""
+
+ project_id: str | None = Field(
+ default=None, description="Client-generated project id (e.g. for imports); generated when omitted"
+ )
+ name: str = Field(description="The project's display name")
+ data: dict[str, Any] = Field(description="The opaque client-owned project document")
+
+
+class ProjectUpdateRequest(BaseModel):
+ """Request body for saving a project with optimistic concurrency."""
+
+ name: str = Field(description="The project's display name")
+ data: dict[str, Any] = Field(description="The opaque client-owned project document")
+ expected_revision: int = Field(description="The revision this save is based on; mismatch returns 409")
+
+
+@projects_router.get("/", operation_id="list_projects", response_model=list[ProjectSummaryDTO])
+async def list_projects(current_user: CurrentUserOrDefault) -> list[ProjectSummaryDTO]:
+ """Lists the current user's projects as lightweight summaries (no documents)."""
+ return ApiDependencies.invoker.services.project_records.list(current_user.user_id)
+
+
+@projects_router.post(
+ "/", operation_id="create_project", response_model=ProjectRecordDTO, status_code=status.HTTP_201_CREATED
+)
+async def create_project(
+ current_user: CurrentUserOrDefault,
+ request: ProjectCreateRequest = Body(description="The project to create"),
+) -> ProjectRecordDTO:
+ """Creates a project for the current user."""
+ try:
+ return ApiDependencies.invoker.services.project_records.create(
+ user_id=current_user.user_id,
+ name=request.name,
+ data=request.data,
+ project_id=request.project_id,
+ )
+ except ProjectRecordExistsError as e:
+ raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
+
+
+@projects_router.get("/{project_id}", operation_id="get_project", response_model=ProjectRecordDTO)
+async def get_project(
+ current_user: CurrentUserOrDefault,
+ project_id: str = Path(description="The id of the project to get"),
+) -> ProjectRecordDTO:
+ """Gets one of the current user's projects, including its document."""
+ try:
+ return ApiDependencies.invoker.services.project_records.get(current_user.user_id, project_id)
+ except ProjectRecordNotFoundError as e:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
+
+
+@projects_router.put("/{project_id}", operation_id="update_project", response_model=ProjectRecordDTO)
+async def update_project(
+ current_user: CurrentUserOrDefault,
+ project_id: str = Path(description="The id of the project to save"),
+ request: ProjectUpdateRequest = Body(description="The project document and the revision it is based on"),
+) -> ProjectRecordDTO:
+ """Saves a project. Returns 409 with the current revision when the save is based on a stale revision."""
+ try:
+ return ApiDependencies.invoker.services.project_records.update(
+ user_id=current_user.user_id,
+ project_id=project_id,
+ expected_revision=request.expected_revision,
+ name=request.name,
+ data=request.data,
+ )
+ except ProjectRecordNotFoundError as e:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
+ except ProjectRecordConflictError as e:
+ raise HTTPException(
+ status_code=status.HTTP_409_CONFLICT,
+ detail={"message": str(e), "current_revision": e.current_revision},
+ )
+
+
+@projects_router.delete("/{project_id}", operation_id="delete_project", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_project(
+ current_user: CurrentUserOrDefault,
+ project_id: str = Path(description="The id of the project to delete"),
+) -> None:
+ """Deletes one of the current user's projects. Idempotent."""
+ ApiDependencies.invoker.services.project_records.delete(current_user.user_id, project_id)
diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py
index cd2260d2271..02c466d0a17 100644
--- a/invokeai/app/api/routers/session_queue.py
+++ b/invokeai/app/api/routers/session_queue.py
@@ -1,4 +1,4 @@
-from typing import Optional
+from typing import Callable, Optional
from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter
@@ -6,6 +6,7 @@
from invokeai.app.api.auth_dependencies import AdminUserOrDefault, CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.invocations.fields import ImageField
from invokeai.app.services.session_processor.session_processor_common import SessionProcessorStatus
from invokeai.app.services.session_queue.session_queue_common import (
Batch,
@@ -38,6 +39,59 @@ class SessionQueueAndProcessorStatus(BaseModel):
processor: SessionProcessorStatus
+def _image_record_exists(image_name: str) -> bool:
+ return ApiDependencies.invoker.services.image_records.exists(image_name)
+
+
+def strip_missing_image_results(
+ queue_item: SessionQueueItem, image_exists: Callable[[str], bool] | None = None
+) -> SessionQueueItem:
+ """Remove result outputs whose image records have been deleted.
+
+ Completed queue history can outlive its output images. API clients hydrate
+ images listed in `session.results`; returning stale names makes them loop on
+ 404s. Keep the queue item/history, but do not advertise impossible outputs.
+ """
+ if not queue_item.session.results:
+ return queue_item
+
+ image_exists = image_exists or _image_record_exists
+ filtered_results = {}
+ did_filter = False
+ exists_cache: dict[str, bool] = {}
+
+ def cached_exists(image_name: str) -> bool:
+ if image_name not in exists_cache:
+ exists_cache[image_name] = image_exists(image_name)
+ return exists_cache[image_name]
+
+ for node_id, output in queue_item.session.results.items():
+ image = getattr(output, "image", None)
+ if isinstance(image, ImageField) and not cached_exists(image.image_name):
+ did_filter = True
+ continue
+
+ collection = getattr(output, "collection", None)
+ if isinstance(collection, list) and any(isinstance(item, ImageField) for item in collection):
+ filtered_collection = [
+ item for item in collection if not isinstance(item, ImageField) or cached_exists(item.image_name)
+ ]
+ if len(filtered_collection) != len(collection):
+ did_filter = True
+ if len(filtered_collection) == 0:
+ continue
+ output = output.model_copy(update={"collection": filtered_collection})
+
+ filtered_results[node_id] = output
+
+ if not did_filter:
+ return queue_item
+
+ sanitized_item = queue_item.model_copy(deep=True)
+ sanitized_item.session.results = filtered_results
+ return sanitized_item
+
+
def sanitize_queue_item_for_user(
queue_item: SessionQueueItem, current_user_id: str, is_admin: bool
) -> SessionQueueItem:
@@ -57,7 +111,7 @@ def sanitize_queue_item_for_user(
"""
# Admins and item owners can see everything
if is_admin or queue_item.user_id == current_user_id:
- return queue_item
+ return strip_missing_image_results(queue_item)
# For non-admins viewing other users' items, strip everything except
# item_id, queue_id, status, and timestamps
@@ -83,6 +137,18 @@ def sanitize_queue_item_for_user(
return sanitized_item
+def get_queue_item_for_mutation(queue_id: str, item_id: int, current_user: CurrentUserOrDefault) -> SessionQueueItem:
+ queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
+
+ if queue_item.queue_id != queue_id:
+ raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
+
+ if queue_item.user_id != current_user.user_id and not current_user.is_admin:
+ raise HTTPException(status_code=403, detail=f"You do not have permission to mutate queue item {item_id}")
+
+ return queue_item
+
+
@session_queue_router.post(
"/{queue_id}/enqueue_batch",
operation_id="enqueue_batch",
@@ -140,6 +206,9 @@ async def get_queue_item_ids(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
+ origin_prefix: Optional[str] = Query(
+ default=None, description="Only include queue items whose origin starts with this prefix"
+ ),
) -> ItemIdsResult:
"""Gets all queue item ids that match the given parameters.
@@ -152,7 +221,9 @@ async def get_queue_item_ids(
current_user is required so the endpoint stays behind authentication in multiuser mode.
"""
try:
- return ApiDependencies.invoker.services.session_queue.get_queue_item_ids(queue_id=queue_id, order_dir=order_dir)
+ return ApiDependencies.invoker.services.session_queue.get_queue_item_ids(
+ queue_id=queue_id, order_dir=order_dir, origin_prefix=origin_prefix
+ )
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while listing all queue item ids: {e}")
@@ -320,15 +391,21 @@ async def retry_items_by_id(
try:
# Check authorization: user must own all items or be an admin
if not current_user.is_admin:
+ for item_id in item_ids:
+ try:
+ get_queue_item_for_mutation(queue_id, item_id, current_user)
+ except SessionQueueItemNotFoundError:
+ # Skip items that don't exist - they will be handled by retry_items_by_id
+ continue
+ else:
for item_id in item_ids:
try:
queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
- if queue_item.user_id != current_user.user_id:
+ if queue_item.queue_id != queue_id:
raise HTTPException(
- status_code=403, detail=f"You do not have permission to retry queue item {item_id}"
+ status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}"
)
except SessionQueueItemNotFoundError:
- # Skip items that don't exist - they will be handled by retry_items_by_id
continue
return ApiDependencies.invoker.services.session_queue.retry_items_by_id(queue_id=queue_id, item_ids=item_ids)
@@ -399,10 +476,13 @@ async def prune(
async def get_current_queue_item(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
+ origin_prefix: Optional[str] = Query(
+ default=None, description="Only include queue items whose origin starts with this prefix"
+ ),
) -> Optional[SessionQueueItem]:
"""Gets the currently execution queue item"""
try:
- item = ApiDependencies.invoker.services.session_queue.get_current(queue_id)
+ item = ApiDependencies.invoker.services.session_queue.get_current(queue_id, origin_prefix=origin_prefix)
if item is not None:
item = sanitize_queue_item_for_user(item, current_user.user_id, current_user.is_admin)
return item
@@ -420,10 +500,13 @@ async def get_current_queue_item(
async def get_next_queue_item(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
+ origin_prefix: Optional[str] = Query(
+ default=None, description="Only include queue items whose origin starts with this prefix"
+ ),
) -> Optional[SessionQueueItem]:
"""Gets the next queue item, without executing it"""
try:
- item = ApiDependencies.invoker.services.session_queue.get_next(queue_id)
+ item = ApiDependencies.invoker.services.session_queue.get_next(queue_id, origin_prefix=origin_prefix)
if item is not None:
item = sanitize_queue_item_for_user(item, current_user.user_id, current_user.is_admin)
return item
@@ -441,11 +524,16 @@ async def get_next_queue_item(
async def get_queue_status(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
+ origin_prefix: Optional[str] = Query(
+ default=None, description="Only include queue items whose origin starts with this prefix"
+ ),
) -> SessionQueueAndProcessorStatus:
"""Gets the status of the session queue. Non-admin users see only their own counts and cannot see current item details unless they own it."""
try:
user_id = None if current_user.is_admin else current_user.user_id
- queue = ApiDependencies.invoker.services.session_queue.get_queue_status(queue_id, user_id=user_id)
+ queue = ApiDependencies.invoker.services.session_queue.get_queue_status(
+ queue_id, user_id=user_id, origin_prefix=origin_prefix
+ )
processor = ApiDependencies.invoker.services.session_processor.get_status()
return SessionQueueAndProcessorStatus(queue=queue, processor=processor)
except Exception as e:
@@ -511,12 +599,7 @@ async def delete_queue_item(
) -> None:
"""Deletes a queue item. Users can only delete their own items unless they are an admin."""
try:
- # Get the queue item to check ownership
- queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
-
- # Check authorization: user must own the item or be an admin
- if queue_item.user_id != current_user.user_id and not current_user.is_admin:
- raise HTTPException(status_code=403, detail="You do not have permission to delete this queue item")
+ get_queue_item_for_mutation(queue_id, item_id, current_user)
ApiDependencies.invoker.services.session_queue.delete_queue_item(item_id)
except SessionQueueItemNotFoundError:
@@ -541,12 +624,7 @@ async def cancel_queue_item(
) -> SessionQueueItem:
"""Cancels a queue item. Users can only cancel their own items unless they are an admin."""
try:
- # Get the queue item to check ownership
- queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
-
- # Check authorization: user must own the item or be an admin
- if queue_item.user_id != current_user.user_id and not current_user.is_admin:
- raise HTTPException(status_code=403, detail="You do not have permission to cancel this queue item")
+ get_queue_item_for_mutation(queue_id, item_id, current_user)
return ApiDependencies.invoker.services.session_queue.cancel_queue_item(item_id)
except SessionQueueItemNotFoundError:
diff --git a/invokeai/app/api/sockets.py b/invokeai/app/api/sockets.py
index 7e93c332d64..acba63f2846 100644
--- a/invokeai/app/api/sockets.py
+++ b/invokeai/app/api/sockets.py
@@ -29,6 +29,7 @@
ModelInstallCompleteEvent,
ModelInstallDownloadProgressEvent,
ModelInstallDownloadsCompleteEvent,
+ ModelInstallDownloadStartedEvent,
ModelInstallErrorEvent,
ModelInstallStartedEvent,
ModelLoadCompleteEvent,
@@ -77,6 +78,7 @@ class BulkDownloadSubscriptionEvent(BaseModel):
DownloadStartedEvent,
ModelLoadStartedEvent,
ModelLoadCompleteEvent,
+ ModelInstallDownloadStartedEvent,
ModelInstallDownloadProgressEvent,
ModelInstallDownloadsCompleteEvent,
ModelInstallStartedEvent,
@@ -87,6 +89,16 @@ class BulkDownloadSubscriptionEvent(BaseModel):
BULK_DOWNLOAD_EVENTS = {BulkDownloadStartedEvent, BulkDownloadCompleteEvent, BulkDownloadErrorEvent}
+MODEL_INSTALL_EVENTS = (
+ ModelInstallDownloadStartedEvent,
+ ModelInstallDownloadProgressEvent,
+ ModelInstallDownloadsCompleteEvent,
+ ModelInstallStartedEvent,
+ ModelInstallCompleteEvent,
+ ModelInstallCancelledEvent,
+ ModelInstallErrorEvent,
+)
+
class SocketIO:
_sub_queue = "subscribe_queue"
@@ -354,7 +366,12 @@ async def _handle_queue_event(self, event: FastAPIEvent[QueueEventBase]):
logger.error(f"Error handling queue event {event[0]}: {e}", exc_info=True)
async def _handle_model_event(self, event: FastAPIEvent[ModelEventBase | DownloadEventBase]) -> None:
- await self._sio.emit(event=event[0], data=event[1].model_dump(mode="json"))
+ event_name, event_data = event
+ if isinstance(event_data, MODEL_INSTALL_EVENTS):
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room="admin")
+ return
+
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"))
async def _handle_bulk_image_download_event(self, event: FastAPIEvent[BulkDownloadEventBase]) -> None:
event_name, event_data = event
diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py
index 4b79e1eeb0c..db9ddb0b02b 100644
--- a/invokeai/app/api_app.py
+++ b/invokeai/app/api_app.py
@@ -26,6 +26,7 @@
images,
model_manager,
model_relationships,
+ projects,
recall_parameters,
session_queue,
style_presets,
@@ -186,6 +187,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
app.include_router(workflows.workflows_router, prefix="/api")
app.include_router(style_presets.style_presets_router, prefix="/api")
app.include_router(client_state.client_state_router, prefix="/api")
+app.include_router(projects.projects_router, prefix="/api")
app.include_router(recall_parameters.recall_parameters_router, prefix="/api")
app.include_router(custom_nodes.custom_nodes_router, prefix="/api")
diff --git a/invokeai/app/services/image_records/image_records_base.py b/invokeai/app/services/image_records/image_records_base.py
index 8c71dfba9e7..9e263522cd9 100644
--- a/invokeai/app/services/image_records/image_records_base.py
+++ b/invokeai/app/services/image_records/image_records_base.py
@@ -30,6 +30,11 @@ def get_metadata(self, image_name: str) -> Optional[MetadataField]:
"""Gets an image's metadata'."""
pass
+ @abstractmethod
+ def exists(self, image_name: str) -> bool:
+ """Returns whether an image record exists."""
+ pass
+
@abstractmethod
def update(
self,
diff --git a/invokeai/app/services/image_records/image_records_sqlite.py b/invokeai/app/services/image_records/image_records_sqlite.py
index 1eb3857dba6..8a6a7f47c68 100644
--- a/invokeai/app/services/image_records/image_records_sqlite.py
+++ b/invokeai/app/services/image_records/image_records_sqlite.py
@@ -84,6 +84,18 @@ def get_metadata(self, image_name: str) -> Optional[MetadataField]:
metadata_raw = cast(Optional[str], as_dict.get("metadata", None))
return MetadataFieldValidator.validate_json(metadata_raw) if metadata_raw is not None else None
+ def exists(self, image_name: str) -> bool:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT 1 FROM images
+ WHERE image_name = ?
+ LIMIT 1;
+ """,
+ (image_name,),
+ )
+ return cursor.fetchone() is not None
+
def update(
self,
image_name: str,
diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py
index 4a190f37edc..dabc65fb9f6 100644
--- a/invokeai/app/services/images/images_default.py
+++ b/invokeai/app/services/images/images_default.py
@@ -132,6 +132,9 @@ def get_pil_image(self, image_name: str) -> PILImageType:
try:
record = self.__invoker.services.image_records.get(image_name)
return self.__invoker.services.image_files.get(image_name, image_subfolder=record.image_subfolder)
+ except ImageRecordNotFoundException:
+ self.__invoker.services.logger.debug(f"Image record not found: {image_name}")
+ raise
except ImageFileNotFoundException:
self.__invoker.services.logger.error("Failed to get image file")
raise
@@ -143,7 +146,7 @@ def get_record(self, image_name: str) -> ImageRecord:
try:
return self.__invoker.services.image_records.get(image_name)
except ImageRecordNotFoundException:
- self.__invoker.services.logger.error("Image record not found")
+ self.__invoker.services.logger.debug(f"Image record not found: {image_name}")
raise
except Exception as e:
self.__invoker.services.logger.error("Problem getting image record")
@@ -162,7 +165,7 @@ def get_dto(self, image_name: str) -> ImageDTO:
return image_dto
except ImageRecordNotFoundException:
- self.__invoker.services.logger.error("Image record not found")
+ self.__invoker.services.logger.debug(f"Image record not found: {image_name}")
raise
except Exception as e:
self.__invoker.services.logger.error("Problem getting image DTO")
@@ -172,7 +175,7 @@ def get_metadata(self, image_name: str) -> Optional[MetadataField]:
try:
return self.__invoker.services.image_records.get_metadata(image_name)
except ImageRecordNotFoundException:
- self.__invoker.services.logger.error("Image record not found")
+ self.__invoker.services.logger.debug(f"Image record not found: {image_name}")
raise
except Exception as e:
self.__invoker.services.logger.error("Problem getting image metadata")
@@ -182,6 +185,9 @@ def get_workflow(self, image_name: str) -> Optional[str]:
try:
record = self.__invoker.services.image_records.get(image_name)
return self.__invoker.services.image_files.get_workflow(image_name, image_subfolder=record.image_subfolder)
+ except ImageRecordNotFoundException:
+ self.__invoker.services.logger.debug(f"Image record not found: {image_name}")
+ raise
except ImageFileNotFoundException:
self.__invoker.services.logger.error("Image file not found")
raise
@@ -193,6 +199,9 @@ def get_graph(self, image_name: str) -> Optional[str]:
try:
record = self.__invoker.services.image_records.get(image_name)
return self.__invoker.services.image_files.get_graph(image_name, image_subfolder=record.image_subfolder)
+ except ImageRecordNotFoundException:
+ self.__invoker.services.logger.debug(f"Image record not found: {image_name}")
+ raise
except ImageFileNotFoundException:
self.__invoker.services.logger.error("Image file not found")
raise
@@ -208,6 +217,9 @@ def get_path(self, image_name: str, thumbnail: bool = False) -> str:
image_name, thumbnail, image_subfolder=record.image_subfolder
)
)
+ except ImageRecordNotFoundException:
+ self.__invoker.services.logger.debug(f"Image record not found: {image_name}")
+ raise
except Exception as e:
self.__invoker.services.logger.error("Problem getting image path")
raise e
diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py
index 2c95f87b41d..923bfdd5905 100644
--- a/invokeai/app/services/invocation_services.py
+++ b/invokeai/app/services/invocation_services.py
@@ -34,6 +34,7 @@
)
from invokeai.app.services.model_relationships.model_relationships_base import ModelRelationshipsServiceABC
from invokeai.app.services.names.names_base import NameServiceBase
+ from invokeai.app.services.project_records.project_records_base import ProjectRecordsStorageBase
from invokeai.app.services.session_processor.session_processor_base import SessionProcessorBase
from invokeai.app.services.session_queue.session_queue_base import SessionQueueBase
from invokeai.app.services.urls.urls_base import UrlServiceBase
@@ -78,6 +79,7 @@ def __init__(
style_preset_image_files: "StylePresetImageFileStorageBase",
workflow_thumbnails: "WorkflowThumbnailServiceBase",
client_state_persistence: "ClientStatePersistenceABC",
+ project_records: "ProjectRecordsStorageBase",
users: "UserServiceBase",
):
self.board_images = board_images
@@ -110,4 +112,5 @@ def __init__(
self.style_preset_image_files = style_preset_image_files
self.workflow_thumbnails = workflow_thumbnails
self.client_state_persistence = client_state_persistence
+ self.project_records = project_records
self.users = users
diff --git a/invokeai/app/services/project_records/project_records_base.py b/invokeai/app/services/project_records/project_records_base.py
new file mode 100644
index 00000000000..0bc02b74d70
--- /dev/null
+++ b/invokeai/app/services/project_records/project_records_base.py
@@ -0,0 +1,62 @@
+from abc import ABC, abstractmethod
+from typing import Any
+
+from invokeai.app.services.project_records.project_records_common import ProjectRecordDTO, ProjectSummaryDTO
+
+
+class ProjectRecordsStorageBase(ABC):
+ """Storage for per-user workbench project documents.
+
+ All operations are scoped by user_id; a user can never read or write
+ another user's projects. Saves use optimistic concurrency via the
+ project's monotonic revision.
+ """
+
+ @abstractmethod
+ def create(self, user_id: str, name: str, data: dict[str, Any], project_id: str | None = None) -> ProjectRecordDTO:
+ """Create a project for the user.
+
+ Args:
+ user_id: The owning user.
+ name: The project's display name.
+ data: The opaque client-owned project document.
+ project_id: Client-generated id (e.g. for imports); generated when omitted.
+
+ Returns:
+ The created project record.
+
+ Raises:
+ ProjectRecordExistsError: The user already has a project with this id.
+ """
+ pass
+
+ @abstractmethod
+ def get(self, user_id: str, project_id: str) -> ProjectRecordDTO:
+ """Get one of the user's projects, including its document.
+
+ Raises:
+ ProjectRecordNotFoundError: No such project for this user.
+ """
+ pass
+
+ @abstractmethod
+ def list(self, user_id: str) -> list[ProjectSummaryDTO]:
+ """List the user's projects as lightweight summaries, oldest first."""
+ pass
+
+ @abstractmethod
+ def update(
+ self, user_id: str, project_id: str, expected_revision: int, name: str, data: dict[str, Any]
+ ) -> ProjectRecordDTO:
+ """Save a project if the caller's revision is current.
+
+ Raises:
+ ProjectRecordNotFoundError: No such project for this user.
+ ProjectRecordConflictError: The stored revision differs from expected_revision.
+ """
+ pass
+
+ @abstractmethod
+ def delete(self, user_id: str, project_id: str) -> None:
+ """Delete one of the user's projects. Idempotent: deleting a missing project is a no-op."""
+ pass
diff --git a/invokeai/app/services/project_records/project_records_common.py b/invokeai/app/services/project_records/project_records_common.py
new file mode 100644
index 00000000000..dc25b170174
--- /dev/null
+++ b/invokeai/app/services/project_records/project_records_common.py
@@ -0,0 +1,45 @@
+"""Common types and errors for the project records service."""
+
+from typing import Any
+
+from pydantic import BaseModel, Field
+
+
+class ProjectRecordNotFoundError(Exception):
+ """Raised when a project record is not found for the requesting user."""
+
+ def __init__(self, project_id: str) -> None:
+ super().__init__(f"Project {project_id} not found")
+
+
+class ProjectRecordExistsError(Exception):
+ """Raised when creating a project with an id the user already has."""
+
+ def __init__(self, project_id: str) -> None:
+ super().__init__(f"Project {project_id} already exists")
+
+
+class ProjectRecordConflictError(Exception):
+ """Raised when a save carries a stale revision (another client saved first)."""
+
+ def __init__(self, project_id: str, expected_revision: int, current_revision: int) -> None:
+ self.current_revision = current_revision
+ super().__init__(
+ f"Project {project_id} is at revision {current_revision}; the save expected revision {expected_revision}"
+ )
+
+
+class ProjectSummaryDTO(BaseModel):
+ """Lightweight project listing entry; the document payload is omitted."""
+
+ project_id: str = Field(description="The project's client-generated identifier")
+ name: str = Field(description="The project's display name")
+ revision: int = Field(description="Monotonic revision, incremented on every save")
+ created_at: str = Field(description="When the project was created")
+ updated_at: str = Field(description="When the project was last saved")
+
+
+class ProjectRecordDTO(ProjectSummaryDTO):
+ """Full project record including the client-owned document."""
+
+ data: dict[str, Any] = Field(description="The opaque client-owned project document")
diff --git a/invokeai/app/services/project_records/project_records_sqlite.py b/invokeai/app/services/project_records/project_records_sqlite.py
new file mode 100644
index 00000000000..0369a626f45
--- /dev/null
+++ b/invokeai/app/services/project_records/project_records_sqlite.py
@@ -0,0 +1,134 @@
+import json
+import sqlite3
+import uuid
+from typing import Any
+
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.project_records.project_records_base import ProjectRecordsStorageBase
+from invokeai.app.services.project_records.project_records_common import (
+ ProjectRecordConflictError,
+ ProjectRecordDTO,
+ ProjectRecordExistsError,
+ ProjectRecordNotFoundError,
+ ProjectSummaryDTO,
+)
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+
+
+class ProjectRecordsSqlite(ProjectRecordsStorageBase):
+ """SQLite implementation of per-user project document storage."""
+
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+
+ def create(self, user_id: str, name: str, data: dict[str, Any], project_id: str | None = None) -> ProjectRecordDTO:
+ project_id = project_id or uuid.uuid4().hex
+
+ try:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ INSERT INTO projects (project_id, user_id, name, data)
+ VALUES (?, ?, ?, ?);
+ """,
+ (project_id, user_id, name, json.dumps(data)),
+ )
+ except sqlite3.IntegrityError as e:
+ raise ProjectRecordExistsError(project_id) from e
+
+ return self.get(user_id, project_id)
+
+ def get(self, user_id: str, project_id: str) -> ProjectRecordDTO:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT project_id, name, data, revision, created_at, updated_at
+ FROM projects
+ WHERE user_id = ? AND project_id = ?;
+ """,
+ (user_id, project_id),
+ )
+ row = cursor.fetchone()
+
+ if row is None:
+ raise ProjectRecordNotFoundError(project_id)
+
+ return ProjectRecordDTO(
+ project_id=row[0],
+ name=row[1],
+ data=json.loads(row[2]),
+ revision=row[3],
+ created_at=row[4],
+ updated_at=row[5],
+ )
+
+ def list(self, user_id: str) -> list[ProjectSummaryDTO]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT project_id, name, revision, created_at, updated_at
+ FROM projects
+ WHERE user_id = ?
+ -- rowid breaks ties between rows created in the same millisecond,
+ -- keeping the listing in true insertion order.
+ ORDER BY created_at ASC, rowid ASC;
+ """,
+ (user_id,),
+ )
+ rows = cursor.fetchall()
+
+ return [
+ ProjectSummaryDTO(
+ project_id=row[0],
+ name=row[1],
+ revision=row[2],
+ created_at=row[3],
+ updated_at=row[4],
+ )
+ for row in rows
+ ]
+
+ def update(
+ self, user_id: str, project_id: str, expected_revision: int, name: str, data: dict[str, Any]
+ ) -> ProjectRecordDTO:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ UPDATE projects
+ SET name = ?, data = ?, revision = revision + 1
+ WHERE user_id = ? AND project_id = ? AND revision = ?;
+ """,
+ (name, json.dumps(data), user_id, project_id, expected_revision),
+ )
+
+ if cursor.rowcount == 0:
+ # Distinguish "gone" from "someone else saved first".
+ cursor.execute(
+ """--sql
+ SELECT revision FROM projects
+ WHERE user_id = ? AND project_id = ?;
+ """,
+ (user_id, project_id),
+ )
+ row = cursor.fetchone()
+
+ if row is None:
+ raise ProjectRecordNotFoundError(project_id)
+
+ raise ProjectRecordConflictError(project_id, expected_revision, row[0])
+
+ return self.get(user_id, project_id)
+
+ def delete(self, user_id: str, project_id: str) -> None:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ DELETE FROM projects
+ WHERE user_id = ? AND project_id = ?;
+ """,
+ (user_id, project_id),
+ )
diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py
index bb7971cf90d..09b521d6812 100644
--- a/invokeai/app/services/session_queue/session_queue_base.py
+++ b/invokeai/app/services/session_queue/session_queue_base.py
@@ -43,12 +43,12 @@ def enqueue_batch(
pass
@abstractmethod
- def get_current(self, queue_id: str) -> Optional[SessionQueueItem]:
+ def get_current(self, queue_id: str, origin_prefix: Optional[str] = None) -> Optional[SessionQueueItem]:
"""Gets the currently-executing session queue item"""
pass
@abstractmethod
- def get_next(self, queue_id: str) -> Optional[SessionQueueItem]:
+ def get_next(self, queue_id: str, origin_prefix: Optional[str] = None) -> Optional[SessionQueueItem]:
"""Gets the next session queue item (does not dequeue it)"""
pass
@@ -78,6 +78,7 @@ def get_queue_status(
queue_id: str,
user_id: Optional[str] = None,
acting_user_id: Optional[str] = None,
+ origin_prefix: Optional[str] = None,
) -> SessionQueueStatus:
"""Gets the status of the queue.
@@ -191,6 +192,7 @@ def get_queue_item_ids(
queue_id: str,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
user_id: Optional[str] = None,
+ origin_prefix: Optional[str] = None,
) -> ItemIdsResult:
"""Gets all queue item ids that match the given parameters. If user_id is provided, only returns items for that user."""
pass
diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py
index c29ed9b0038..71eba1eb48d 100644
--- a/invokeai/app/services/session_queue/session_queue_sqlite.py
+++ b/invokeai/app/services/session_queue/session_queue_sqlite.py
@@ -193,7 +193,7 @@ async def enqueue_batch(
SELECT item_id
FROM session_queue
WHERE batch_id = ?
- ORDER BY item_id DESC;
+ ORDER BY item_id ASC;
""",
(batch.batch_id,),
)
@@ -233,10 +233,9 @@ def dequeue(self) -> Optional[SessionQueueItem]:
queue_item = self._set_queue_item_status(item_id=queue_item.item_id, status="in_progress")
return queue_item
- def get_next(self, queue_id: str) -> Optional[SessionQueueItem]:
+ def get_next(self, queue_id: str, origin_prefix: Optional[str] = None) -> Optional[SessionQueueItem]:
with self._db.transaction() as cursor:
- cursor.execute(
- """--sql
+ query = """--sql
SELECT
sq.*,
u.display_name as user_display_name,
@@ -246,22 +245,28 @@ def get_next(self, queue_id: str) -> Optional[SessionQueueItem]:
WHERE
sq.queue_id = ?
AND sq.status = 'pending'
+ """
+ params = [queue_id]
+ if origin_prefix is not None:
+ query += """--sql
+ AND sq.origin LIKE ?
+ """
+ params.append(f"{origin_prefix}%")
+ query += """--sql
ORDER BY
sq.priority DESC,
sq.created_at ASC
LIMIT 1
- """,
- (queue_id,),
- )
+ """
+ cursor.execute(query, params)
result = cast(Union[sqlite3.Row, None], cursor.fetchone())
if result is None:
return None
return SessionQueueItem.queue_item_from_dict(dict(result))
- def get_current(self, queue_id: str) -> Optional[SessionQueueItem]:
+ def get_current(self, queue_id: str, origin_prefix: Optional[str] = None) -> Optional[SessionQueueItem]:
with self._db.transaction() as cursor:
- cursor.execute(
- """--sql
+ query = """--sql
SELECT
sq.*,
u.display_name as user_display_name,
@@ -271,10 +276,17 @@ def get_current(self, queue_id: str) -> Optional[SessionQueueItem]:
WHERE
sq.queue_id = ?
AND sq.status = 'in_progress'
+ """
+ params = [queue_id]
+ if origin_prefix is not None:
+ query += """--sql
+ AND sq.origin LIKE ?
+ """
+ params.append(f"{origin_prefix}%")
+ query += """--sql
LIMIT 1
- """,
- (queue_id,),
- )
+ """
+ cursor.execute(query, params)
result = cast(Union[sqlite3.Row, None], cursor.fetchone())
if result is None:
return None
@@ -832,6 +844,7 @@ def get_queue_item_ids(
queue_id: str,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
user_id: Optional[str] = None,
+ origin_prefix: Optional[str] = None,
) -> ItemIdsResult:
with self._db.transaction() as cursor_:
query = """--sql
@@ -845,6 +858,10 @@ def get_queue_item_ids(
query += " AND user_id = ?"
query_params.append(user_id)
+ if origin_prefix is not None:
+ query += " AND origin LIKE ?"
+ query_params.append(f"{origin_prefix}%")
+
query += f" ORDER BY created_at {order_dir.value}"
cursor_.execute(query, query_params)
@@ -858,20 +875,23 @@ def get_queue_status(
queue_id: str,
user_id: Optional[str] = None,
acting_user_id: Optional[str] = None,
+ origin_prefix: Optional[str] = None,
) -> SessionQueueStatus:
with self._db.transaction() as cursor:
- # Aggregate counts are always global (across all users). This lets a non-admin's
- # badge show "own / total" — their share of the whole queue — and lets the queue
- # list surface (redacted) entries belonging to other users.
- cursor.execute(
- """--sql
+ # Aggregate counts are global across all users within the requested scope.
+ query = """--sql
SELECT status, count(*)
FROM session_queue
WHERE queue_id = ?
- GROUP BY status
- """,
- (queue_id,),
- )
+ """
+ params: list[str] = [queue_id]
+
+ if origin_prefix is not None:
+ query += " AND origin LIKE ?"
+ params.append(f"{origin_prefix}%")
+
+ query += " GROUP BY status"
+ cursor.execute(query, params)
counts_result = cast(list[sqlite3.Row], cursor.fetchall())
# When user_id is provided, additionally compute that user's own counts so the
@@ -879,18 +899,24 @@ def get_queue_status(
# separate user_pending/user_in_progress fields and never replace the global counts.
user_counts_result: list[sqlite3.Row] = []
if user_id is not None:
- cursor.execute(
- """--sql
+ user_query = """--sql
SELECT status, count(*)
FROM session_queue
WHERE queue_id = ? AND user_id = ?
+ """
+ user_params = [queue_id, user_id]
+
+ if origin_prefix is not None:
+ user_query += " AND origin LIKE ?"
+ user_params.append(f"{origin_prefix}%")
+
+ user_query += """--sql
GROUP BY status
- """,
- (queue_id, user_id),
- )
+ """
+ cursor.execute(user_query, user_params)
user_counts_result = cast(list[sqlite3.Row], cursor.fetchall())
- current_item = self.get_current(queue_id=queue_id)
+ current_item = self.get_current(queue_id=queue_id, origin_prefix=origin_prefix)
total = sum(row[1] or 0 for row in counts_result)
counts: dict[str, int] = {row[0]: row[1] for row in counts_result}
diff --git a/invokeai/app/services/shared/sqlite/sqlite_util.py b/invokeai/app/services/shared/sqlite/sqlite_util.py
index 3e1d5c53f3e..14b6c61a85a 100644
--- a/invokeai/app/services/shared/sqlite/sqlite_util.py
+++ b/invokeai/app/services/shared/sqlite/sqlite_util.py
@@ -35,6 +35,7 @@
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_30 import build_migration_30
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_31 import build_migration_31
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_32 import build_migration_32
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_33 import build_migration_33
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
@@ -87,6 +88,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
migrator.register_migration(build_migration_30())
migrator.register_migration(build_migration_31())
migrator.register_migration(build_migration_32())
+ migrator.register_migration(build_migration_33())
migrator.run_migrations()
return db
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py
index 5e06d634e76..edb60d4ac2f 100644
--- a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py
@@ -37,7 +37,6 @@ def _repair_model_relationships_fks(self, cursor: sqlite3.Cursor) -> None:
# Foreign keys already point at the correct table, nothing to repair.
return
- # Rebuild the table with the correct foreign keys referencing models(id).
cursor.execute("ALTER TABLE model_relationships RENAME TO model_relationships_old;")
cursor.execute(
"""
@@ -55,7 +54,7 @@ def _repair_model_relationships_fks(self, cursor: sqlite3.Cursor) -> None:
"""
)
- # Copy over the existing links, dropping any orphaned rows whose model keys no
+ # Copy over existing links, dropping orphaned rows whose model keys no
# longer exist -- these would violate the restored foreign keys.
cursor.execute(
"""
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_33.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_33.py
new file mode 100644
index 00000000000..44d54803d1f
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_33.py
@@ -0,0 +1,63 @@
+"""Migration 33: Add the projects table for server-side workbench project persistence.
+
+Projects are the v7 workbench's primary unit of work (spec: State Ownership).
+Each row stores one user-owned project document as opaque JSON, plus a
+monotonic revision used for optimistic concurrency: clients send the revision
+they loaded, and a save against a stale revision is rejected so concurrent
+tabs/devices cannot silently overwrite each other.
+"""
+
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration33Callback:
+ """Migration to add the projects table."""
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._create_projects_table(cursor)
+
+ def _create_projects_table(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute(
+ """--sql
+ CREATE TABLE projects (
+ -- Client-generated identifier; unique per user, not globally.
+ project_id TEXT NOT NULL,
+ user_id TEXT NOT NULL,
+ name TEXT NOT NULL,
+ -- Opaque client-owned project document (JSON).
+ data TEXT NOT NULL,
+ -- Incremented on every update; used for optimistic concurrency.
+ revision INTEGER NOT NULL DEFAULT 1,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ PRIMARY KEY (user_id, project_id),
+ FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
+ );
+ """
+ )
+ cursor.execute(
+ """--sql
+ CREATE TRIGGER tg_projects_updated_at
+ AFTER UPDATE ON projects
+ FOR EACH ROW
+ BEGIN
+ UPDATE projects
+ SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE user_id = OLD.user_id AND project_id = OLD.project_id;
+ END;
+ """
+ )
+
+
+def build_migration_33() -> Migration:
+ """Builds the migration object for migrating from version 32 to version 33. This includes:
+ - Creating the `projects` table for per-user workbench project persistence.
+ - Adding a trigger to keep `updated_at` current.
+ """
+ return Migration(
+ from_version=32,
+ to_version=33,
+ callback=Migration33Callback(),
+ )
diff --git a/invokeai/backend/model_manager/load/model_cache/cache_stats.py b/invokeai/backend/model_manager/load/model_cache/cache_stats.py
index 4998ac6c77a..c339d3b7ad1 100644
--- a/invokeai/backend/model_manager/load/model_cache/cache_stats.py
+++ b/invokeai/backend/model_manager/load/model_cache/cache_stats.py
@@ -9,6 +9,7 @@ class CacheStats(object):
hits: int = 0 # cache hits
misses: int = 0 # cache misses
high_watermark: int = 0 # amount of cache used
+ cache_used: int = 0 # current amount of cache used
in_cache: int = 0 # number of models in cache
cleared: int = 0 # number of models cleared to make space
cache_size: int = 0 # total size of cache
diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache.py b/invokeai/backend/model_manager/load/model_cache/model_cache.py
index e3a0928e52b..6b5da659848 100644
--- a/invokeai/backend/model_manager/load/model_cache/model_cache.py
+++ b/invokeai/backend/model_manager/load/model_cache/model_cache.py
@@ -77,6 +77,12 @@ class CacheEntrySnapshot:
current_vram_bytes: int
+@dataclass(frozen=True)
+class CacheClearResult:
+ models_cleared: int
+ bytes_freed: int
+
+
class CacheMissCallback(Protocol):
def __call__(
self,
@@ -360,6 +366,7 @@ def put(self, key: str, model: AnyModel, execution_device: Optional[torch.device
cache_record = CacheRecord(key=key, cached_model=wrapped_model)
self._cached_models[key] = cache_record
self._cache_stack.append(key)
+ self._sync_current_stats()
self._logger.debug(
f"Added model {key} (Type: {model.__class__.__name__}, Wrap mode: {wrapped_model.__class__.__name__}, Model size: {size / MB:.2f}MB)"
)
@@ -378,6 +385,11 @@ def _get_cache_snapshot(self) -> dict[str, CacheEntrySnapshot]:
return overview
+ def _sync_current_stats(self) -> None:
+ if self.stats:
+ self.stats.cache_used = self._get_ram_in_use()
+ self.stats.in_cache = len(self._cached_models)
+
@synchronized
@record_activity
def get(self, key: str, stats_name: Optional[str] = None) -> CacheRecord:
@@ -405,6 +417,7 @@ def get(self, key: str, stats_name: Optional[str] = None) -> CacheRecord:
if self.stats:
stats_name = stats_name or key
self.stats.high_watermark = max(self.stats.high_watermark, self._get_ram_in_use())
+ self.stats.cache_used = self._get_ram_in_use()
self.stats.in_cache = len(self._cached_models)
self.stats.loaded_model_sizes[stats_name] = max(
self.stats.loaded_model_sizes.get(stats_name, 0), cache_entry.cached_model.total_bytes()
@@ -485,6 +498,7 @@ def unlock(self, cache_entry: CacheRecord) -> None:
self._delete_cache_entry(cache_entry)
if self.stats:
self.stats.cleared = (self.stats.cleared or 0) + 1
+ self._sync_current_stats()
snapshot = self._get_cache_snapshot()
for cb in self._on_cache_models_cleared_callbacks:
cb(
@@ -820,16 +834,16 @@ def _log_cache_state(self, title: str = "Model cache state:", include_entry_deta
self._logger.debug(log)
@synchronized
- def make_room(self, bytes_needed: int) -> None:
+ def make_room(self, bytes_needed: int) -> CacheClearResult:
"""Make enough room in the cache to accommodate a new model of indicated size.
Note: This function deletes all of the cache's internal references to a model in order to free it. If there are
external references to the model, there's nothing that the cache can do about it, and those models will not be
garbage-collected.
"""
- self._make_room_internal(bytes_needed)
+ return self._make_room_internal(bytes_needed)
- def _make_room_internal(self, bytes_needed: int) -> None:
+ def _make_room_internal(self, bytes_needed: int) -> CacheClearResult:
"""Internal implementation of make_room(). Assumes the lock is already held."""
self._logger.debug(f"Making room for {bytes_needed / MB:.2f}MB of RAM.")
self._log_cache_state(title="Before dropping models:")
@@ -878,9 +892,12 @@ def _make_room_internal(self, bytes_needed: int) -> None:
)
gc.collect()
+ self._sync_current_stats()
+
TorchDevice.empty_cache()
self._logger.debug(f"Dropped {models_cleared} models to free {ram_bytes_freed / MB:.2f}MB of RAM.")
self._log_cache_state(title="After dropping models:")
+ return CacheClearResult(models_cleared=models_cleared, bytes_freed=ram_bytes_freed)
def _delete_cache_entry(self, cache_entry: CacheRecord) -> None:
"""Delete cache_entry from the cache if it exists. No exception is thrown if it doesn't exist."""
@@ -918,6 +935,7 @@ def drop_model(self, model_key: str) -> int:
if dropped:
if self.stats:
self.stats.cleared = len(dropped)
+ self._sync_current_stats()
snapshot = self._get_cache_snapshot()
for cb in self._on_cache_models_cleared_callbacks:
cb(
diff --git a/invokeai/frontend/web/openapi.json b/invokeai/frontend/web/openapi.json
index 7033408b197..16642659763 100644
--- a/invokeai/frontend/web/openapi.json
+++ b/invokeai/frontend/web/openapi.json
@@ -728,6 +728,27 @@
}
}
},
+ "/api/v2/models/models_dir": {
+ "get": {
+ "tags": ["model_manager"],
+ "summary": "Get Models Dir",
+ "description": "Get the absolute path of the directory managed models are stored in.\n\nModel config `path` values are relative to this directory unless they are\nabsolute (in-place installs from outside it).",
+ "operationId": "get_models_dir",
+ "responses": {
+ "200": {
+ "description": "The absolute path of the models directory",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string",
+ "title": "Response Get Models Dir"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/v2/models/get_by_attrs": {
"get": {
"tags": ["model_manager"],
@@ -2890,6 +2911,24 @@
"title": "Access Token"
},
"description": "access token for the remote resource"
+ },
+ {
+ "name": "X-Model-Source-Access-Token",
+ "in": "header",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "access token for the remote resource",
+ "title": "X-Model-Source-Access-Token"
+ },
+ "description": "access token for the remote resource"
}
],
"requestBody": {
@@ -3777,7 +3816,9 @@
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/EmptyModelCacheResponse"
+ }
}
}
}
@@ -3805,7 +3846,12 @@
}
}
}
- }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
},
"post": {
"tags": ["model_manager"],
@@ -6531,7 +6577,12 @@
}
}
}
- }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
}
},
"/api/v1/app/external_providers/config": {
@@ -6554,7 +6605,12 @@
}
}
}
- }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
}
},
"/api/v1/app/external_providers/config/{provider_id}": {
@@ -6964,6 +7020,24 @@
"default": "DESC"
},
"description": "The order of sort"
+ },
+ {
+ "name": "origin_prefix",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Only include queue items whose origin starts with this prefix",
+ "title": "Origin Prefix"
+ },
+ "description": "Only include queue items whose origin starts with this prefix"
}
],
"responses": {
@@ -7542,6 +7616,24 @@
"title": "Queue Id"
},
"description": "The queue id to perform this operation on"
+ },
+ {
+ "name": "origin_prefix",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Only include queue items whose origin starts with this prefix",
+ "title": "Origin Prefix"
+ },
+ "description": "Only include queue items whose origin starts with this prefix"
}
],
"responses": {
@@ -7604,6 +7696,24 @@
"title": "Queue Id"
},
"description": "The queue id to perform this operation on"
+ },
+ {
+ "name": "origin_prefix",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Only include queue items whose origin starts with this prefix",
+ "title": "Origin Prefix"
+ },
+ "description": "Only include queue items whose origin starts with this prefix"
}
],
"responses": {
@@ -7666,6 +7776,24 @@
"title": "Queue Id"
},
"description": "The queue id to perform this operation on"
+ },
+ {
+ "name": "origin_prefix",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Only include queue items whose origin starts with this prefix",
+ "title": "Origin Prefix"
+ },
+ "description": "Only include queue items whose origin starts with this prefix"
}
],
"responses": {
@@ -9589,6 +9717,223 @@
}
}
},
+ "/api/v1/projects/": {
+ "get": {
+ "tags": ["projects"],
+ "summary": "List Projects",
+ "description": "Lists the current user's projects as lightweight summaries (no documents).",
+ "operationId": "list_projects",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/ProjectSummaryDTO"
+ },
+ "type": "array",
+ "title": "Response List Projects"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ },
+ "post": {
+ "tags": ["projects"],
+ "summary": "Create Project",
+ "description": "Creates a project for the current user.",
+ "operationId": "create_project",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProjectCreateRequest",
+ "description": "The project to create"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProjectRecordDTO"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/projects/{project_id}": {
+ "get": {
+ "tags": ["projects"],
+ "summary": "Get Project",
+ "description": "Gets one of the current user's projects, including its document.",
+ "operationId": "get_project",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "project_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The id of the project to get",
+ "title": "Project Id"
+ },
+ "description": "The id of the project to get"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProjectRecordDTO"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ },
+ "put": {
+ "tags": ["projects"],
+ "summary": "Update Project",
+ "description": "Saves a project. Returns 409 with the current revision when the save is based on a stale revision.",
+ "operationId": "update_project",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "project_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The id of the project to save",
+ "title": "Project Id"
+ },
+ "description": "The id of the project to save"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProjectUpdateRequest",
+ "description": "The project document and the revision it is based on"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProjectRecordDTO"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": ["projects"],
+ "summary": "Delete Project",
+ "description": "Deletes one of the current user's projects. Idempotent.",
+ "operationId": "delete_project",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "project_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The id of the project to delete",
+ "title": "Project Id"
+ },
+ "description": "The id of the project to delete"
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "Successful Response"
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/v1/recall/{queue_id}": {
"post": {
"tags": ["recall"],
@@ -14352,6 +14697,11 @@
"title": "High Watermark",
"default": 0
},
+ "cache_used": {
+ "type": "integer",
+ "title": "Cache Used",
+ "default": 0
+ },
"in_cache": {
"type": "integer",
"title": "In Cache",
@@ -21996,6 +22346,21 @@
"required": ["node_id", "field"],
"title": "EdgeConnection"
},
+ "EmptyModelCacheResponse": {
+ "properties": {
+ "models_cleared": {
+ "type": "integer",
+ "title": "Models Cleared"
+ },
+ "bytes_freed": {
+ "type": "integer",
+ "title": "Bytes Freed"
+ }
+ },
+ "type": "object",
+ "required": ["models_cleared", "bytes_freed"],
+ "title": "EmptyModelCacheResponse"
+ },
"EnqueueBatchResult": {
"properties": {
"queue_id": {
@@ -58819,6 +59184,133 @@
"title": "ProgressImage",
"type": "object"
},
+ "ProjectCreateRequest": {
+ "properties": {
+ "project_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Project Id",
+ "description": "Client-generated project id (e.g. for imports); generated when omitted"
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "The project's display name"
+ },
+ "data": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Data",
+ "description": "The opaque client-owned project document"
+ }
+ },
+ "type": "object",
+ "required": ["name", "data"],
+ "title": "ProjectCreateRequest",
+ "description": "Request body for creating a project."
+ },
+ "ProjectRecordDTO": {
+ "properties": {
+ "project_id": {
+ "type": "string",
+ "title": "Project Id",
+ "description": "The project's client-generated identifier"
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "The project's display name"
+ },
+ "revision": {
+ "type": "integer",
+ "title": "Revision",
+ "description": "Monotonic revision, incremented on every save"
+ },
+ "created_at": {
+ "type": "string",
+ "title": "Created At",
+ "description": "When the project was created"
+ },
+ "updated_at": {
+ "type": "string",
+ "title": "Updated At",
+ "description": "When the project was last saved"
+ },
+ "data": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Data",
+ "description": "The opaque client-owned project document"
+ }
+ },
+ "type": "object",
+ "required": ["project_id", "name", "revision", "created_at", "updated_at", "data"],
+ "title": "ProjectRecordDTO",
+ "description": "Full project record including the client-owned document."
+ },
+ "ProjectSummaryDTO": {
+ "properties": {
+ "project_id": {
+ "type": "string",
+ "title": "Project Id",
+ "description": "The project's client-generated identifier"
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "The project's display name"
+ },
+ "revision": {
+ "type": "integer",
+ "title": "Revision",
+ "description": "Monotonic revision, incremented on every save"
+ },
+ "created_at": {
+ "type": "string",
+ "title": "Created At",
+ "description": "When the project was created"
+ },
+ "updated_at": {
+ "type": "string",
+ "title": "Updated At",
+ "description": "When the project was last saved"
+ }
+ },
+ "type": "object",
+ "required": ["project_id", "name", "revision", "created_at", "updated_at"],
+ "title": "ProjectSummaryDTO",
+ "description": "Lightweight project listing entry; the document payload is omitted."
+ },
+ "ProjectUpdateRequest": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "The project's display name"
+ },
+ "data": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Data",
+ "description": "The opaque client-owned project document"
+ },
+ "expected_revision": {
+ "type": "integer",
+ "title": "Expected Revision",
+ "description": "The revision this save is based on; mismatch returns 409"
+ }
+ },
+ "type": "object",
+ "required": ["name", "data", "expected_revision"],
+ "title": "ProjectUpdateRequest",
+ "description": "Request body for saving a project with optimistic concurrency."
+ },
"PromptTemplateInvocation": {
"category": "prompt",
"class": "invocation",
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index 7864579706a..d946c1ba125 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -388,6 +388,29 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v2/models/models_dir": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get Models Dir
+ * @description Get the absolute path of the directory managed models are stored in.
+ *
+ * Model config `path` values are relative to this directory unless they are
+ * absolute (in-place installs from outside it).
+ */
+ get: operations["get_models_dir"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v2/models/get_by_attrs": {
parameters: {
query?: never;
@@ -2648,6 +2671,58 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/projects/": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List Projects
+ * @description Lists the current user's projects as lightweight summaries (no documents).
+ */
+ get: operations["list_projects"];
+ put?: never;
+ /**
+ * Create Project
+ * @description Creates a project for the current user.
+ */
+ post: operations["create_project"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/projects/{project_id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get Project
+ * @description Gets one of the current user's projects, including its document.
+ */
+ get: operations["get_project"];
+ /**
+ * Update Project
+ * @description Saves a project. Returns 409 with the current revision when the save is based on a stale revision.
+ */
+ put: operations["update_project"];
+ post?: never;
+ /**
+ * Delete Project
+ * @description Deletes one of the current user's projects. Idempotent.
+ */
+ delete: operations["delete_project"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/recall/{queue_id}": {
parameters: {
query?: never;
@@ -5164,6 +5239,11 @@ export type components = {
* @default 0
*/
high_watermark?: number;
+ /**
+ * Cache Used
+ * @default 0
+ */
+ cache_used?: number;
/**
* In Cache
* @default 0
@@ -9006,6 +9086,13 @@ export type components = {
*/
field: string;
};
+ /** EmptyModelCacheResponse */
+ EmptyModelCacheResponse: {
+ /** Models Cleared */
+ models_cleared: number;
+ /** Bytes Freed */
+ bytes_freed: number;
+ };
/** EnqueueBatchResult */
EnqueueBatchResult: {
/**
@@ -24845,6 +24932,121 @@ export type components = {
*/
dataURL: string;
};
+ /**
+ * ProjectCreateRequest
+ * @description Request body for creating a project.
+ */
+ ProjectCreateRequest: {
+ /**
+ * Project Id
+ * @description Client-generated project id (e.g. for imports); generated when omitted
+ */
+ project_id?: string | null;
+ /**
+ * Name
+ * @description The project's display name
+ */
+ name: string;
+ /**
+ * Data
+ * @description The opaque client-owned project document
+ */
+ data: {
+ [key: string]: unknown;
+ };
+ };
+ /**
+ * ProjectRecordDTO
+ * @description Full project record including the client-owned document.
+ */
+ ProjectRecordDTO: {
+ /**
+ * Project Id
+ * @description The project's client-generated identifier
+ */
+ project_id: string;
+ /**
+ * Name
+ * @description The project's display name
+ */
+ name: string;
+ /**
+ * Revision
+ * @description Monotonic revision, incremented on every save
+ */
+ revision: number;
+ /**
+ * Created At
+ * @description When the project was created
+ */
+ created_at: string;
+ /**
+ * Updated At
+ * @description When the project was last saved
+ */
+ updated_at: string;
+ /**
+ * Data
+ * @description The opaque client-owned project document
+ */
+ data: {
+ [key: string]: unknown;
+ };
+ };
+ /**
+ * ProjectSummaryDTO
+ * @description Lightweight project listing entry; the document payload is omitted.
+ */
+ ProjectSummaryDTO: {
+ /**
+ * Project Id
+ * @description The project's client-generated identifier
+ */
+ project_id: string;
+ /**
+ * Name
+ * @description The project's display name
+ */
+ name: string;
+ /**
+ * Revision
+ * @description Monotonic revision, incremented on every save
+ */
+ revision: number;
+ /**
+ * Created At
+ * @description When the project was created
+ */
+ created_at: string;
+ /**
+ * Updated At
+ * @description When the project was last saved
+ */
+ updated_at: string;
+ };
+ /**
+ * ProjectUpdateRequest
+ * @description Request body for saving a project with optimistic concurrency.
+ */
+ ProjectUpdateRequest: {
+ /**
+ * Name
+ * @description The project's display name
+ */
+ name: string;
+ /**
+ * Data
+ * @description The opaque client-owned project document
+ */
+ data: {
+ [key: string]: unknown;
+ };
+ /**
+ * Expected Revision
+ * @description The revision this save is based on; mismatch returns 409
+ */
+ expected_revision: number;
+ };
/**
* Prompt Template
* @description Applies a Style Preset template to positive and negative prompts.
@@ -33601,6 +33803,26 @@ export interface operations {
};
};
};
+ get_models_dir: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The absolute path of the models directory */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": string;
+ };
+ };
+ };
+ };
get_model_records_by_attrs: {
parameters: {
query: {
@@ -34215,7 +34437,10 @@ export interface operations {
/** @description access token for the remote resource */
access_token?: string | null;
};
- header?: never;
+ header?: {
+ /** @description access token for the remote resource */
+ "X-Model-Source-Access-Token"?: string | null;
+ };
path?: never;
cookie?: never;
};
@@ -34712,7 +34937,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
+ "application/json": components["schemas"]["EmptyModelCacheResponse"];
};
};
};
@@ -36836,6 +37061,8 @@ export interface operations {
query?: {
/** @description The order of sort */
order_dir?: components["schemas"]["SQLiteDirection"];
+ /** @description Only include queue items whose origin starts with this prefix */
+ origin_prefix?: string | null;
};
header?: never;
path: {
@@ -37203,7 +37430,10 @@ export interface operations {
};
get_current_queue_item: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Only include queue items whose origin starts with this prefix */
+ origin_prefix?: string | null;
+ };
header?: never;
path: {
/** @description The queue id to perform this operation on */
@@ -37235,7 +37465,10 @@ export interface operations {
};
get_next_queue_item: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Only include queue items whose origin starts with this prefix */
+ origin_prefix?: string | null;
+ };
header?: never;
path: {
/** @description The queue id to perform this operation on */
@@ -37267,7 +37500,10 @@ export interface operations {
};
get_queue_status: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Only include queue items whose origin starts with this prefix */
+ origin_prefix?: string | null;
+ };
header?: never;
path: {
/** @description The queue id to perform this operation on */
@@ -38417,6 +38653,157 @@ export interface operations {
};
};
};
+ list_projects: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ProjectSummaryDTO"][];
+ };
+ };
+ };
+ };
+ create_project: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ProjectCreateRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ProjectRecordDTO"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ get_project: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id of the project to get */
+ project_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ProjectRecordDTO"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ update_project: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id of the project to save */
+ project_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ProjectUpdateRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ProjectRecordDTO"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ delete_project: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id of the project to delete */
+ project_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
get_recall_parameters: {
parameters: {
query?: never;
diff --git a/invokeai/frontend/webv2/.gitignore b/invokeai/frontend/webv2/.gitignore
new file mode 100644
index 00000000000..22f1644ccfa
--- /dev/null
+++ b/invokeai/frontend/webv2/.gitignore
@@ -0,0 +1,3 @@
+dist/
+node_modules/
+stats.html
diff --git a/invokeai/frontend/webv2/.oxfmtrc.json b/invokeai/frontend/webv2/.oxfmtrc.json
new file mode 100644
index 00000000000..a261d816261
--- /dev/null
+++ b/invokeai/frontend/webv2/.oxfmtrc.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
+ "arrowParens": "always",
+ "bracketSameLine": false,
+ "bracketSpacing": true,
+ "endOfLine": "lf",
+ "ignorePatterns": ["dist/**", "node_modules/**", "stats.html"],
+ "printWidth": 120,
+ "semi": true,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "sortImports": {
+ "groups": [
+ "type-import",
+ ["value-builtin", "value-external"],
+ ["type-internal", "value-internal"],
+ ["type-parent", "type-sibling", "type-index"],
+ ["value-parent", "value-sibling", "value-index"],
+ "unknown"
+ ]
+ },
+ "sortPackageJson": {
+ "sortScripts": true
+ }
+}
diff --git a/invokeai/frontend/webv2/.oxlintrc.json b/invokeai/frontend/webv2/.oxlintrc.json
new file mode 100644
index 00000000000..aba3b3877a9
--- /dev/null
+++ b/invokeai/frontend/webv2/.oxlintrc.json
@@ -0,0 +1,137 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["typescript", "react", "import", "unicorn", "oxc", "react-perf", "eslint"],
+ "categories": {
+ "correctness": "error"
+ },
+ "rules": {
+ "curly": "error",
+ "eqeqeq": "error",
+
+ "import/no-cycle": "error",
+ "import/no-duplicates": "error",
+ "import/no-relative-parent-imports": "error",
+ "import/no-absolute-path": "error",
+ "import/no-amd": "error",
+ "import/no-commonjs": "error",
+ "import/no-mutable-exports": "error",
+
+ "no-console": "warn",
+ "no-eval": "error",
+ "no-extend-native": "error",
+ "no-implied-eval": "error",
+ "no-label-var": "error",
+ "no-promise-executor-return": "error",
+ "no-return-assign": "error",
+ "no-sequences": "error",
+ "no-template-curly-in-string": "error",
+ "no-throw-literal": "error",
+ "no-unmodified-loop-condition": "error",
+ "no-var": "error",
+
+ "prefer-template": "error",
+ "radix": "error",
+ "no-restricted-exports": "error",
+
+ // "eslint/complexity": [
+ // "error",
+ // {
+ // "max": 3
+ // }
+ // ],
+
+ "react/jsx-curly-brace-presence": [
+ "error",
+ {
+ "props": "never",
+ "children": "never"
+ }
+ ],
+ "react/no-children-prop": "error",
+ "react/no-danger": "error",
+ "react/no-unknown-property": "error",
+ "react/jsx-no-useless-fragment": "error",
+ "react/exhaustive-deps": "error",
+ "react/react-compiler": "error",
+
+ "react-hooks/exhaustive-deps": "error",
+ "react-hooks/rules-of-hooks": "error",
+
+ "react-perf/jsx-no-new-object-as-prop": [
+ "error",
+ {
+ "nativeAllowList": "all"
+ }
+ ],
+ "react-perf/jsx-no-new-function-as-prop": [
+ "error",
+ {
+ "nativeAllowList": "all"
+ }
+ ],
+ "react-perf/jsx-no-new-array-as-prop": [
+ "error",
+ {
+ "nativeAllowList": "all"
+ }
+ ],
+ "react-perf/jsx-no-jsx-as-prop": [
+ "error",
+ {
+ "nativeAllowList": "all"
+ }
+ ],
+
+ "require-await": "error",
+
+ "typescript/consistent-type-imports": [
+ "error",
+ {
+ "fixStyle": "separate-type-imports",
+ "prefer": "type-imports"
+ }
+ ],
+ "typescript/no-empty-interface": [
+ "error",
+ {
+ "allowSingleExtends": true
+ }
+ ],
+ "typescript/consistent-type-assertions": [
+ "error",
+ {
+ "assertionStyle": "as"
+ }
+ ],
+ "typescript/ban-ts-comment": "error",
+ "typescript/no-import-type-side-effects": "error",
+
+ "unicorn/no-abusive-eslint-disable": "error",
+ "unicorn/error-message": "error",
+ "unicorn/throw-new-error": "error",
+ "unicorn/prefer-type-error": "error",
+
+ "vitest/require-mock-type-parameters": "off"
+ },
+ "env": {
+ "browser": true,
+ "builtin": true,
+ "es2022": true,
+ "node": true
+ },
+ "globals": {
+ "GlobalCompositeOperation": "readonly",
+ "RequestInit": "readonly"
+ },
+ "ignorePatterns": [
+ "dist/**",
+ "node_modules/**",
+ "static/**",
+ ".husky/**",
+ "patches/**",
+ "stats.html",
+ "index.html",
+ ".yarn/**",
+ "**/*.scss"
+ ]
+}
diff --git a/invokeai/frontend/webv2/OXC_RULE_COMPATIBILITY.md b/invokeai/frontend/webv2/OXC_RULE_COMPATIBILITY.md
new file mode 100644
index 00000000000..2af36ab0b91
--- /dev/null
+++ b/invokeai/frontend/webv2/OXC_RULE_COMPATIBILITY.md
@@ -0,0 +1,23 @@
+# OXC Rule Compatibility
+
+`webv2` uses `oxlint` and `oxfmt` instead of ESLint and Prettier.
+
+The config mirrors the existing Invoke web lint intent where OXC supports equivalent rules:
+
+- React and hooks checks: `react/jsx-no-bind`, `react/jsx-curly-brace-presence`, `react-hooks/*`.
+- TypeScript checks: `typescript/consistent-type-imports`, `typescript/no-empty-interface`, plus TypeScript compile checks through `tsc --noEmit`.
+- Import checks: `import/no-duplicates`, `import/no-cycle`.
+- General correctness/style checks: `curly`, `no-var`, `prefer-template`, `radix`, `eqeqeq`, `no-eval`, `no-extend-native`, `no-implied-eval`, `no-label-var`, `no-return-assign`, `no-sequences`, `no-template-curly-in-string`, `no-throw-literal`, `no-unmodified-loop-condition`, `no-console`, `no-promise-executor-return`, and `require-await`.
+- Formatting: `oxfmt` uses the same print width, tab width, semicolon, single quote, and trailing comma preferences as the existing web formatter. It uses `lf` line endings because `oxfmt` does not support Prettier's `auto` value.
+
+Known unsupported or intentionally deferred equivalents:
+
+- `brace-style`, `one-var`, and `react/jsx-no-bind`: oxlint 1.69.0 does not expose these ESLint/plugin rule names.
+- `simple-import-sort/*`: oxlint does not currently provide the same configurable import sorting behavior.
+- `unused-imports/no-unused-imports`: oxlint reports unused bindings, but it is not the same plugin rule.
+- `@typescript-eslint/ban-ts-comment`: no exact oxlint equivalent is configured yet.
+- `@typescript-eslint/no-import-type-side-effects`: no exact oxlint equivalent is configured yet.
+- `@typescript-eslint/consistent-type-assertions`: no exact oxlint equivalent is configured yet.
+- `path/no-relative-imports`: no exact oxlint equivalent is configured yet.
+- `no-restricted-syntax`, `no-restricted-properties`, `no-restricted-imports`: no exact oxlint config equivalent is configured yet.
+- `i18next/no-literal-string` and Storybook-specific overrides: not configured for the initial `webv2` shell.
diff --git a/invokeai/frontend/webv2/index.html b/invokeai/frontend/webv2/index.html
new file mode 100644
index 00000000000..d5a772e46db
--- /dev/null
+++ b/invokeai/frontend/webv2/index.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+ Invoke V7 Workbench
+
+
+
+
+
+
+
diff --git a/invokeai/frontend/webv2/package.json b/invokeai/frontend/webv2/package.json
new file mode 100644
index 00000000000..4bc3b578967
--- /dev/null
+++ b/invokeai/frontend/webv2/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@invoke-ai/invoke-ai-webv2",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "pnpm run lint && vite build",
+ "dev": "vite dev",
+ "dev:host": "vite dev --host",
+ "fix": "pnpm run lint:oxc:fix && pnpm run format:write",
+ "format:check": "oxfmt --check .",
+ "format:write": "oxfmt --write .",
+ "lint": "pnpm run format:check && pnpm run lint:oxc && pnpm run lint:tsc",
+ "lint:oxc": "oxlint --react-plugin --import-plugin --deny-warnings .",
+ "lint:oxc:fix": "oxlint --react-plugin --import-plugin --fix .",
+ "lint:tsc": "tsc --noEmit",
+ "preview": "vite preview",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "@chakra-ui/react": "^3.36.0",
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/modifiers": "^9.0.0",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "@emotion/react": "^11.14.0",
+ "@fontsource/inter": "^5.2.8",
+ "@tanstack/react-router": "^1.170.16",
+ "@tanstack/react-virtual": "^3.14.3",
+ "@xyflow/react": "^12.11.1",
+ "lucide-react": "^1.21.0",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "react-hook-tanstack-virtual": "^0.0.4",
+ "react-icons": "^5.6.0",
+ "socket.io-client": "^4.8.3",
+ "tinykeys": "^4.0.0",
+ "use-sync-external-store": "^1.6.0",
+ "zod": "^4.4.3"
+ },
+ "devDependencies": {
+ "@rolldown/plugin-babel": "^0.2.3",
+ "@types/node": "^26.0.1",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@types/use-sync-external-store": "^1.5.0",
+ "@vitejs/plugin-react": "^6.0.3",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxfmt": "^0.56.0",
+ "oxlint": "^1.71.0",
+ "typescript": "^6.0.3",
+ "vite": "^8.1.0",
+ "vitest": "^4.1.9"
+ },
+ "engines": {
+ "pnpm": "10"
+ },
+ "packageManager": "pnpm@10.12.4"
+}
diff --git a/invokeai/frontend/webv2/pnpm-lock.yaml b/invokeai/frontend/webv2/pnpm-lock.yaml
new file mode 100644
index 00000000000..16860f2e0c1
--- /dev/null
+++ b/invokeai/frontend/webv2/pnpm-lock.yaml
@@ -0,0 +1,3701 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@chakra-ui/react':
+ specifier: ^3.36.0
+ version: 3.36.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@dnd-kit/core':
+ specifier: ^6.3.1
+ version: 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@dnd-kit/modifiers':
+ specifier: ^9.0.0
+ version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
+ '@dnd-kit/sortable':
+ specifier: ^10.0.0
+ version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
+ '@dnd-kit/utilities':
+ specifier: ^3.2.2
+ version: 3.2.2(react@19.2.7)
+ '@emotion/react':
+ specifier: ^11.14.0
+ version: 11.14.0(@types/react@19.2.17)(react@19.2.7)
+ '@fontsource/inter':
+ specifier: ^5.2.8
+ version: 5.2.8
+ '@tanstack/react-router':
+ specifier: ^1.170.16
+ version: 1.170.16(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@tanstack/react-virtual':
+ specifier: ^3.14.3
+ version: 3.14.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@xyflow/react':
+ specifier: ^12.11.1
+ version: 12.11.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ lucide-react:
+ specifier: ^1.21.0
+ version: 1.21.0(react@19.2.7)
+ react:
+ specifier: ^19.2.7
+ version: 19.2.7
+ react-dom:
+ specifier: ^19.2.7
+ version: 19.2.7(react@19.2.7)
+ react-hook-tanstack-virtual:
+ specifier: ^0.0.4
+ version: 0.0.4(@tanstack/react-virtual@3.14.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@tanstack/virtual-core@3.17.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react-icons:
+ specifier: ^5.6.0
+ version: 5.6.0(react@19.2.7)
+ socket.io-client:
+ specifier: ^4.8.3
+ version: 4.8.3
+ tinykeys:
+ specifier: ^4.0.0
+ version: 4.0.0
+ use-sync-external-store:
+ specifier: ^1.6.0
+ version: 1.6.0(react@19.2.7)
+ zod:
+ specifier: ^4.4.3
+ version: 4.4.3
+ devDependencies:
+ '@rolldown/plugin-babel':
+ specifier: ^0.2.3
+ version: 0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.1.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))
+ '@types/node':
+ specifier: ^26.0.1
+ version: 26.0.1
+ '@types/react':
+ specifier: ^19.2.17
+ version: 19.2.17
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.17)
+ '@types/use-sync-external-store':
+ specifier: ^1.5.0
+ version: 1.5.0
+ '@vitejs/plugin-react':
+ specifier: ^6.0.3
+ version: 6.0.3(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.1.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))
+ babel-plugin-react-compiler:
+ specifier: ^1.0.0
+ version: 1.0.0
+ oxfmt:
+ specifier: ^0.56.0
+ version: 0.56.0
+ oxlint:
+ specifier: ^1.71.0
+ version: 1.71.0
+ typescript:
+ specifier: ^6.0.3
+ version: 6.0.3
+ vite:
+ specifier: ^8.1.0
+ version: 8.1.0(@types/node@26.0.1)(esbuild@0.27.7)
+ vitest:
+ specifier: ^4.1.9
+ version: 4.1.9(@types/node@26.0.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))
+
+packages:
+
+ '@ark-ui/react@5.37.2':
+ resolution: {integrity: sha512-Q0R2Ah50kUhup0Ljxg65zGJq5yBV52BLm1coRkjHHid40d1yclaDGfhPL48kcF/xtjAFlGLkL6SiENkGvfh+mw==}
+ peerDependencies:
+ react: '>=18.0.0'
+ react-dom: '>=18.0.0'
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.7':
+ resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.7':
+ resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.7':
+ resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.7':
+ resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/runtime@7.29.7':
+ resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.7':
+ resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.7':
+ resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
+ engines: {node: '>=6.9.0'}
+
+ '@chakra-ui/react@3.36.0':
+ resolution: {integrity: sha512-6AxUbJsC6yyTzPeYL8sxyAL07lflT0NA+S6tcPzEuwdMux+benRMFOpPktnkifWKl/Vq/JD7fhxDyMuDQ4M0gA==}
+ peerDependencies:
+ '@emotion/react': '>=11'
+ react: '>=18'
+ react-dom: '>=18'
+
+ '@dnd-kit/accessibility@3.1.1':
+ resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
+ peerDependencies:
+ react: '>=16.8.0'
+
+ '@dnd-kit/core@6.3.1':
+ resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@dnd-kit/modifiers@9.0.0':
+ resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==}
+ peerDependencies:
+ '@dnd-kit/core': ^6.3.0
+ react: '>=16.8.0'
+
+ '@dnd-kit/sortable@10.0.0':
+ resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
+ peerDependencies:
+ '@dnd-kit/core': ^6.3.0
+ react: '>=16.8.0'
+
+ '@dnd-kit/utilities@3.2.2':
+ resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
+ peerDependencies:
+ react: '>=16.8.0'
+
+ '@emnapi/core@1.11.1':
+ resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
+
+ '@emnapi/runtime@1.11.1':
+ resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
+
+ '@emnapi/wasi-threads@1.2.2':
+ resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
+
+ '@emotion/babel-plugin@11.13.5':
+ resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
+
+ '@emotion/cache@11.14.0':
+ resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==}
+
+ '@emotion/hash@0.9.2':
+ resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
+
+ '@emotion/is-prop-valid@1.4.0':
+ resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==}
+
+ '@emotion/memoize@0.9.0':
+ resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==}
+
+ '@emotion/react@11.14.0':
+ resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: '>=16.8.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@emotion/serialize@1.3.3':
+ resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==}
+
+ '@emotion/sheet@1.4.0':
+ resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==}
+
+ '@emotion/unitless@0.10.0':
+ resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==}
+
+ '@emotion/use-insertion-effect-with-fallbacks@1.2.0':
+ resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==}
+ peerDependencies:
+ react: '>=16.8.0'
+
+ '@emotion/utils@1.4.2':
+ resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==}
+
+ '@emotion/weak-memoize@0.4.0':
+ resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==}
+
+ '@esbuild/aix-ppc64@0.27.7':
+ resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.27.7':
+ resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.27.7':
+ resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.27.7':
+ resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.27.7':
+ resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.27.7':
+ resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.27.7':
+ resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.27.7':
+ resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.27.7':
+ resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.27.7':
+ resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.27.7':
+ resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.27.7':
+ resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.27.7':
+ resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.27.7':
+ resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.27.7':
+ resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.27.7':
+ resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.27.7':
+ resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.27.7':
+ resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.27.7':
+ resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.27.7':
+ resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.27.7':
+ resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.27.7':
+ resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.27.7':
+ resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.27.7':
+ resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.27.7':
+ resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.27.7':
+ resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@floating-ui/core@1.7.5':
+ resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
+
+ '@floating-ui/dom@1.7.6':
+ resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
+
+ '@floating-ui/utils@0.2.11':
+ resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+
+ '@fontsource/inter@5.2.8':
+ resolution: {integrity: sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==}
+
+ '@internationalized/date@3.12.2':
+ resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==}
+
+ '@internationalized/number@3.6.6':
+ resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/wasm-runtime@1.1.6':
+ resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@oxc-project/types@0.137.0':
+ resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==}
+
+ '@oxfmt/binding-android-arm-eabi@0.56.0':
+ resolution: {integrity: sha512-CSCxi7ovYojgfdPOdUb9T508HKeAdDIKeRGg7x8IZwVJrWz9gVgX7MbUnFqtQAE4QvoNo07mj2JlwnOzJw4qqA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@oxfmt/binding-android-arm64@0.56.0':
+ resolution: {integrity: sha512-HYJFnd+PkDwf6S9ZPGzXXtjNqvRWFnnhdbWaouh4mi/SxU8wmDuzlMn3xo/wDTGnr4Q1VA7ZzOaE/D4biW0W6A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxfmt/binding-darwin-arm64@0.56.0':
+ resolution: {integrity: sha512-sftR/bEOr+t1gs+evwsHi/Xbq2FAPA2uU3VMr8n6ZU9PoK/IMSfnfu7+OEe/uy1+knhrFl4Wvy7Vkm3uo9mJ7g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxfmt/binding-darwin-x64@0.56.0':
+ resolution: {integrity: sha512-z66SdjLqa3MUPKvTp3Mbb5nSjKSbnYxJGeB+Wx987s8T5hPcIRiBMfnJ6zcPgYtQn3x5xjvdzNVkXrSeYH6ZFg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxfmt/binding-freebsd-x64@0.56.0':
+ resolution: {integrity: sha512-t2tkrV1vtZyaItSQ71dTi2ZVKZEI39b/LqLT12V5KMfIeXK6N32TUC1jhOXKVQmhECq9j2ZXMQV3JeT1kh9Vmg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxfmt/binding-linux-arm-gnueabihf@0.56.0':
+ resolution: {integrity: sha512-+gCy+Tp3RHeXQ9y/QrS76lXIpZkbziTyp6hIgjB2MssCwfMph3vG/GEfkhO34Rai1vhYIaUkvv8UT1BcDorJPw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxfmt/binding-linux-arm-musleabihf@0.56.0':
+ resolution: {integrity: sha512-0kKkVvQ2I+FJ2sxQyUu1zJ0yWP5kcWse/yVFnGQSFCXMwSSkfEaUGu0dW774O7nyy3jrcBGap7OSc8dZmU/CdA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxfmt/binding-linux-arm64-gnu@0.56.0':
+ resolution: {integrity: sha512-npkA2siMbyWRh+wEhi1aTAx4RirukGcGNt8V4Ch86pG+xU9aurqS1MZOnKYMu03ISwat3rB6zkQx51SsB9obNw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-arm64-musl@0.56.0':
+ resolution: {integrity: sha512-UekqOjGkV4/MkqreCV9SPIB2jlR3/HbXrmhV1rVXJZ9wfDXMyCMriLtq3tHqLY4PkbVWNtfcm1kMojJ26KLSJw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-ppc64-gnu@0.56.0':
+ resolution: {integrity: sha512-XSzveSpeZMD5XJpew5lRFVtNnT04xd3rJxENXmk7wkZzN9oWzv2aFJyoNDhOtoz69BYaS/fg4SYl+CfEZRpB0Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-riscv64-gnu@0.56.0':
+ resolution: {integrity: sha512-EkQ0nJa7k7HDDIVuPF7WY+k4k+bzdclLYtyIXNt7/OqVghfNiMym6YGppFBgx1XRIHW6QylxBz5OogumPjPJbQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-riscv64-musl@0.56.0':
+ resolution: {integrity: sha512-dyjAGW8jKRge0ik6U/dgvQG0nVpA3iBlRskQTz5qJLvQWIrySxX5jpqzPetLBNIIZ231KA82fDdi1nLTk8ENCw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-s390x-gnu@0.56.0':
+ resolution: {integrity: sha512-60ZGH3LtfqlW8X6vcLdSFY4lvCQYINurttYBKaALnHCDVAUCYJ1LsUgS6p1XOzVlzEDx3yNUZvDF1Lvt59zoZw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@oxfmt/binding-linux-x64-gnu@0.56.0':
+ resolution: {integrity: sha512-u1suj1tgJHK4ZqB7buCtdbNef2n8+d0lXTPJwLHNmtyK6p+DTpsaoDvmqhQrA56fgKYv4LuRxNtL8YooebKOew==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxfmt/binding-linux-x64-musl@0.56.0':
+ resolution: {integrity: sha512-aYGLvlQHt80y+qKEtfJY/Nm27G0125Lv+qyh9SJ4Cjc6lXnXjD+ndfhqQnbV24POpMi7rNRi0jvx/0d70FRpCQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxfmt/binding-openharmony-arm64@0.56.0':
+ resolution: {integrity: sha512-H/re/gO+7ysVc+kywHNuzY3C33EN9sQcZhg0kp1ZwOZl7y998ZE5mhnBiuGR/nYI0pqLL5xQfrHVUOJ/cIJsCA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxfmt/binding-win32-arm64-msvc@0.56.0':
+ resolution: {integrity: sha512-6qLNXfXmtAs8jXDvYMkxk6Wec5SUJoew+ZX1uOZmqaR7ks0EJFbAohuOCELDyJMWyVlxotVG8Xf8m74Bfq0O2w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxfmt/binding-win32-ia32-msvc@0.56.0':
+ resolution: {integrity: sha512-UXEXuKphAe15bsob4AswNMArCw38XSmUIs3wk1s6e6MX9OWGW/IRWU95s1hZDiVg09STy1jHgyN2qkqbu1FT0w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@oxfmt/binding-win32-x64-msvc@0.56.0':
+ resolution: {integrity: sha512-HPyNDjky+NIOuaMvHZflR+kst3YWdUOH2JUQYkf99grqZ5mEBTQM6h9iGy501Z8Xt5xMScrwHOuVCOlqDrktRw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@oxlint/binding-android-arm-eabi@1.71.0':
+ resolution: {integrity: sha512-ImGmd1njEg4FEJH03jhRnveEegtO3czCtfptvaHivKAZQIYATbVFBrrzbaYMYv0oJioTnxZAZVSyV+oL7W8S2g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@oxlint/binding-android-arm64@1.71.0':
+ resolution: {integrity: sha512-4A5BEexBrwY1YFF8Kiq/lp/wQPRG79G3BWIE1FuWaM5MvmpYSd+7ZySVcKkHdwo0UDzdQGddp6pD9mpctMqLnw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxlint/binding-darwin-arm64@1.71.0':
+ resolution: {integrity: sha512-9wJA9GJulLwS2usU3CEisI/ESDO1n1z9eyTCvApMDrAkbJ1ve0mORgTMjcWWsKxkzkeZ2N/Gpra5IQE7x8tYgQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxlint/binding-darwin-x64@1.71.0':
+ resolution: {integrity: sha512-PlLCjS06V0PeJMAJwzjrExw1sYNW9Gch3JtNlcwwZDXGlTYDuwHNN89zYH8LTXFfgkVtsYvs2nv0FqrzyuFDzg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxlint/binding-freebsd-x64@1.71.0':
+ resolution: {integrity: sha512-Lhil7bWre0ncxbUoDoxfS0JzpTz17BRQKW7iwoAUY8GJ66+WwJEfYPCFJ1P0WgVZR5/O/b3Q2pENlHOjeXLOGQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.71.0':
+ resolution: {integrity: sha512-Oo9/L58PYD3RC0x05d2upAPLllHytTjHQGsnC06P6Ynn7jKkp5mdImQxXdJ3+FnBaKspNpGogzgVsi6g872LiA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm-musleabihf@1.71.0':
+ resolution: {integrity: sha512-mSHfyfgJrEbyIR29ejaeS50BdPk+GoNPlC1dckpDiUZbJAIel68sjSMdOt4WY0/gva+ECC7FNITQkxMJU+vSBw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm64-gnu@1.71.0':
+ resolution: {integrity: sha512-n9yY4M2tiy3aij4AqtlnspzpfdpeT5JQfK2/w2d8oyp5W0FRwOb1dIeX99nORNcxGr08iD9bH8N5XFz3I2iy8w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm64-musl@1.71.0':
+ resolution: {integrity: sha512-fJZrs5sDZtTaPIOiemRQQmo82Ezy+vOGXemPc4Ok7iVVsYsFa7SlW6Z5XN819VfsqBHRm3NJ3rTdnR8+bJYJdQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxlint/binding-linux-ppc64-gnu@1.71.0':
+ resolution: {integrity: sha512-cwl7VKGERIy9p+G+AvZdfy/06q0aHXaTt/mMRReC751iuNYJgqKjB7NydXSS30nBT9vtr2tunciOtrR4fD6FUA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@oxlint/binding-linux-riscv64-gnu@1.71.0':
+ resolution: {integrity: sha512-eZ8ieVXvzGi8jr7+ybQGPK2STw3mldfxZlgA2738iflfB/rzA69sE6m5rDRpQaxC7dpm745Enlh1Tod0QAk9Gg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxlint/binding-linux-riscv64-musl@1.71.0':
+ resolution: {integrity: sha512-puMDbQYe6+NXwfMusojoA7CXGn2b3utukmd23PQqc1E3XhVCwyZ+FueSMzDYeNgDV2dUfIVXAAKZBcFDeCL6sA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxlint/binding-linux-s390x-gnu@1.71.0':
+ resolution: {integrity: sha512-4NJLxBs1ujISCt3L/1FcywLs73PWtJuw+piD6feK2V6h6OS6P7xu9/sWt1DTRLibe6QCzmfZzmM/2HPORoV/Lg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@oxlint/binding-linux-x64-gnu@1.71.0':
+ resolution: {integrity: sha512-cFDaiR8L3430qp88tfZnvFlt3KotFhR/DlbIL0nHOMMYiG/9Wy4l+6f7t8G8pTa9bd8Lt8+M0y/qjRQ/xcB74g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxlint/binding-linux-x64-musl@1.71.0':
+ resolution: {integrity: sha512-orfixdt76KlpNly9z0PkWBBNfwjKz+JFVLP/7wnVchlKNU9Dpt9InU/ZggeSej6fC7qwHmHNOGlhLnQXcYoGuA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxlint/binding-openharmony-arm64@1.71.0':
+ resolution: {integrity: sha512-9emQu2lAp6yhPB3XuI+++vR+l/o6JR1X+EpxwcumPdQXBWXEPAsquPGL7l158EqU8SebQMXTUa/S5zN98juyHw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxlint/binding-win32-arm64-msvc@1.71.0':
+ resolution: {integrity: sha512-bd5kI8spYwTm3BILDtGhi73zoup5dw8MlPQNT8YB3BD5UIsjNe3K9/4ctrzQMX4SZMoK5HgzVLkLJzacEXB7fA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxlint/binding-win32-ia32-msvc@1.71.0':
+ resolution: {integrity: sha512-W4HvOHGzVLHcrmFu+bMrJlho+/yrlX5ZNdJZqGe8MEldkQG+RHYhxxad9P4jvWAYFmIqUA5i9DQ8QsJqSU9GIw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@oxlint/binding-win32-x64-msvc@1.71.0':
+ resolution: {integrity: sha512-D2kyEIPHk/G/wiZLnwTVC/sVst+T/lKldVOjAFpgTIBUAOlry72e5OiapDbDBF4LfJLkN5ypJb/8Eu6yJzkveQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@pandacss/is-valid-prop@1.11.3':
+ resolution: {integrity: sha512-YaHK+p5DaN8AUpsRx5OqqGxaZzn8uNIdVhP+K1cjvjv3+Qa9D/75/A1dPyLmfKrSRJc8UoR9WN9fxQX0rVzhzQ==}
+ engines: {node: '>=20'}
+
+ '@rolldown/binding-android-arm64@1.1.3':
+ resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.1.3':
+ resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.1.3':
+ resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.1.3':
+ resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.3':
+ resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.3':
+ resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-musl@1.1.3':
+ resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.3':
+ resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.3':
+ resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-gnu@1.1.3':
+ resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-musl@1.1.3':
+ resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-openharmony-arm64@1.1.3':
+ resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.1.3':
+ resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.3':
+ resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.1.3':
+ resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/plugin-babel@0.2.3':
+ resolution: {integrity: sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==}
+ engines: {node: '>=22.12.0 || ^24.0.0'}
+ peerDependencies:
+ '@babel/core': ^7.29.0 || ^8.0.0-rc.1
+ '@babel/plugin-transform-runtime': ^7.29.0 || ^8.0.0-rc.1
+ '@babel/runtime': ^7.27.0 || ^8.0.0-rc.1
+ rolldown: ^1.0.0-rc.5
+ vite: ^8.0.0
+ peerDependenciesMeta:
+ '@babel/plugin-transform-runtime':
+ optional: true
+ '@babel/runtime':
+ optional: true
+ vite:
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@socket.io/component-emitter@3.1.2':
+ resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@swc/helpers@0.5.23':
+ resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
+
+ '@tanstack/history@1.162.0':
+ resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==}
+ engines: {node: '>=20.19'}
+
+ '@tanstack/react-router@1.170.16':
+ resolution: {integrity: sha512-w6eq1IJklujs1tESazaK/FxH0+H2l8vm/QPuu1cD3oRW/ubgKneQpd7b64ti/8gUyEimzimJQZDmJr6YHfP5+g==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ react: '>=18.0.0 || >=19.0.0'
+ react-dom: '>=18.0.0 || >=19.0.0'
+
+ '@tanstack/react-store@0.9.3':
+ resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ '@tanstack/react-virtual@3.14.3':
+ resolution: {integrity: sha512-k/cnHPVaOfn46hSbiY6n4Dzf4QjCGWSF40zR5QIIYUqPAjpA6TN7InfYmcMiDVQGP2iUn9xsRbAl8u1v3UmeVQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ '@tanstack/router-core@1.171.13':
+ resolution: {integrity: sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA==}
+ engines: {node: '>=20.19'}
+
+ '@tanstack/store@0.9.3':
+ resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==}
+
+ '@tanstack/virtual-core@3.17.1':
+ resolution: {integrity: sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA==}
+
+ '@tybys/wasm-util@0.10.3':
+ resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-drag@3.0.7':
+ resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-selection@3.0.11':
+ resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
+
+ '@types/d3-transition@3.0.9':
+ resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
+
+ '@types/d3-zoom@3.0.8':
+ resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/node@26.0.1':
+ resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==}
+
+ '@types/parse-json@4.0.2':
+ resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
+
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.17':
+ resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
+
+ '@types/use-sync-external-store@1.5.0':
+ resolution: {integrity: sha512-5dyB8nLC/qogMrlCizZnYWQTA4lnb/v+It+sqNl5YnSRAPMlIqY/X0Xn+gZw8vOL+TgTTr28VEbn3uf8fUtAkw==}
+
+ '@vitejs/plugin-react@6.0.3':
+ resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
+ babel-plugin-react-compiler: ^1.0.0
+ vite: ^8.0.0
+ peerDependenciesMeta:
+ '@rolldown/plugin-babel':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+
+ '@vitest/expect@4.1.9':
+ resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==}
+
+ '@vitest/mocker@4.1.9':
+ resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.1.9':
+ resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==}
+
+ '@vitest/runner@4.1.9':
+ resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==}
+
+ '@vitest/snapshot@4.1.9':
+ resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==}
+
+ '@vitest/spy@4.1.9':
+ resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==}
+
+ '@vitest/utils@4.1.9':
+ resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==}
+
+ '@xyflow/react@12.11.1':
+ resolution: {integrity: sha512-L+zBoLGSXham0MnlY8QqjfR7/C5JNw0zxkaey5aZ5XmCgJBAdH4+WRIu8CR40d3l/BdU635V6YbhBK1jMo8/6Q==}
+ peerDependencies:
+ '@types/react': '>=17'
+ '@types/react-dom': '>=17'
+ react: '>=17'
+ react-dom: '>=17'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@xyflow/system@0.0.78':
+ resolution: {integrity: sha512-lY0z2qP33fUhTva9Vaxrk0lqZta2pkbxB1trHAx1omnJqRtPvDlAQYV2r5fhS6AdpkulYmbNW0svy+A4/t4B/g==}
+
+ '@zag-js/accordion@1.41.2':
+ resolution: {integrity: sha512-7G//V7svGGT8k5avw7bbQvbRC0Q/9QtX51b4iyAB1alR9E5mFd6Ch8q4njwcXClMQ7xePS3jUfVnzVGiRInEiQ==}
+
+ '@zag-js/anatomy@1.41.2':
+ resolution: {integrity: sha512-Fm9hqdrvaCzCsdcf19G8WZxYtHElKltkGHdhqMEt4XU+ULTr1DK7KbOtDDv9J27CuzqSLALUz5QfRjPftoKHwg==}
+
+ '@zag-js/angle-slider@1.41.2':
+ resolution: {integrity: sha512-+7bZHAZx0MEbjTMr2tD+meFJ0EJwFfUEcTqmdLzFGr/ySAMCWlcadDBz+ZmrSn03aKLps8FxliVLzsFJNgUqIQ==}
+
+ '@zag-js/aria-hidden@1.41.2':
+ resolution: {integrity: sha512-qEcYmwlQr3qjA0T/IZ5a/o7fRUxfQ14tXjAFhR3GXCtxBKaqS+wnq/LN09Xw4bin3QWTINU+Z0oXFs9spWtNwA==}
+
+ '@zag-js/async-list@1.41.2':
+ resolution: {integrity: sha512-NZZEIGFdeDp2uHjsLVegLAGJOYGwI9HPJI1V2c/P1TQmfmrfWyWELAvnnW4kWYVUKYD9TxKQkm6LvqpHrQzgfQ==}
+
+ '@zag-js/auto-resize@1.41.2':
+ resolution: {integrity: sha512-CYq+JQ1TTkEiK7OcNcMTS0f4wFvtmManvUltije5o50gDQ7vFYA81oQh4A9pAB1keUi9Zv06CNefFARaTcne5w==}
+
+ '@zag-js/avatar@1.41.2':
+ resolution: {integrity: sha512-+4K0tRtIQysMGuERh5JRmn46uq7gJ6IjJd5DKj74VBtVVY8T6HGk0D0DZxmOIgADHP1qSvowLDoOX8kUyBHarQ==}
+
+ '@zag-js/carousel@1.41.2':
+ resolution: {integrity: sha512-H6sDxyWIzkvtHN4M/BYvFy7pi8j+kLEtfPZvgNl2+gRnfAHVK9/XqpoUsjw1DAo5Fh25pPuaTnh52Kml9OK0iA==}
+
+ '@zag-js/cascade-select@1.41.2':
+ resolution: {integrity: sha512-dBByZmIAJU/B+YIAzczng05lCJXEwEm4df6GmUZtioKHZdeN+WEKUP4qzFDFZdytXympGfJCIBvtgf87gACYVQ==}
+
+ '@zag-js/checkbox@1.41.2':
+ resolution: {integrity: sha512-aQ5dZWHUHRIw4cbLtzrR+dc8M0N6wSiiCml/zbU3ciHOTBXSrs2rKnp/L99xhi97Lj2uCEhpIZ704Ou/MjGuCg==}
+
+ '@zag-js/clipboard@1.41.2':
+ resolution: {integrity: sha512-FM+PNGEeY+YpF1dVr9d8kg3DQM66LRnkR4Mva1oprqnrCXTYWj+k7uig893ubSG2xw1GO5b/WJd27kSmNoKnmw==}
+
+ '@zag-js/collapsible@1.41.2':
+ resolution: {integrity: sha512-uMIp4rqd3iI6VFPMAOcbu9dh9WV4l85nNPYQiBtMKRHgbkfaZdfzF+E+NX3KquIeHW6JiBFUiIyCU0Sf2Cscdw==}
+
+ '@zag-js/collection@1.41.2':
+ resolution: {integrity: sha512-ZZWuvfPZI8ccWd4aLpuU47k1jSc9eO+F3FM3iBJuvgegCH6g3+HDEwGN6wdePHnYfv7zyIKCGKr816zXcIWQbw==}
+
+ '@zag-js/color-picker@1.41.2':
+ resolution: {integrity: sha512-UynyJ/bTBeSlq6ziHr9GVrFycDjVxfLFIb8Aivu/XvihrAAvkhXUxwy+1ax+hj7peGlTY590xoQAvQixV+McBA==}
+
+ '@zag-js/color-utils@1.41.2':
+ resolution: {integrity: sha512-Lsi5c2ztGZqud0oeDtAn3xFhgrYMaeaz1zi4p+mr0zXOuQNuZzfsrJ0stQ45s8L/xT8UJtGhYyEVy1+xjE4ARA==}
+
+ '@zag-js/combobox@1.41.2':
+ resolution: {integrity: sha512-V9jQteyQHs8ZNQx/FcA98P1NVZas/yjiW1V7s4L+h8MnRS3AK97+FAHAT83KCiZ34/vkpLF6ZEHI2zkcQpNXcg==}
+
+ '@zag-js/core@1.41.2':
+ resolution: {integrity: sha512-xXTN3zKwOtMI4+5dG2cG+T1B4WR3X9alXiYaPJKaGd4N2eYRj9JEPte3Hv9gtFm+RM0b9VIwksHEg25rqE4apw==}
+
+ '@zag-js/date-input@1.41.2':
+ resolution: {integrity: sha512-J8PdpEpM9TBxZBEE8/Nxi39LNJOZY7lilTbgcDABR5uiviZUk5++5GSdUTspcjFUIXx5SvqmdCk2RF7hsUEHVQ==}
+ peerDependencies:
+ '@internationalized/date': '>=3.0.0'
+
+ '@zag-js/date-picker@1.41.2':
+ resolution: {integrity: sha512-j9LaznCV4QCfrq/y/mNAN9XgIRFFg+414TxGT4TK8mfWouIaFvxEoKdGP0l/LL4DFv1NRDaRdSBBcw3eXtMVnA==}
+ peerDependencies:
+ '@internationalized/date': '>=3.0.0'
+
+ '@zag-js/date-utils@1.41.2':
+ resolution: {integrity: sha512-jdcWLa5fYLTsvGWJkx4v/9Hzqf6UHdvJDeP6NxHpwoEmzYy7+AghYKBNSnRrJIBCQJgl1vM9OkpMvYrLNHr9jw==}
+ peerDependencies:
+ '@internationalized/date': '>=3.0.0'
+
+ '@zag-js/dialog@1.41.2':
+ resolution: {integrity: sha512-t1N72snpFGiKj7cg2PivPdsy+X1YdgXPv7bzovpqjMLy0kN+gYTPoV1Iy/hQA+g021dz9XGgXzkDuU45sJqsFA==}
+
+ '@zag-js/dismissable@1.41.2':
+ resolution: {integrity: sha512-hO/tFhRZ7S+LOOljGOQJIubbc3MXg41+iWR1yUXKl76cAenbxaCit1LZmUCwQPvRN0GndK6bDQo5ETjHZz/k7A==}
+
+ '@zag-js/dom-query@1.41.2':
+ resolution: {integrity: sha512-+eBk1nlJA312mNmY/GSThLRwcCRqMIL+A1pLsWvTlQLQjmH1/UxoAuv6l2yvRCT33XmC8FBlBIKnXhOCpDvIZA==}
+
+ '@zag-js/drawer@1.41.2':
+ resolution: {integrity: sha512-aJql6L0cfHd1wXbemfLcNnjqTA/CbwVgQdWEVn5Qci6zhy4UJXPQeBBfxfgPgEOAbW60gL/gaaXG3d9Vx6+6oA==}
+
+ '@zag-js/editable@1.41.2':
+ resolution: {integrity: sha512-loTM1lrHBBqYfR8SJZrYayVdWHMpXCLVir+aDTQ8/d4bb/Gfl/L8CJNs3BRj3yt9zvLoWTLO+4LOY3hLyQSR2w==}
+
+ '@zag-js/file-upload@1.41.2':
+ resolution: {integrity: sha512-WFwIaKvHpCUjyYMvp42VoydT1WIP5DhDlpmG/nrF4i0ro7pLGb1A4dvyBrAfGcozCB88yR1y1iZKPDY1I9/uUw==}
+
+ '@zag-js/file-utils@1.41.2':
+ resolution: {integrity: sha512-Ih+8ULbId0M+CFR4IsqG5y/0VLCk2l+1rgPH+21L40dlSB6z6qKSP2tG7W69Cj2/3vryZsn67ibn26iCPG/vOw==}
+
+ '@zag-js/floating-panel@1.41.2':
+ resolution: {integrity: sha512-nJP3oZ4YrJh+7H5YdwNccSzPGXFqjQN1ujZ/xGDhegjz4XtL48QMFnAasxlRI8VGjse9Tj8VXlQZxXPrASGzlA==}
+
+ '@zag-js/focus-trap@1.41.2':
+ resolution: {integrity: sha512-3QTtGUjFU2OLbyrDlyoYWvKZecCmtn/+bsfsHW159jJHiEGHVYK6CY8AI1ePsMk9gVay48bXh008j+lVli7gAw==}
+
+ '@zag-js/focus-visible@1.41.2':
+ resolution: {integrity: sha512-oRjwtgafUdGVwLJUN6mKsnBQbez/CHYAPkPg1FxOnr5GFpEpr8oMTOZJ3wdPM2U1ynS9QnUUu/sXc3KQv/jX0Q==}
+
+ '@zag-js/highlight-word@1.41.2':
+ resolution: {integrity: sha512-95zcZKqNrL7JlqAckfzHa+LRbnfoz1lj6skUhq/uHSnni1vH6+8fNWT8ruo9G7vpGopbyRmmcie5p41SbAwQjQ==}
+
+ '@zag-js/hover-card@1.41.2':
+ resolution: {integrity: sha512-Xn9RVzgTkKaVzyJTDdBJiXmCleNi1/hmW8z73tC0vOiQvSSvPejg/JkzqTOLFODvQHK3NOw54QHZvmjYp9Mubw==}
+
+ '@zag-js/i18n-utils@1.41.2':
+ resolution: {integrity: sha512-f1xqaEY79awBxgUyjFso0UEpIoEHZq+zRvB0nUVFHJRW7Ds/QhaFHKSRnf2QBTlP/ObvCT225R+piNAAc17Aaw==}
+
+ '@zag-js/image-cropper@1.41.2':
+ resolution: {integrity: sha512-750aT4U+J/TJw/Z1QVoLU9JG0luCtf9CyvShtJFIxeS+i25aUoBI9pOKgSFABsOutIqNJTPq4gYpqtuxFjSxRw==}
+
+ '@zag-js/interact-outside@1.41.2':
+ resolution: {integrity: sha512-dM4Fn9iyqQeqkCMRYZP+bAgWEPKRVQRqMmcPsN0OXBhhFKC31Em15LTIZXaOtVKAjH+iwx+UvSYFRiWwEjkOEA==}
+
+ '@zag-js/json-tree-utils@1.41.2':
+ resolution: {integrity: sha512-gNaOzsbCwmTd2HM3/u0xQdWX5UDBfl8tCXFavzbamkFH0iYQOXJb7cqUXBVuI4KScIbHPCKwrzZjqA5Sg9qzAQ==}
+
+ '@zag-js/listbox@1.41.2':
+ resolution: {integrity: sha512-iDGrZleP3ui2Q6Jgmr9RYlbd7njdJHs8Qb3IJrSIZBIeYyYmFvVUfAFjk0g/z/amjTx6uYxRASWSPy/RETd4ug==}
+
+ '@zag-js/live-region@1.41.2':
+ resolution: {integrity: sha512-7ubIW5AQt1wx9S/gFN+rU4TyvuFWJrL/DhnDWPNlH5g3luDVHSNeYGeeqf4d4tkcibtpYZa0pg9CbXxRxrfwVA==}
+
+ '@zag-js/marquee@1.41.2':
+ resolution: {integrity: sha512-cT77aMhrtAK3oe2O6+X3TLtQs8wupdPUtZgHMWVxCcQOwagrk7RUBEQwU3Cx7cTswpBdTKSuDYgUggfgCs96wg==}
+
+ '@zag-js/menu@1.41.2':
+ resolution: {integrity: sha512-wwix8hcAUSi0scpWXCiDppfdZV01Za8nN0gqLt9GdhCiVSlr0rs9pK1ROgPKJTyc43UZfyFPqtTWVHvEHMM70w==}
+
+ '@zag-js/navigation-menu@1.41.2':
+ resolution: {integrity: sha512-aROB9CHzskZpnoFuGFkp7dbkZdsXvp2nxQgsgld02I1sDqiwcQd+YMdB0/6Ik0oz6X8I70RKdUuQM9WQFQfDnA==}
+
+ '@zag-js/number-input@1.41.2':
+ resolution: {integrity: sha512-QoQWCiVHO+ciUbq8uL8Kxhtk4o3UpwzYpJkfsOfitzsZoYpPc7V/A6+n5yABV2SOwpqBODwNASZdxiEa60kfow==}
+
+ '@zag-js/pagination@1.41.2':
+ resolution: {integrity: sha512-vZTxz4DrIDfedMcTjDeEkOc1iXD9wkP8eKuEcDieHOodnjSnNNwtmoFw5DCWv+yEa/TByIamXBZ8ZxHY144JgA==}
+
+ '@zag-js/password-input@1.41.2':
+ resolution: {integrity: sha512-OWKFl0S12Qnlf4R4WbMCJ/YU0kGfezm5tP0UiyubMO/Fixv6H0twDFaJSPg2F6POv5uomCcGubc1H7gO+fIhsQ==}
+
+ '@zag-js/pin-input@1.41.2':
+ resolution: {integrity: sha512-Wrn3YDbmWL3qvUIzN4QyLO7PzEhunX7z8DwBpmrK04p7HxR9pniTLVIPk27xk2MzyAPYcl4mwd9/Mc88tBxHsg==}
+
+ '@zag-js/popover@1.41.2':
+ resolution: {integrity: sha512-h/LlVMIERM+NWzYV0ZHURlJuaqkT8XxwyEV96XfVzjknDRFNoPSl5IttVYtoaPoUqC0p/Y4oTSiee4mUZKOLHw==}
+
+ '@zag-js/popper@1.41.2':
+ resolution: {integrity: sha512-Iz4D5YAIiIPn4IHGjhX3QatR/RyGaDt43lBSZv0RYcPQYtFg9sUuek3wizjW9qXgdvItevvNMqRdpl7f3es09g==}
+
+ '@zag-js/presence@1.41.2':
+ resolution: {integrity: sha512-OhOLPAf/DYPmgoEntrlrf3LOrkwA+Y0J3K03NXHXPnkRB7h8jyIbHqzHS0jRTb1pvsO2P/yowRyoYtptY2Zmog==}
+
+ '@zag-js/progress@1.41.2':
+ resolution: {integrity: sha512-SnzrqN+Z568NoO4rrgjrIc/S4EXMEne5CDgWwt2kQ8yq9VysdH8TtHAjyciFRIio7cdgEZNHKw9jSccpMWgRwA==}
+
+ '@zag-js/qr-code@1.41.2':
+ resolution: {integrity: sha512-+JLswCNnzf58aQTaX0SMUA9wRC8Hb/a/ZveJIXTz6853Siemay2HqOqB2WQIeF33HNldUFD/a4+4Q8ugg93ubA==}
+
+ '@zag-js/radio-group@1.41.2':
+ resolution: {integrity: sha512-EZjos61jKHZlNw8ez80vG2T9gUwpooxcVpYOKS2hyhuEXn3wEoefS5v1WFbmpoA/8TUpUQnYxisAeNuQfEQCuQ==}
+
+ '@zag-js/rating-group@1.41.2':
+ resolution: {integrity: sha512-SbJP4HiK5XRy/oC3xRowjZOCThq1n/QA2Z/XAkvKLJArVQCYjrH3MoWwWvMVNfNC5+ZJluborR2AGF7tlVLzxA==}
+
+ '@zag-js/react@1.41.2':
+ resolution: {integrity: sha512-5Bx7mQAron4LFWI8Hhs/uw5kwQ19s2Tn30HhctozLqmCu4nnJSTSh7GRvX+uwRZnztGXBXoOrgBWIepU4RXFGg==}
+ peerDependencies:
+ react: '>=18.0.0'
+ react-dom: '>=18.0.0'
+
+ '@zag-js/rect-utils@1.41.2':
+ resolution: {integrity: sha512-GWBTamaMLMG1p7Fe6V0dsXeTEmk+tqG3ciovzmjxURCJ3Yq2EkAMRhS0v5DG0oo+PyrPEIzEWukGBQkh/XRgYQ==}
+
+ '@zag-js/remove-scroll@1.41.2':
+ resolution: {integrity: sha512-ieIrOgPKlCikAGEBIboQJoU7oxrL5BEY670tDOu7Eg7rNOdAwGXLEKPX2A/q+lREhOiVYjx0D3//vHTvdte80Q==}
+
+ '@zag-js/scroll-area@1.41.2':
+ resolution: {integrity: sha512-hJFAwfIFuS7XmNsS3nB5rPm9OWXfB4d98sID7fjurcaZtDH5LqFriqfXhceNvs7rz7K4f/u8rufXrb6tcvATIA==}
+
+ '@zag-js/scroll-snap@1.41.2':
+ resolution: {integrity: sha512-+70Al6LSASyEZtFyyffUJlDbE88KgYJDud05z8oZTqyEOLlTqnlSNkXq/P3siO7r3sNykg9H+TmAKn+/dVSKuw==}
+
+ '@zag-js/select@1.41.2':
+ resolution: {integrity: sha512-3wGaKABILexoNBJ1bJiHqLLTctR/VMZaNA4cyKiqZeBEWtAkdMhgyY2xKobrP6KtUTqAeUFNVSTu4yCDrAQnUw==}
+
+ '@zag-js/signature-pad@1.41.2':
+ resolution: {integrity: sha512-iNrOxY4gtqhsZdXYvlhF7s+LiOvwV7/kBpNnq8tJ1oYhgTs8wvKtJHrP86/CR05irEXQTaLfmeAJZuBEys9iRA==}
+
+ '@zag-js/slider@1.41.2':
+ resolution: {integrity: sha512-mKK2BwoDbIGxAdkdKkPZJA1SHtEQt3lS9hJ6WghefYU2vyd0BXoIKvcDV3xJOzly5LXYhH5cJITn6JtGK8353A==}
+
+ '@zag-js/splitter@1.41.2':
+ resolution: {integrity: sha512-Ubp4hkmzvVysU31jCINYbBXVqruu7rEPGqugWMzeXC5Hwda648aHpbg4Jix/wRtaGLjsyh6KOVEwAmoJU9NwhQ==}
+
+ '@zag-js/steps@1.41.2':
+ resolution: {integrity: sha512-m0t2r8+FWwa2b2aU5JiNrHVdYHyNZYHK0G3Tq9lCOSQoDeoJIkyta+sIVehLVSY+0Ba9kOlkRUmYLbsnfXaW+Q==}
+
+ '@zag-js/store@1.41.2':
+ resolution: {integrity: sha512-dVZF7E1ezXzynrKhMH3rfSr2rBbCfvTjvXbXz7//1PNULuq58UU5dG93V+9l834npCZxI2+PrpY45wZLJPTsIA==}
+
+ '@zag-js/switch@1.41.2':
+ resolution: {integrity: sha512-qHbQK95UUHN0tj+bf9LLphLcMo/uTg2Pvs0c1Gs03Zh4g3NtHf0uYIhMZKYlCH0hNVlKrmdzLKWgeoDxv9gySw==}
+
+ '@zag-js/tabs@1.41.2':
+ resolution: {integrity: sha512-7YVj2mCcxRbn1wMXP9anaTOVf+J0fa5uaPScr8e4+e2xc+/1WKzqN6V8IDeKS5wV/xzi1r3Ny307sX7Xz4ZJVQ==}
+
+ '@zag-js/tags-input@1.41.2':
+ resolution: {integrity: sha512-aIPEndSO+9LHxyoXLUr0Uttxw2cYMyDuii5w50wn/N7lFJD49U3Sj+6XaB/oJSbDAwn10WrsRDtb7Gs2kmU+Qw==}
+
+ '@zag-js/timer@1.41.2':
+ resolution: {integrity: sha512-PRYLWaip0+1FeVGEMNk5wMGAAIYgBIWwulZ4U5I+2Ayjohzp2NUAfwJ3sqoYvRrfjNYUNAPdU4eGu1zetC+oVQ==}
+
+ '@zag-js/toast@1.41.2':
+ resolution: {integrity: sha512-+F3PsAo6EIz4rh73IOMCb/+FOUp7e3VjUY2q5sdU4IbfOzJBIbVSJxn0PQmHkuxkzWdCom0Lv0qSPFg/UTplnQ==}
+
+ '@zag-js/toggle-group@1.41.2':
+ resolution: {integrity: sha512-C6wn3A89h24hTs0BN9ryEuKatfR493u7QqxS06TeK9oI/KZBvm5Rwm8FPHSUJvsUTkAkow4PsXC0Ra19duzEdQ==}
+
+ '@zag-js/toggle@1.41.2':
+ resolution: {integrity: sha512-EFB9pb3pEtwXt7RSivVLWXV64dKUF8gsn75tt8TbYBgfy+zW85MsRLu8U48TXZN5teQWAKKNujr9bxQsW/CWvQ==}
+
+ '@zag-js/tooltip@1.41.2':
+ resolution: {integrity: sha512-68okWJCFXfW8r0h97kEcU2yKPkq6e0S6QkiYh09ifMXoYjrQw/shPol8PgrS1poqKJigUWtsKm+bw73abhMn3w==}
+
+ '@zag-js/tour@1.41.2':
+ resolution: {integrity: sha512-Q7UsvuHYYBo1Cs4b4OS0e/D8lxd2GpSRII93s5BQPi5HTcBjaWhVysAykmCFbat6Z56z0NBmFHNdhZ8oVPls1A==}
+
+ '@zag-js/tree-view@1.41.2':
+ resolution: {integrity: sha512-QNi0VpV+RyzF4NP72+kSleUpauF9SMAzOAe59nxvs8jAUHqV5diDCInnbQos4+cyFDXFzGq3lElot26A1yI+7Q==}
+
+ '@zag-js/types@1.41.2':
+ resolution: {integrity: sha512-L6CNvK06lIVpy0X8eG3kbDIx8Uuv+3KHElxXYSzRXSJ7/OLCv1sTRgEvnxNtdIWOrksGgxF4JtT7PXtoClGqNQ==}
+
+ '@zag-js/utils@1.41.2':
+ resolution: {integrity: sha512-Yj8FSrR7vGA6ahUhjrThfHAF+PM2Y1Yv2lkXkqZZd60mPBhixcot1+SHOfEMV63JimQcWrmQ8QbeYYMmF+ZpLQ==}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ babel-plugin-macros@3.1.0:
+ resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
+ engines: {node: '>=10', npm: '>=6'}
+
+ babel-plugin-react-compiler@1.0.0:
+ resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
+
+ baseline-browser-mapping@2.10.40:
+ resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ browserslist@4.28.4:
+ resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001799:
+ resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==}
+
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
+
+ classcat@5.0.5:
+ resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
+
+ convert-source-map@1.9.0:
+ resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cookie-es@3.1.1:
+ resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==}
+
+ cosmiconfig@7.1.0:
+ resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
+ engines: {node: '>=10'}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-dispatch@3.0.1:
+ resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
+ engines: {node: '>=12'}
+
+ d3-drag@3.0.0:
+ resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
+ engines: {node: '>=12'}
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-selection@3.0.0:
+ resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
+ d3-transition@3.0.1:
+ resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ d3-selection: 2 - 3
+
+ d3-zoom@3.0.0:
+ resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
+ engines: {node: '>=12'}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ electron-to-chromium@1.5.379:
+ resolution: {integrity: sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==}
+
+ engine.io-client@6.6.6:
+ resolution: {integrity: sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==}
+
+ engine.io-parser@5.2.3:
+ resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
+ engines: {node: '>=10.0.0'}
+
+ error-ex@1.3.4:
+ resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-module-lexer@2.1.0:
+ resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
+
+ esbuild@0.27.7:
+ resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
+ engines: {node: '>=12.0.0'}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ find-root@1.1.0:
+ resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ hasown@2.0.4:
+ resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
+ engines: {node: '>= 0.4'}
+
+ hoist-non-react-statics@3.3.2:
+ resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
+ is-core-module@2.16.2:
+ resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==}
+ engines: {node: '>= 0.4'}
+
+ isbot@5.1.44:
+ resolution: {integrity: sha512-PGEHtwMnKbZpeSEXW2Utx+/JWed7dp6DiH0WWg33vGSDA7RUvpUeJSVlLrVkQ1RCpvDOUc/eH9ql7VsdbBZ8pA==}
+ engines: {node: '>=18'}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ lucide-react@1.21.0:
+ resolution: {integrity: sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.15:
+ resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ node-releases@2.0.50:
+ resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==}
+ engines: {node: '>=18'}
+
+ obug@2.1.3:
+ resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
+ engines: {node: '>=12.20.0'}
+
+ oxfmt@0.56.0:
+ resolution: {integrity: sha512-9Dv0wV3zKiyvhjD7bRKaInKmHQ1sPx3RGOjQkGFJbbdQ16576yf8qhMSO9Q9cvHcs+1NpBsRTkuDDYFFPTJ6gw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ svelte: ^5.0.0
+ vite-plus: '*'
+ peerDependenciesMeta:
+ svelte:
+ optional: true
+ vite-plus:
+ optional: true
+
+ oxlint@1.71.0:
+ resolution: {integrity: sha512-U1m1X+C0vDj7DC1e13IoZULzEcPczE7UOMTs8VlZGHUEIUaSTZKo5qkPsQEfzpgnQ29Pea/w3Xntk62UCecxZw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ oxlint-tsgolint: '>=0.22.1'
+ vite-plus: '*'
+ peerDependenciesMeta:
+ oxlint-tsgolint:
+ optional: true
+ vite-plus:
+ optional: true
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+ path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ perfect-freehand@1.2.3:
+ resolution: {integrity: sha512-bHZSfqDHGNlPpgH2yxXgPHlQSPpEbo+qg7li0M78J9vNAi2yjwLeA4x79BEQhX44lEWpCLSFCeRZwpw0niiXPA==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.15:
+ resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ proxy-compare@3.0.1:
+ resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==}
+
+ proxy-memoize@3.0.1:
+ resolution: {integrity: sha512-VDdG/VYtOgdGkWJx7y0o7p+zArSf2383Isci8C+BP3YXgMYDoPd3cCBjw0JdWb6YBb9sFiOPbAADDVTPJnh+9g==}
+
+ react-dom@19.2.7:
+ resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==}
+ peerDependencies:
+ react: ^19.2.7
+
+ react-hook-tanstack-virtual@0.0.4:
+ resolution: {integrity: sha512-N6OnfdklUygY6K9Ik7SGZlAlc3uVOVm8kp3OP7d4m+0ELGgABm4W/f8kJPcda8CkgQUkFvDO8t8lOzbEsdJG3w==}
+ peerDependencies:
+ '@tanstack/react-virtual': ^3.14.2
+ '@tanstack/virtual-core': ^3.17.0
+ react: ^19.2.0
+ react-dom: ^19.2.0
+
+ react-icons@5.6.0:
+ resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==}
+ peerDependencies:
+ react: '*'
+
+ react-is@16.13.1:
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
+ react@19.2.7:
+ resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve@1.22.12:
+ resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
+ rolldown@1.1.3:
+ resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ seroval-plugins@1.5.4:
+ resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ seroval: ^1.0
+
+ seroval@1.5.4:
+ resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==}
+ engines: {node: '>=10'}
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ socket.io-client@4.8.3:
+ resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==}
+ engines: {node: '>=10.0.0'}
+
+ socket.io-parser@4.2.6:
+ resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==}
+ engines: {node: '>=10.0.0'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.5.7:
+ resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
+ engines: {node: '>=0.10.0'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@4.1.0:
+ resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
+
+ stylis@4.2.0:
+ resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
+
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@1.2.4:
+ resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tinykeys@4.0.0:
+ resolution: {integrity: sha512-z5bQRQHL1PSx3lGOZEAMM2oKp0EBtn5T5f7HJnaKPOzk6vEH0wUPvdHLeQ7F4tmkek8AgoTQISUFmn/5SNF2xA==}
+ engines: {node: '>=22'}
+
+ tinypool@2.1.0:
+ resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
+ engines: {node: ^20.0.0 || >=22.0.0}
+
+ tinyrainbow@3.1.0:
+ resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
+ engines: {node: '>=14.0.0'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@8.3.0:
+ resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uqr@0.1.3:
+ resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==}
+
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ vite@8.1.0:
+ resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.3.0
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitest@4.1.9:
+ resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.1.9
+ '@vitest/browser-preview': 4.1.9
+ '@vitest/browser-webdriverio': 4.1.9
+ '@vitest/coverage-istanbul': 4.1.9
+ '@vitest/coverage-v8': 4.1.9
+ '@vitest/ui': 4.1.9
+ happy-dom: '*'
+ jsdom: '*'
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/coverage-istanbul':
+ optional: true
+ '@vitest/coverage-v8':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ ws@8.21.0:
+ resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ xmlhttprequest-ssl@2.1.2:
+ resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
+ engines: {node: '>=0.4.0'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yaml@1.10.3:
+ resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==}
+ engines: {node: '>= 6'}
+
+ zod@4.4.3:
+ resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+
+ zustand@4.5.7:
+ resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ '@types/react': '>=16.8'
+ immer: '>=9.0.6'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+
+snapshots:
+
+ '@ark-ui/react@5.37.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@internationalized/date': 3.12.2
+ '@zag-js/accordion': 1.41.2
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/angle-slider': 1.41.2
+ '@zag-js/async-list': 1.41.2
+ '@zag-js/auto-resize': 1.41.2
+ '@zag-js/avatar': 1.41.2
+ '@zag-js/carousel': 1.41.2
+ '@zag-js/cascade-select': 1.41.2
+ '@zag-js/checkbox': 1.41.2
+ '@zag-js/clipboard': 1.41.2
+ '@zag-js/collapsible': 1.41.2
+ '@zag-js/collection': 1.41.2
+ '@zag-js/color-picker': 1.41.2
+ '@zag-js/color-utils': 1.41.2
+ '@zag-js/combobox': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/date-input': 1.41.2(@internationalized/date@3.12.2)
+ '@zag-js/date-picker': 1.41.2(@internationalized/date@3.12.2)
+ '@zag-js/date-utils': 1.41.2(@internationalized/date@3.12.2)
+ '@zag-js/dialog': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/drawer': 1.41.2
+ '@zag-js/editable': 1.41.2
+ '@zag-js/file-upload': 1.41.2
+ '@zag-js/file-utils': 1.41.2
+ '@zag-js/floating-panel': 1.41.2
+ '@zag-js/focus-trap': 1.41.2
+ '@zag-js/focus-visible': 1.41.2
+ '@zag-js/highlight-word': 1.41.2
+ '@zag-js/hover-card': 1.41.2
+ '@zag-js/i18n-utils': 1.41.2
+ '@zag-js/image-cropper': 1.41.2
+ '@zag-js/json-tree-utils': 1.41.2
+ '@zag-js/listbox': 1.41.2
+ '@zag-js/marquee': 1.41.2
+ '@zag-js/menu': 1.41.2
+ '@zag-js/navigation-menu': 1.41.2
+ '@zag-js/number-input': 1.41.2
+ '@zag-js/pagination': 1.41.2
+ '@zag-js/password-input': 1.41.2
+ '@zag-js/pin-input': 1.41.2
+ '@zag-js/popover': 1.41.2
+ '@zag-js/presence': 1.41.2
+ '@zag-js/progress': 1.41.2
+ '@zag-js/qr-code': 1.41.2
+ '@zag-js/radio-group': 1.41.2
+ '@zag-js/rating-group': 1.41.2
+ '@zag-js/react': 1.41.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@zag-js/scroll-area': 1.41.2
+ '@zag-js/select': 1.41.2
+ '@zag-js/signature-pad': 1.41.2
+ '@zag-js/slider': 1.41.2
+ '@zag-js/splitter': 1.41.2
+ '@zag-js/steps': 1.41.2
+ '@zag-js/switch': 1.41.2
+ '@zag-js/tabs': 1.41.2
+ '@zag-js/tags-input': 1.41.2
+ '@zag-js/timer': 1.41.2
+ '@zag-js/toast': 1.41.2
+ '@zag-js/toggle': 1.41.2
+ '@zag-js/toggle-group': 1.41.2
+ '@zag-js/tooltip': 1.41.2
+ '@zag-js/tour': 1.41.2
+ '@zag-js/tree-view': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.7': {}
+
+ '@babel/core@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+ '@babel/helpers': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.7':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.29.7':
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.28.4
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.29.7': {}
+
+ '@babel/helper-module-imports@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-option@7.29.7': {}
+
+ '@babel/helpers@7.29.7':
+ dependencies:
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@babel/parser@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@babel/runtime@7.29.7': {}
+
+ '@babel/template@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@babel/traverse@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.7':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@chakra-ui/react@3.36.0(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@ark-ui/react': 5.37.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@emotion/is-prop-valid': 1.4.0
+ '@emotion/react': 11.14.0(@types/react@19.2.17)(react@19.2.7)
+ '@emotion/serialize': 1.3.3
+ '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.7)
+ '@emotion/utils': 1.4.2
+ '@pandacss/is-valid-prop': 1.11.3
+ csstype: 3.2.3
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
+ '@dnd-kit/accessibility@3.1.1(react@19.2.7)':
+ dependencies:
+ react: 19.2.7
+ tslib: 2.8.1
+
+ '@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@dnd-kit/accessibility': 3.1.1(react@19.2.7)
+ '@dnd-kit/utilities': 3.2.2(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ tslib: 2.8.1
+
+ '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@dnd-kit/core': 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@dnd-kit/utilities': 3.2.2(react@19.2.7)
+ react: 19.2.7
+ tslib: 2.8.1
+
+ '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@dnd-kit/core': 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@dnd-kit/utilities': 3.2.2(react@19.2.7)
+ react: 19.2.7
+ tslib: 2.8.1
+
+ '@dnd-kit/utilities@3.2.2(react@19.2.7)':
+ dependencies:
+ react: 19.2.7
+ tslib: 2.8.1
+
+ '@emnapi/core@1.11.1':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.2
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.11.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emotion/babel-plugin@11.13.5':
+ dependencies:
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/runtime': 7.29.7
+ '@emotion/hash': 0.9.2
+ '@emotion/memoize': 0.9.0
+ '@emotion/serialize': 1.3.3
+ babel-plugin-macros: 3.1.0
+ convert-source-map: 1.9.0
+ escape-string-regexp: 4.0.0
+ find-root: 1.1.0
+ source-map: 0.5.7
+ stylis: 4.2.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@emotion/cache@11.14.0':
+ dependencies:
+ '@emotion/memoize': 0.9.0
+ '@emotion/sheet': 1.4.0
+ '@emotion/utils': 1.4.2
+ '@emotion/weak-memoize': 0.4.0
+ stylis: 4.2.0
+
+ '@emotion/hash@0.9.2': {}
+
+ '@emotion/is-prop-valid@1.4.0':
+ dependencies:
+ '@emotion/memoize': 0.9.0
+
+ '@emotion/memoize@0.9.0': {}
+
+ '@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ '@babel/runtime': 7.29.7
+ '@emotion/babel-plugin': 11.13.5
+ '@emotion/cache': 11.14.0
+ '@emotion/serialize': 1.3.3
+ '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.7)
+ '@emotion/utils': 1.4.2
+ '@emotion/weak-memoize': 0.4.0
+ hoist-non-react-statics: 3.3.2
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+ transitivePeerDependencies:
+ - supports-color
+
+ '@emotion/serialize@1.3.3':
+ dependencies:
+ '@emotion/hash': 0.9.2
+ '@emotion/memoize': 0.9.0
+ '@emotion/unitless': 0.10.0
+ '@emotion/utils': 1.4.2
+ csstype: 3.2.3
+
+ '@emotion/sheet@1.4.0': {}
+
+ '@emotion/unitless@0.10.0': {}
+
+ '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.7)':
+ dependencies:
+ react: 19.2.7
+
+ '@emotion/utils@1.4.2': {}
+
+ '@emotion/weak-memoize@0.4.0': {}
+
+ '@esbuild/aix-ppc64@0.27.7':
+ optional: true
+
+ '@esbuild/android-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/android-arm@0.27.7':
+ optional: true
+
+ '@esbuild/android-x64@0.27.7':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/darwin-x64@0.27.7':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.27.7':
+ optional: true
+
+ '@esbuild/linux-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/linux-arm@0.27.7':
+ optional: true
+
+ '@esbuild/linux-ia32@0.27.7':
+ optional: true
+
+ '@esbuild/linux-loong64@0.27.7':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.27.7':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.27.7':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.27.7':
+ optional: true
+
+ '@esbuild/linux-s390x@0.27.7':
+ optional: true
+
+ '@esbuild/linux-x64@0.27.7':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.27.7':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.27.7':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/sunos-x64@0.27.7':
+ optional: true
+
+ '@esbuild/win32-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/win32-ia32@0.27.7':
+ optional: true
+
+ '@esbuild/win32-x64@0.27.7':
+ optional: true
+
+ '@floating-ui/core@1.7.5':
+ dependencies:
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/dom@1.7.6':
+ dependencies:
+ '@floating-ui/core': 1.7.5
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/utils@0.2.11': {}
+
+ '@fontsource/inter@5.2.8': {}
+
+ '@internationalized/date@3.12.2':
+ dependencies:
+ '@swc/helpers': 0.5.23
+
+ '@internationalized/number@3.6.6':
+ dependencies:
+ '@swc/helpers': 0.5.23
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@tybys/wasm-util': 0.10.3
+ optional: true
+
+ '@oxc-project/types@0.137.0': {}
+
+ '@oxfmt/binding-android-arm-eabi@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-android-arm64@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-darwin-arm64@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-darwin-x64@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-freebsd-x64@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm-gnueabihf@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm-musleabihf@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm64-gnu@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm64-musl@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-linux-ppc64-gnu@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-linux-riscv64-gnu@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-linux-riscv64-musl@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-linux-s390x-gnu@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-linux-x64-gnu@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-linux-x64-musl@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-openharmony-arm64@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-win32-arm64-msvc@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-win32-ia32-msvc@0.56.0':
+ optional: true
+
+ '@oxfmt/binding-win32-x64-msvc@0.56.0':
+ optional: true
+
+ '@oxlint/binding-android-arm-eabi@1.71.0':
+ optional: true
+
+ '@oxlint/binding-android-arm64@1.71.0':
+ optional: true
+
+ '@oxlint/binding-darwin-arm64@1.71.0':
+ optional: true
+
+ '@oxlint/binding-darwin-x64@1.71.0':
+ optional: true
+
+ '@oxlint/binding-freebsd-x64@1.71.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.71.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-musleabihf@1.71.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-gnu@1.71.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-musl@1.71.0':
+ optional: true
+
+ '@oxlint/binding-linux-ppc64-gnu@1.71.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-gnu@1.71.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-musl@1.71.0':
+ optional: true
+
+ '@oxlint/binding-linux-s390x-gnu@1.71.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-gnu@1.71.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-musl@1.71.0':
+ optional: true
+
+ '@oxlint/binding-openharmony-arm64@1.71.0':
+ optional: true
+
+ '@oxlint/binding-win32-arm64-msvc@1.71.0':
+ optional: true
+
+ '@oxlint/binding-win32-ia32-msvc@1.71.0':
+ optional: true
+
+ '@oxlint/binding-win32-x64-msvc@1.71.0':
+ optional: true
+
+ '@pandacss/is-valid-prop@1.11.3': {}
+
+ '@rolldown/binding-android-arm64@1.1.3':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.1.3':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.1.3':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.1.3':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.1.3':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.1.3':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.1.3':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.3':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.1.3':
+ optional: true
+
+ '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.1.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))':
+ dependencies:
+ '@babel/core': 7.29.7
+ picomatch: 4.0.4
+ rolldown: 1.1.3
+ optionalDependencies:
+ '@babel/runtime': 7.29.7
+ vite: 8.1.0(@types/node@26.0.1)(esbuild@0.27.7)
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@socket.io/component-emitter@3.1.2': {}
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@swc/helpers@0.5.23':
+ dependencies:
+ tslib: 2.8.1
+
+ '@tanstack/history@1.162.0': {}
+
+ '@tanstack/react-router@1.170.16(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@tanstack/history': 1.162.0
+ '@tanstack/react-store': 0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@tanstack/router-core': 1.171.13
+ isbot: 5.1.44
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
+ '@tanstack/react-store@0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@tanstack/store': 0.9.3
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ use-sync-external-store: 1.6.0(react@19.2.7)
+
+ '@tanstack/react-virtual@3.14.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@tanstack/virtual-core': 3.17.1
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
+ '@tanstack/router-core@1.171.13':
+ dependencies:
+ '@tanstack/history': 1.162.0
+ cookie-es: 3.1.1
+ seroval: 1.5.4
+ seroval-plugins: 1.5.4(seroval@1.5.4)
+
+ '@tanstack/store@0.9.3': {}
+
+ '@tanstack/virtual-core@3.17.1': {}
+
+ '@tybys/wasm-util@0.10.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-drag@3.0.7':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-selection@3.0.11': {}
+
+ '@types/d3-transition@3.0.9':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-zoom@3.0.8':
+ dependencies:
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-selection': 3.0.11
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/node@26.0.1':
+ dependencies:
+ undici-types: 8.3.0
+
+ '@types/parse-json@4.0.2': {}
+
+ '@types/react-dom@19.2.3(@types/react@19.2.17)':
+ dependencies:
+ '@types/react': 19.2.17
+
+ '@types/react@19.2.17':
+ dependencies:
+ csstype: 3.2.3
+
+ '@types/use-sync-external-store@1.5.0': {}
+
+ '@vitejs/plugin-react@6.0.3(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.1.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.1
+ vite: 8.1.0(@types/node@26.0.1)(esbuild@0.27.7)
+ optionalDependencies:
+ '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.1.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))
+ babel-plugin-react-compiler: 1.0.0
+
+ '@vitest/expect@4.1.9':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.1.9
+ '@vitest/utils': 4.1.9
+ chai: 6.2.2
+ tinyrainbow: 3.1.0
+
+ '@vitest/mocker@4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))':
+ dependencies:
+ '@vitest/spy': 4.1.9
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 8.1.0(@types/node@26.0.1)(esbuild@0.27.7)
+
+ '@vitest/pretty-format@4.1.9':
+ dependencies:
+ tinyrainbow: 3.1.0
+
+ '@vitest/runner@4.1.9':
+ dependencies:
+ '@vitest/utils': 4.1.9
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.1.9':
+ dependencies:
+ '@vitest/pretty-format': 4.1.9
+ '@vitest/utils': 4.1.9
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.1.9': {}
+
+ '@vitest/utils@4.1.9':
+ dependencies:
+ '@vitest/pretty-format': 4.1.9
+ convert-source-map: 2.0.0
+ tinyrainbow: 3.1.0
+
+ '@xyflow/react@12.11.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@xyflow/system': 0.0.78
+ classcat: 5.0.5
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ zustand: 4.5.7(@types/react@19.2.17)(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+ transitivePeerDependencies:
+ - immer
+
+ '@xyflow/system@0.0.78':
+ dependencies:
+ '@types/d3-drag': 3.0.7
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-selection': 3.0.11
+ '@types/d3-transition': 3.0.9
+ '@types/d3-zoom': 3.0.8
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-zoom: 3.0.0
+
+ '@zag-js/accordion@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/anatomy@1.41.2': {}
+
+ '@zag-js/angle-slider@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/rect-utils': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/aria-hidden@1.41.2':
+ dependencies:
+ '@zag-js/dom-query': 1.41.2
+
+ '@zag-js/async-list@1.41.2':
+ dependencies:
+ '@zag-js/core': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/auto-resize@1.41.2':
+ dependencies:
+ '@zag-js/dom-query': 1.41.2
+
+ '@zag-js/avatar@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/carousel@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/scroll-snap': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/cascade-select@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/collection': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-visible': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/rect-utils': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/checkbox@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-visible': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/clipboard@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/collapsible@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/collection@1.41.2':
+ dependencies:
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/color-picker@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/color-utils': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/color-utils@1.41.2':
+ dependencies:
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/combobox@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/collection': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-visible': 1.41.2
+ '@zag-js/live-region': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/core@1.41.2':
+ dependencies:
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/date-input@1.41.2(@internationalized/date@3.12.2)':
+ dependencies:
+ '@internationalized/date': 3.12.2
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/date-utils': 1.41.2(@internationalized/date@3.12.2)
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/live-region': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/date-picker@1.41.2(@internationalized/date@3.12.2)':
+ dependencies:
+ '@internationalized/date': 3.12.2
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/date-utils': 1.41.2(@internationalized/date@3.12.2)
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/live-region': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/date-utils@1.41.2(@internationalized/date@3.12.2)':
+ dependencies:
+ '@internationalized/date': 3.12.2
+
+ '@zag-js/dialog@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/aria-hidden': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-trap': 1.41.2
+ '@zag-js/remove-scroll': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/dismissable@1.41.2':
+ dependencies:
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/interact-outside': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/dom-query@1.41.2':
+ dependencies:
+ '@zag-js/types': 1.41.2
+
+ '@zag-js/drawer@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/aria-hidden': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-trap': 1.41.2
+ '@zag-js/remove-scroll': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/editable@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/interact-outside': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/file-upload@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/file-utils': 1.41.2
+ '@zag-js/i18n-utils': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/file-utils@1.41.2':
+ dependencies:
+ '@zag-js/i18n-utils': 1.41.2
+
+ '@zag-js/floating-panel@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/rect-utils': 1.41.2
+ '@zag-js/store': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/focus-trap@1.41.2':
+ dependencies:
+ '@zag-js/dom-query': 1.41.2
+
+ '@zag-js/focus-visible@1.41.2':
+ dependencies:
+ '@zag-js/dom-query': 1.41.2
+
+ '@zag-js/highlight-word@1.41.2': {}
+
+ '@zag-js/hover-card@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/i18n-utils@1.41.2':
+ dependencies:
+ '@zag-js/dom-query': 1.41.2
+
+ '@zag-js/image-cropper@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/interact-outside@1.41.2':
+ dependencies:
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/json-tree-utils@1.41.2': {}
+
+ '@zag-js/listbox@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/collection': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-visible': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/live-region@1.41.2': {}
+
+ '@zag-js/marquee@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/menu@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-visible': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/rect-utils': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/navigation-menu@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/number-input@1.41.2':
+ dependencies:
+ '@internationalized/number': 3.6.6
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/pagination@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/password-input@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/pin-input@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/popover@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/aria-hidden': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-trap': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/remove-scroll': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/popper@1.41.2':
+ dependencies:
+ '@floating-ui/dom': 1.7.6
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/presence@1.41.2':
+ dependencies:
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+
+ '@zag-js/progress@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/qr-code@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+ proxy-memoize: 3.0.1
+ uqr: 0.1.3
+
+ '@zag-js/radio-group@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-visible': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/rating-group@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/react@1.41.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
+ dependencies:
+ '@zag-js/core': 1.41.2
+ '@zag-js/store': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
+ '@zag-js/rect-utils@1.41.2': {}
+
+ '@zag-js/remove-scroll@1.41.2':
+ dependencies:
+ '@zag-js/dom-query': 1.41.2
+
+ '@zag-js/scroll-area@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/scroll-snap@1.41.2':
+ dependencies:
+ '@zag-js/dom-query': 1.41.2
+
+ '@zag-js/select@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/collection': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-visible': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/signature-pad@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+ perfect-freehand: 1.2.3
+
+ '@zag-js/slider@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/splitter@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/steps@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/store@1.41.2':
+ dependencies:
+ proxy-compare: 3.0.1
+
+ '@zag-js/switch@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-visible': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/tabs@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/tags-input@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/auto-resize': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/interact-outside': 1.41.2
+ '@zag-js/live-region': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/timer@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/toast@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/toggle-group@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/toggle@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/tooltip@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-visible': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/tour@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dismissable': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/focus-trap': 1.41.2
+ '@zag-js/interact-outside': 1.41.2
+ '@zag-js/popper': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/tree-view@1.41.2':
+ dependencies:
+ '@zag-js/anatomy': 1.41.2
+ '@zag-js/collection': 1.41.2
+ '@zag-js/core': 1.41.2
+ '@zag-js/dom-query': 1.41.2
+ '@zag-js/types': 1.41.2
+ '@zag-js/utils': 1.41.2
+
+ '@zag-js/types@1.41.2':
+ dependencies:
+ csstype: 3.2.3
+
+ '@zag-js/utils@1.41.2': {}
+
+ assertion-error@2.0.1: {}
+
+ babel-plugin-macros@3.1.0:
+ dependencies:
+ '@babel/runtime': 7.29.7
+ cosmiconfig: 7.1.0
+ resolve: 1.22.12
+
+ babel-plugin-react-compiler@1.0.0:
+ dependencies:
+ '@babel/types': 7.29.7
+
+ baseline-browser-mapping@2.10.40: {}
+
+ browserslist@4.28.4:
+ dependencies:
+ baseline-browser-mapping: 2.10.40
+ caniuse-lite: 1.0.30001799
+ electron-to-chromium: 1.5.379
+ node-releases: 2.0.50
+ update-browserslist-db: 1.2.3(browserslist@4.28.4)
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001799: {}
+
+ chai@6.2.2: {}
+
+ classcat@5.0.5: {}
+
+ convert-source-map@1.9.0: {}
+
+ convert-source-map@2.0.0: {}
+
+ cookie-es@3.1.1: {}
+
+ cosmiconfig@7.1.0:
+ dependencies:
+ '@types/parse-json': 4.0.2
+ import-fresh: 3.3.1
+ parse-json: 5.2.0
+ path-type: 4.0.0
+ yaml: 1.10.3
+
+ csstype@3.2.3: {}
+
+ d3-color@3.1.0: {}
+
+ d3-dispatch@3.0.1: {}
+
+ d3-drag@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-selection: 3.0.0
+
+ d3-ease@3.0.1: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-selection@3.0.0: {}
+
+ d3-timer@3.0.1: {}
+
+ d3-transition@3.0.1(d3-selection@3.0.0):
+ dependencies:
+ d3-color: 3.1.0
+ d3-dispatch: 3.0.1
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-timer: 3.0.1
+
+ d3-zoom@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ detect-libc@2.1.2: {}
+
+ electron-to-chromium@1.5.379: {}
+
+ engine.io-client@6.6.6:
+ dependencies:
+ '@socket.io/component-emitter': 3.1.2
+ debug: 4.4.3
+ engine.io-parser: 5.2.3
+ ws: 8.21.0
+ xmlhttprequest-ssl: 2.1.2
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ engine.io-parser@5.2.3: {}
+
+ error-ex@1.3.4:
+ dependencies:
+ is-arrayish: 0.2.1
+
+ es-errors@1.3.0: {}
+
+ es-module-lexer@2.1.0: {}
+
+ esbuild@0.27.7:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.27.7
+ '@esbuild/android-arm': 0.27.7
+ '@esbuild/android-arm64': 0.27.7
+ '@esbuild/android-x64': 0.27.7
+ '@esbuild/darwin-arm64': 0.27.7
+ '@esbuild/darwin-x64': 0.27.7
+ '@esbuild/freebsd-arm64': 0.27.7
+ '@esbuild/freebsd-x64': 0.27.7
+ '@esbuild/linux-arm': 0.27.7
+ '@esbuild/linux-arm64': 0.27.7
+ '@esbuild/linux-ia32': 0.27.7
+ '@esbuild/linux-loong64': 0.27.7
+ '@esbuild/linux-mips64el': 0.27.7
+ '@esbuild/linux-ppc64': 0.27.7
+ '@esbuild/linux-riscv64': 0.27.7
+ '@esbuild/linux-s390x': 0.27.7
+ '@esbuild/linux-x64': 0.27.7
+ '@esbuild/netbsd-arm64': 0.27.7
+ '@esbuild/netbsd-x64': 0.27.7
+ '@esbuild/openbsd-arm64': 0.27.7
+ '@esbuild/openbsd-x64': 0.27.7
+ '@esbuild/openharmony-arm64': 0.27.7
+ '@esbuild/sunos-x64': 0.27.7
+ '@esbuild/win32-arm64': 0.27.7
+ '@esbuild/win32-ia32': 0.27.7
+ '@esbuild/win32-x64': 0.27.7
+ optional: true
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ expect-type@1.4.0: {}
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ find-root@1.1.0: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ hasown@2.0.4:
+ dependencies:
+ function-bind: 1.1.2
+
+ hoist-non-react-statics@3.3.2:
+ dependencies:
+ react-is: 16.13.1
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ is-arrayish@0.2.1: {}
+
+ is-core-module@2.16.2:
+ dependencies:
+ hasown: 2.0.4
+
+ isbot@5.1.44: {}
+
+ js-tokens@4.0.0: {}
+
+ jsesc@3.1.0: {}
+
+ json-parse-even-better-errors@2.3.1: {}
+
+ json5@2.2.3: {}
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ lines-and-columns@1.2.4: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ lucide-react@1.21.0(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.15: {}
+
+ node-releases@2.0.50: {}
+
+ obug@2.1.3: {}
+
+ oxfmt@0.56.0:
+ dependencies:
+ tinypool: 2.1.0
+ optionalDependencies:
+ '@oxfmt/binding-android-arm-eabi': 0.56.0
+ '@oxfmt/binding-android-arm64': 0.56.0
+ '@oxfmt/binding-darwin-arm64': 0.56.0
+ '@oxfmt/binding-darwin-x64': 0.56.0
+ '@oxfmt/binding-freebsd-x64': 0.56.0
+ '@oxfmt/binding-linux-arm-gnueabihf': 0.56.0
+ '@oxfmt/binding-linux-arm-musleabihf': 0.56.0
+ '@oxfmt/binding-linux-arm64-gnu': 0.56.0
+ '@oxfmt/binding-linux-arm64-musl': 0.56.0
+ '@oxfmt/binding-linux-ppc64-gnu': 0.56.0
+ '@oxfmt/binding-linux-riscv64-gnu': 0.56.0
+ '@oxfmt/binding-linux-riscv64-musl': 0.56.0
+ '@oxfmt/binding-linux-s390x-gnu': 0.56.0
+ '@oxfmt/binding-linux-x64-gnu': 0.56.0
+ '@oxfmt/binding-linux-x64-musl': 0.56.0
+ '@oxfmt/binding-openharmony-arm64': 0.56.0
+ '@oxfmt/binding-win32-arm64-msvc': 0.56.0
+ '@oxfmt/binding-win32-ia32-msvc': 0.56.0
+ '@oxfmt/binding-win32-x64-msvc': 0.56.0
+
+ oxlint@1.71.0:
+ optionalDependencies:
+ '@oxlint/binding-android-arm-eabi': 1.71.0
+ '@oxlint/binding-android-arm64': 1.71.0
+ '@oxlint/binding-darwin-arm64': 1.71.0
+ '@oxlint/binding-darwin-x64': 1.71.0
+ '@oxlint/binding-freebsd-x64': 1.71.0
+ '@oxlint/binding-linux-arm-gnueabihf': 1.71.0
+ '@oxlint/binding-linux-arm-musleabihf': 1.71.0
+ '@oxlint/binding-linux-arm64-gnu': 1.71.0
+ '@oxlint/binding-linux-arm64-musl': 1.71.0
+ '@oxlint/binding-linux-ppc64-gnu': 1.71.0
+ '@oxlint/binding-linux-riscv64-gnu': 1.71.0
+ '@oxlint/binding-linux-riscv64-musl': 1.71.0
+ '@oxlint/binding-linux-s390x-gnu': 1.71.0
+ '@oxlint/binding-linux-x64-gnu': 1.71.0
+ '@oxlint/binding-linux-x64-musl': 1.71.0
+ '@oxlint/binding-openharmony-arm64': 1.71.0
+ '@oxlint/binding-win32-arm64-msvc': 1.71.0
+ '@oxlint/binding-win32-ia32-msvc': 1.71.0
+ '@oxlint/binding-win32-x64-msvc': 1.71.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ error-ex: 1.3.4
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
+ path-parse@1.0.7: {}
+
+ path-type@4.0.0: {}
+
+ pathe@2.0.3: {}
+
+ perfect-freehand@1.2.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.4: {}
+
+ postcss@8.5.15:
+ dependencies:
+ nanoid: 3.3.15
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ proxy-compare@3.0.1: {}
+
+ proxy-memoize@3.0.1:
+ dependencies:
+ proxy-compare: 3.0.1
+
+ react-dom@19.2.7(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ scheduler: 0.27.0
+
+ react-hook-tanstack-virtual@0.0.4(@tanstack/react-virtual@3.14.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@tanstack/virtual-core@3.17.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ '@tanstack/react-virtual': 3.14.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@tanstack/virtual-core': 3.17.1
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
+ react-icons@5.6.0(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+
+ react-is@16.13.1: {}
+
+ react@19.2.7: {}
+
+ resolve-from@4.0.0: {}
+
+ resolve@1.22.12:
+ dependencies:
+ es-errors: 1.3.0
+ is-core-module: 2.16.2
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ rolldown@1.1.3:
+ dependencies:
+ '@oxc-project/types': 0.137.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.1.3
+ '@rolldown/binding-darwin-arm64': 1.1.3
+ '@rolldown/binding-darwin-x64': 1.1.3
+ '@rolldown/binding-freebsd-x64': 1.1.3
+ '@rolldown/binding-linux-arm-gnueabihf': 1.1.3
+ '@rolldown/binding-linux-arm64-gnu': 1.1.3
+ '@rolldown/binding-linux-arm64-musl': 1.1.3
+ '@rolldown/binding-linux-ppc64-gnu': 1.1.3
+ '@rolldown/binding-linux-s390x-gnu': 1.1.3
+ '@rolldown/binding-linux-x64-gnu': 1.1.3
+ '@rolldown/binding-linux-x64-musl': 1.1.3
+ '@rolldown/binding-openharmony-arm64': 1.1.3
+ '@rolldown/binding-wasm32-wasi': 1.1.3
+ '@rolldown/binding-win32-arm64-msvc': 1.1.3
+ '@rolldown/binding-win32-x64-msvc': 1.1.3
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ seroval-plugins@1.5.4(seroval@1.5.4):
+ dependencies:
+ seroval: 1.5.4
+
+ seroval@1.5.4: {}
+
+ siginfo@2.0.0: {}
+
+ socket.io-client@4.8.3:
+ dependencies:
+ '@socket.io/component-emitter': 3.1.2
+ debug: 4.4.3
+ engine.io-client: 6.6.6
+ socket.io-parser: 4.2.6
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ socket.io-parser@4.2.6:
+ dependencies:
+ '@socket.io/component-emitter': 3.1.2
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ source-map-js@1.2.1: {}
+
+ source-map@0.5.7: {}
+
+ stackback@0.0.2: {}
+
+ std-env@4.1.0: {}
+
+ stylis@4.2.0: {}
+
+ supports-preserve-symlinks-flag@1.0.0: {}
+
+ tinybench@2.9.0: {}
+
+ tinyexec@1.2.4: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ tinykeys@4.0.0: {}
+
+ tinypool@2.1.0: {}
+
+ tinyrainbow@3.1.0: {}
+
+ tslib@2.8.1: {}
+
+ typescript@6.0.3: {}
+
+ undici-types@8.3.0: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.4):
+ dependencies:
+ browserslist: 4.28.4
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uqr@0.1.3: {}
+
+ use-sync-external-store@1.6.0(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+
+ vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.15
+ rolldown: 1.1.3
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 26.0.1
+ esbuild: 0.27.7
+ fsevents: 2.3.3
+
+ vitest@4.1.9(@types/node@26.0.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)):
+ dependencies:
+ '@vitest/expect': 4.1.9
+ '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))
+ '@vitest/pretty-format': 4.1.9
+ '@vitest/runner': 4.1.9
+ '@vitest/snapshot': 4.1.9
+ '@vitest/spy': 4.1.9
+ '@vitest/utils': 4.1.9
+ es-module-lexer: 2.1.0
+ expect-type: 1.4.0
+ magic-string: 0.30.21
+ obug: 2.1.3
+ pathe: 2.0.3
+ picomatch: 4.0.4
+ std-env: 4.1.0
+ tinybench: 2.9.0
+ tinyexec: 1.2.4
+ tinyglobby: 0.2.17
+ tinyrainbow: 3.1.0
+ vite: 8.1.0(@types/node@26.0.1)(esbuild@0.27.7)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 26.0.1
+ transitivePeerDependencies:
+ - msw
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ ws@8.21.0: {}
+
+ xmlhttprequest-ssl@2.1.2: {}
+
+ yallist@3.1.1: {}
+
+ yaml@1.10.3: {}
+
+ zod@4.4.3: {}
+
+ zustand@4.5.7(@types/react@19.2.17)(react@19.2.7):
+ dependencies:
+ use-sync-external-store: 1.6.0(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ react: 19.2.7
diff --git a/invokeai/frontend/webv2/src/App.tsx b/invokeai/frontend/webv2/src/App.tsx
new file mode 100644
index 00000000000..bc0fe6cdcd9
--- /dev/null
+++ b/invokeai/frontend/webv2/src/App.tsx
@@ -0,0 +1,15 @@
+import { ChakraProvider } from '@chakra-ui/react';
+import { RouterProvider } from '@tanstack/react-router';
+
+import { router } from './router';
+import { system } from './theme/system';
+import { AppToaster } from './workbench/components/ui/toaster';
+import { ThemeController } from './workbench/ThemeController';
+
+export const App = () => (
+
+
+
+
+
+);
diff --git a/invokeai/frontend/webv2/src/WorkbenchApp.tsx b/invokeai/frontend/webv2/src/WorkbenchApp.tsx
new file mode 100644
index 00000000000..6ffc11d4c6c
--- /dev/null
+++ b/invokeai/frontend/webv2/src/WorkbenchApp.tsx
@@ -0,0 +1,41 @@
+import { useSearch } from '@tanstack/react-router';
+import { WorkbenchShell } from '@workbench/shell';
+import { useMemo } from 'react';
+
+import type { WorkbenchSearch } from './workbench/projects/session';
+
+import { SessionExpiryGuard } from './workbench/auth/components/SessionExpiryGuard';
+import { WorkbenchHotkeyRuntime } from './workbench/hotkeys/WorkbenchHotkeyRuntime';
+import { WidgetHosts } from './workbench/widget-frame/WidgetHosts';
+import { WorkbenchProvider } from './workbench/WorkbenchContext';
+import { WorkbenchRuntime } from './workbench/WorkbenchRuntime';
+import { WorkbenchSessionController } from './workbench/WorkbenchSessionController';
+
+/**
+ * The authenticated editor: providers, editor-only runtimes, and the shell.
+ * The shared backend socket is mounted above this route; `WorkbenchRuntime`
+ * attaches the generation queue listeners while the editor is open.
+ *
+ * The route's search params shape the boot: ?project deep-links a library
+ * project into the session, ?new starts a fresh draft. Both are consumed by
+ * the persistence load; the session controller handles params that change
+ * while the editor is already mounted.
+ */
+export const WorkbenchApp = () => {
+ const search = useSearch({ strict: false }) as WorkbenchSearch;
+ const loadOptions = useMemo(
+ () => ({ createNew: search.new, openProjectId: search.project }),
+ [search.new, search.project]
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/invokeai/frontend/webv2/src/assets/SplashImage.webp b/invokeai/frontend/webv2/src/assets/SplashImage.webp
new file mode 100644
index 00000000000..02ab32d523c
Binary files /dev/null and b/invokeai/frontend/webv2/src/assets/SplashImage.webp differ
diff --git a/invokeai/frontend/webv2/src/lucide-icons.d.ts b/invokeai/frontend/webv2/src/lucide-icons.d.ts
new file mode 100644
index 00000000000..0330f2f1c87
--- /dev/null
+++ b/invokeai/frontend/webv2/src/lucide-icons.d.ts
@@ -0,0 +1,6 @@
+declare module 'lucide-react/dist/esm/icons/*.mjs' {
+ import type { ComponentType, SVGProps } from 'react';
+
+ const Icon: ComponentType>;
+ export default Icon;
+}
diff --git a/invokeai/frontend/webv2/src/main.tsx b/invokeai/frontend/webv2/src/main.tsx
new file mode 100644
index 00000000000..a4a889073f2
--- /dev/null
+++ b/invokeai/frontend/webv2/src/main.tsx
@@ -0,0 +1,17 @@
+import '@fontsource/inter/index.css';
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+
+import { App } from './App';
+
+const rootElement = document.getElementById('root');
+
+if (!rootElement) {
+ throw new Error('Unable to mount Invoke V7: root element was not found.');
+}
+
+createRoot(rootElement).render(
+
+
+
+);
diff --git a/invokeai/frontend/webv2/src/router.tsx b/invokeai/frontend/webv2/src/router.tsx
new file mode 100644
index 00000000000..b6a06e12be0
--- /dev/null
+++ b/invokeai/frontend/webv2/src/router.tsx
@@ -0,0 +1,203 @@
+import {
+ createHashHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ lazyRouteComponent,
+ Navigate,
+ Outlet,
+ redirect,
+} from '@tanstack/react-router';
+
+import { getCapabilities, type Capabilities } from './workbench/auth/capabilities';
+import { LoginScreen } from './workbench/auth/components/LoginScreen';
+import { SetupScreen } from './workbench/auth/components/SetupScreen';
+import { ensureAuthSession } from './workbench/auth/session';
+import { SocketHubRuntime } from './workbench/backend/SocketHubRuntime';
+import { WorkbenchSplashScreen } from './workbench/components/WorkbenchSplashScreen';
+import { Launchpad } from './workbench/launchpad/Launchpad';
+import { peekOpenProjectIds, type WorkbenchSearch } from './workbench/projects/session';
+import { loadWorkbenchSettings } from './workbench/settings/store';
+
+/**
+ * Code-based route tree. The authenticated layout route owns the auth guard,
+ * which resolves the backend's multi-user status once per app load:
+ *
+ * - multi-user off → authenticated routes render directly; /login + /setup bounce home
+ * - setup required → everything funnels to /setup
+ * - signed out → authenticated routes redirect to /login
+ *
+ * Under it, `/` is the Launchpad (project library, model manager, profile,
+ * resources) and `/app` is the editor. The editor bundle — canvas, workflow,
+ * widgets — is code-split behind `lazyRouteComponent`, so the Launchpad stays
+ * light; `defaultPreload: 'intent'` starts fetching that chunk the moment a
+ * project card is hovered.
+ *
+ * Hash history is used because the bundle is served with a relative base
+ * (`./`), so the app cannot rely on server-side fallback for deep paths.
+ */
+
+const rootRoute = createRootRoute({ component: Outlet });
+
+/**
+ * Wraps every authenticated route with the shared backend socket transport.
+ * Feature runtimes attach their own listeners only where needed, keeping the
+ * light Launchpad bundle free of editor/model-manager code.
+ */
+const AuthenticatedLayout = () => (
+ <>
+
+
+ >
+);
+
+const authenticatedRoute = createRoute({
+ beforeLoad: async () => {
+ const session = await ensureAuthSession();
+
+ if (session.multiuserEnabled) {
+ if (session.setupRequired) {
+ throw redirect({ to: '/setup' });
+ }
+
+ if (session.user === null) {
+ throw redirect({ to: '/login' });
+ }
+ }
+
+ await loadWorkbenchSettings();
+ },
+ component: AuthenticatedLayout,
+ getParentRoute: () => rootRoute,
+ id: 'authenticated',
+});
+
+const requireLaunchpadCapability = async (capability: keyof Capabilities): Promise => {
+ const session = await ensureAuthSession();
+
+ if (!getCapabilities(session)[capability]) {
+ throw redirect({ to: '/projects' });
+ }
+};
+
+// `/` plus a deep-link alias per Launchpad section. All render the same shell;
+// it reads the path to pick the active page.
+const homeRoute = createRoute({
+ component: Launchpad,
+ getParentRoute: () => authenticatedRoute,
+ path: '/',
+});
+
+const projectsHomeRoute = createRoute({
+ component: Launchpad,
+ getParentRoute: () => authenticatedRoute,
+ path: 'projects',
+});
+
+const modelsHomeRoute = createRoute({
+ beforeLoad: () => requireLaunchpadCapability('canManageModels'),
+ component: Launchpad,
+ getParentRoute: () => authenticatedRoute,
+ path: 'models',
+});
+
+const nodesHomeRoute = createRoute({
+ beforeLoad: () => requireLaunchpadCapability('canManageNodes'),
+ component: Launchpad,
+ getParentRoute: () => authenticatedRoute,
+ path: 'nodes',
+});
+
+const usersHomeRoute = createRoute({
+ beforeLoad: () => requireLaunchpadCapability('canManageUsers'),
+ component: Launchpad,
+ getParentRoute: () => authenticatedRoute,
+ path: 'users',
+});
+
+const workbenchRoute = createRoute({
+ beforeLoad: async ({ cause, search }) => {
+ // Photoshop semantics: the editor without documents is Home. A definite
+ // empty session redirects unless the URL explicitly asks for a project or
+ // a fresh draft; an unknowable session (first run, legacy blob, backend
+ // unreachable) falls through and the editor boots from what it can find.
+ //
+ // Only actual entries are checked: search-only changes ('stay', e.g. the
+ // session controller stripping ?new before the draft has autosaved) must
+ // not re-evaluate a blob that is still catching up, and hover preloads
+ // should not hit the session endpoint at all.
+ if (cause !== 'enter' || search.project || search.new) {
+ return;
+ }
+
+ const openProjectIds = await peekOpenProjectIds();
+
+ if (openProjectIds !== null && openProjectIds.length === 0) {
+ throw redirect({ to: '/' });
+ }
+ },
+ component: lazyRouteComponent(() => import('./WorkbenchApp'), 'WorkbenchApp'),
+ getParentRoute: () => authenticatedRoute,
+ path: '/app',
+ validateSearch: (search: Record): WorkbenchSearch => ({
+ new: search.new === true || search.new === 'true' || search.new === 1 ? true : undefined,
+ project: typeof search.project === 'string' && search.project.length > 0 ? search.project : undefined,
+ }),
+});
+
+const loginRoute = createRoute({
+ beforeLoad: async () => {
+ const session = await ensureAuthSession();
+
+ if (session.multiuserEnabled && session.setupRequired) {
+ throw redirect({ to: '/setup' });
+ }
+
+ if (!session.multiuserEnabled || session.user !== null) {
+ throw redirect({ to: '/' });
+ }
+ },
+ component: LoginScreen,
+ getParentRoute: () => rootRoute,
+ path: '/login',
+});
+
+const setupRoute = createRoute({
+ beforeLoad: async () => {
+ const session = await ensureAuthSession();
+
+ if (!session.multiuserEnabled || !session.setupRequired) {
+ throw redirect({ to: '/' });
+ }
+ },
+ component: SetupScreen,
+ getParentRoute: () => rootRoute,
+ path: '/setup',
+});
+
+const RouterPending = () => ;
+
+export const router = createRouter({
+ defaultNotFoundComponent: () => ,
+ defaultPendingComponent: RouterPending,
+ defaultPreload: 'intent',
+ history: createHashHistory(),
+ routeTree: rootRoute.addChildren([
+ authenticatedRoute.addChildren([
+ homeRoute,
+ projectsHomeRoute,
+ modelsHomeRoute,
+ nodesHomeRoute,
+ usersHomeRoute,
+ workbenchRoute,
+ ]),
+ loginRoute,
+ setupRoute,
+ ]),
+});
+
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: typeof router;
+ }
+}
diff --git a/invokeai/frontend/webv2/src/theme/__fixtures__/legacyTokenBaseline.json b/invokeai/frontend/webv2/src/theme/__fixtures__/legacyTokenBaseline.json
new file mode 100644
index 00000000000..906dba0ba17
--- /dev/null
+++ b/invokeai/frontend/webv2/src/theme/__fixtures__/legacyTokenBaseline.json
@@ -0,0 +1,237 @@
+{
+ "classic": {
+ "bg": "oklch(21.074% 0.0087 264.37)",
+ "bg.subtle": "oklch(26.279% 0.0123 264.34)",
+ "bg.muted": "oklch(31.237% 0.0157 264.32)",
+ "bg.panel": "oklch(31.237% 0.0157 264.32)",
+ "bg.emphasized": "oklch(36.004% 0.019 264.3)",
+ "bg.inset": "oklch(21.074% 0.0087 264.37)",
+ "bg.error": "color-mix(in oklab, oklch(70.61% 0.0841 19.38) 14%, oklch(26.279% 0.0123 264.34))",
+ "bg.success": "color-mix(in oklab, oklch(79.8% 0.1132 141.63) 14%, oklch(26.279% 0.0123 264.34))",
+ "bg.warning": "color-mix(in oklab, oklch(76.62% 0.0612 62.9) 14%, oklch(26.279% 0.0123 264.34))",
+ "fg": "oklch(96.017% 0.0029 264.54)",
+ "fg.muted": "oklch(73.736% 0.0202 264.44)",
+ "fg.subtle": "oklch(57.965% 0.0337 264.27)",
+ "fg.grid": "oklch(40.619% 0.0221 264.29)",
+ "fg.error": "oklch(70.61% 0.0841 19.38)",
+ "fg.success": "oklch(79.8% 0.1132 141.63)",
+ "fg.warning": "oklch(76.62% 0.0612 62.9)",
+ "border": "oklch(40.619% 0.0221 264.29)",
+ "border.subtle": "oklch(40.619% 0.0221 264.29)",
+ "border.muted": "oklch(40.619% 0.0221 264.29)",
+ "border.emphasized": "oklch(45.106% 0.0251 264.28)",
+ "border.error": "oklch(70.61% 0.0841 19.38)",
+ "gray.contrast": "oklch(21.074% 0.0087 264.37)",
+ "gray.fg": "oklch(96.017% 0.0029 264.54)",
+ "gray.subtle": "oklch(31.237% 0.0157 264.32)",
+ "gray.muted": "oklch(36.004% 0.019 264.3)",
+ "gray.emphasized": "oklch(45.106% 0.0251 264.28)",
+ "gray.solid": "oklch(96.017% 0.0029 264.54)",
+ "gray.focusRing": "oklch(77.738% 0.1 231.76)",
+ "gray.border": "oklch(45.106% 0.0251 264.28)",
+ "brand.solid": "oklch(92.041% 0.2103 116.59)",
+ "brand.contrast": "oklch(21.074% 0.0087 264.37)",
+ "brand.fg": "oklch(92.041% 0.2103 116.59)",
+ "brand.subtle": "color-mix(in oklab, oklch(92.041% 0.2103 116.59) 16%, oklch(26.279% 0.0123 264.34))",
+ "brand.muted": "color-mix(in oklab, oklch(92.041% 0.2103 116.59) 26%, oklch(26.279% 0.0123 264.34))",
+ "brand.emphasized": "color-mix(in oklab, oklch(92.041% 0.2103 116.59) 36%, oklch(26.279% 0.0123 264.34))",
+ "brand.focusRing": "oklch(77.738% 0.1 231.76)",
+ "brand.border": "oklch(92.041% 0.2103 116.59)",
+ "accent.solid": "oklch(77.738% 0.1 231.76)",
+ "accent.contrast": "oklch(21.074% 0.0087 264.37)",
+ "accent.fg": "oklch(77.738% 0.1 231.76)",
+ "accent.subtle": "color-mix(in oklab, oklch(77.738% 0.1 231.76) 16%, oklch(26.279% 0.0123 264.34))",
+ "accent.muted": "color-mix(in oklab, oklch(77.738% 0.1 231.76) 26%, oklch(26.279% 0.0123 264.34))",
+ "accent.emphasized": "color-mix(in oklab, oklch(77.738% 0.1 231.76) 36%, oklch(26.279% 0.0123 264.34))",
+ "accent.focusRing": "oklch(77.738% 0.1 231.76)",
+ "accent.border": "oklch(77.738% 0.1 231.76)"
+ },
+ "light": {
+ "bg": "oklch(96% 0.0038 264)",
+ "bg.subtle": "oklch(99.4% 0.002 264)",
+ "bg.muted": "oklch(97% 0.003 264)",
+ "bg.panel": "oklch(97% 0.003 264)",
+ "bg.emphasized": "oklch(93.5% 0.005 264)",
+ "bg.inset": "oklch(96% 0.0038 264)",
+ "bg.error": "color-mix(in oklab, oklch(55.509% 0.1707 24.62) 14%, oklch(99.4% 0.002 264))",
+ "bg.success": "color-mix(in oklab, oklch(53% 0.15 150) 14%, oklch(99.4% 0.002 264))",
+ "bg.warning": "color-mix(in oklab, oklch(60% 0.13 72) 14%, oklch(99.4% 0.002 264))",
+ "fg": "oklch(22.5% 0.013 264)",
+ "fg.muted": "oklch(45% 0.017 264)",
+ "fg.subtle": "oklch(63% 0.014 264)",
+ "fg.grid": "oklch(88.5% 0.0075 264)",
+ "fg.error": "oklch(55.509% 0.1707 24.62)",
+ "fg.success": "oklch(53% 0.15 150)",
+ "fg.warning": "oklch(60% 0.13 72)",
+ "border": "oklch(90.5% 0.0065 264)",
+ "border.subtle": "oklch(90.5% 0.0065 264)",
+ "border.muted": "oklch(90.5% 0.0065 264)",
+ "border.emphasized": "oklch(84% 0.0095 264)",
+ "border.error": "oklch(55.509% 0.1707 24.62)",
+ "gray.contrast": "oklch(96% 0.0038 264)",
+ "gray.fg": "oklch(22.5% 0.013 264)",
+ "gray.subtle": "oklch(95.5% 0.005 264)",
+ "gray.muted": "oklch(93.5% 0.005 264)",
+ "gray.emphasized": "oklch(84% 0.0095 264)",
+ "gray.solid": "oklch(22.5% 0.013 264)",
+ "gray.focusRing": "oklch(54.615% 0.2152 262.88)",
+ "gray.border": "oklch(84% 0.0095 264)",
+ "brand.solid": "oklch(92.041% 0.2103 116.59)",
+ "brand.contrast": "oklch(22.5% 0.013 264)",
+ "brand.fg": "oklch(92.041% 0.2103 116.59)",
+ "brand.subtle": "color-mix(in oklab, oklch(92.041% 0.2103 116.59) 16%, oklch(99.4% 0.002 264))",
+ "brand.muted": "color-mix(in oklab, oklch(92.041% 0.2103 116.59) 26%, oklch(99.4% 0.002 264))",
+ "brand.emphasized": "color-mix(in oklab, oklch(92.041% 0.2103 116.59) 36%, oklch(99.4% 0.002 264))",
+ "brand.focusRing": "oklch(54.615% 0.2152 262.88)",
+ "brand.border": "oklch(92.041% 0.2103 116.59)",
+ "accent.solid": "oklch(54.615% 0.2152 262.88)",
+ "accent.contrast": "oklch(100% 0 0)",
+ "accent.fg": "oklch(54.615% 0.2152 262.88)",
+ "accent.subtle": "color-mix(in oklab, oklch(54.615% 0.2152 262.88) 16%, oklch(99.4% 0.002 264))",
+ "accent.muted": "color-mix(in oklab, oklch(54.615% 0.2152 262.88) 26%, oklch(99.4% 0.002 264))",
+ "accent.emphasized": "color-mix(in oklab, oklch(54.615% 0.2152 262.88) 36%, oklch(99.4% 0.002 264))",
+ "accent.focusRing": "oklch(54.615% 0.2152 262.88)",
+ "accent.border": "oklch(54.615% 0.2152 262.88)"
+ },
+ "forest": {
+ "bg": "oklch(17.349% 0.0154 144.88)",
+ "bg.subtle": "oklch(19.03% 0.0175 144.85)",
+ "bg.muted": "oklch(20.99% 0.0219 144.74)",
+ "bg.panel": "oklch(20.99% 0.0219 144.74)",
+ "bg.emphasized": "oklch(25.62% 0.03 144.64)",
+ "bg.inset": "oklch(17.349% 0.0154 144.88)",
+ "bg.error": "color-mix(in oklab, oklch(69.305% 0.1467 35.44) 14%, oklch(19.03% 0.0175 144.85))",
+ "bg.success": "color-mix(in oklab, oklch(78% 0.16 148) 14%, oklch(19.03% 0.0175 144.85))",
+ "bg.warning": "color-mix(in oklab, oklch(80% 0.12 76) 14%, oklch(19.03% 0.0175 144.85))",
+ "fg": "oklch(90.57% 0.0511 134.45)",
+ "fg.muted": "oklch(71.619% 0.0575 135.25)",
+ "fg.subtle": "oklch(53.577% 0.0538 136.73)",
+ "fg.grid": "oklch(34.825% 0.0526 141.63)",
+ "fg.error": "oklch(69.305% 0.1467 35.44)",
+ "fg.success": "oklch(78% 0.16 148)",
+ "fg.warning": "oklch(80% 0.12 76)",
+ "border": "oklch(27.425% 0.0354 143.04)",
+ "border.subtle": "oklch(27.425% 0.0354 143.04)",
+ "border.muted": "oklch(27.425% 0.0354 143.04)",
+ "border.emphasized": "oklch(36.133% 0.0575 141.06)",
+ "border.error": "oklch(69.305% 0.1467 35.44)",
+ "gray.contrast": "oklch(17.349% 0.0154 144.88)",
+ "gray.fg": "oklch(90.57% 0.0511 134.45)",
+ "gray.subtle": "oklch(28.489% 0.0483 141.22)",
+ "gray.muted": "oklch(25.62% 0.03 144.64)",
+ "gray.emphasized": "oklch(36.133% 0.0575 141.06)",
+ "gray.solid": "oklch(90.57% 0.0511 134.45)",
+ "gray.focusRing": "oklch(70.437% 0.1101 178.23)",
+ "gray.border": "oklch(36.133% 0.0575 141.06)",
+ "brand.solid": "oklch(79.714% 0.1784 136.37)",
+ "brand.contrast": "oklch(18.298% 0.0314 147.69)",
+ "brand.fg": "oklch(79.714% 0.1784 136.37)",
+ "brand.subtle": "color-mix(in oklab, oklch(79.714% 0.1784 136.37) 16%, oklch(19.03% 0.0175 144.85))",
+ "brand.muted": "color-mix(in oklab, oklch(79.714% 0.1784 136.37) 26%, oklch(19.03% 0.0175 144.85))",
+ "brand.emphasized": "color-mix(in oklab, oklch(79.714% 0.1784 136.37) 36%, oklch(19.03% 0.0175 144.85))",
+ "brand.focusRing": "oklch(70.437% 0.1101 178.23)",
+ "brand.border": "oklch(79.714% 0.1784 136.37)",
+ "accent.solid": "oklch(70.437% 0.1101 178.23)",
+ "accent.contrast": "oklch(17.44% 0.0258 171.89)",
+ "accent.fg": "oklch(70.437% 0.1101 178.23)",
+ "accent.subtle": "color-mix(in oklab, oklch(70.437% 0.1101 178.23) 16%, oklch(19.03% 0.0175 144.85))",
+ "accent.muted": "color-mix(in oklab, oklch(70.437% 0.1101 178.23) 26%, oklch(19.03% 0.0175 144.85))",
+ "accent.emphasized": "color-mix(in oklab, oklch(70.437% 0.1101 178.23) 36%, oklch(19.03% 0.0175 144.85))",
+ "accent.focusRing": "oklch(70.437% 0.1101 178.23)",
+ "accent.border": "oklch(70.437% 0.1101 178.23)"
+ },
+ "mono": {
+ "bg": "oklch(18.22% 0 0)",
+ "bg.subtle": "oklch(20.019% 0 0)",
+ "bg.muted": "oklch(22.213% 0 0)",
+ "bg.panel": "oklch(22.213% 0 0)",
+ "bg.emphasized": "oklch(27.274% 0 0)",
+ "bg.inset": "oklch(18.22% 0 0)",
+ "bg.error": "color-mix(in oklab, oklch(71.115% 0.0934 19.64) 14%, oklch(20.019% 0 0))",
+ "bg.success": "color-mix(in oklab, oklch(76% 0.06 150) 14%, oklch(20.019% 0 0))",
+ "bg.warning": "color-mix(in oklab, oklch(79% 0.07 80) 14%, oklch(20.019% 0 0))",
+ "fg": "oklch(93.1% 0 0)",
+ "fg.muted": "oklch(71.547% 0 0)",
+ "fg.subtle": "oklch(53.824% 0 0)",
+ "fg.grid": "oklch(32.897% 0 0)",
+ "fg.error": "oklch(71.115% 0.0934 19.64)",
+ "fg.success": "oklch(76% 0.06 150)",
+ "fg.warning": "oklch(79% 0.07 80)",
+ "border": "oklch(28.908% 0 0)",
+ "border.subtle": "oklch(28.908% 0 0)",
+ "border.muted": "oklch(28.908% 0 0)",
+ "border.emphasized": "oklch(34.846% 0 0)",
+ "border.error": "oklch(71.115% 0.0934 19.64)",
+ "gray.contrast": "oklch(18.22% 0 0)",
+ "gray.fg": "oklch(93.1% 0 0)",
+ "gray.subtle": "oklch(28.908% 0 0)",
+ "gray.muted": "oklch(27.274% 0 0)",
+ "gray.emphasized": "oklch(34.846% 0 0)",
+ "gray.solid": "oklch(93.1% 0 0)",
+ "gray.focusRing": "oklch(68.622% 0 0)",
+ "gray.border": "oklch(34.846% 0 0)",
+ "brand.solid": "oklch(93.1% 0 0)",
+ "brand.contrast": "oklch(18.22% 0 0)",
+ "brand.fg": "oklch(93.1% 0 0)",
+ "brand.subtle": "color-mix(in oklab, oklch(93.1% 0 0) 16%, oklch(20.019% 0 0))",
+ "brand.muted": "color-mix(in oklab, oklch(93.1% 0 0) 26%, oklch(20.019% 0 0))",
+ "brand.emphasized": "color-mix(in oklab, oklch(93.1% 0 0) 36%, oklch(20.019% 0 0))",
+ "brand.focusRing": "oklch(68.622% 0 0)",
+ "brand.border": "oklch(93.1% 0 0)",
+ "accent.solid": "oklch(68.622% 0 0)",
+ "accent.contrast": "oklch(18.22% 0 0)",
+ "accent.fg": "oklch(68.622% 0 0)",
+ "accent.subtle": "color-mix(in oklab, oklch(68.622% 0 0) 16%, oklch(20.019% 0 0))",
+ "accent.muted": "color-mix(in oklab, oklch(68.622% 0 0) 26%, oklch(20.019% 0 0))",
+ "accent.emphasized": "color-mix(in oklab, oklch(68.622% 0 0) 36%, oklch(20.019% 0 0))",
+ "accent.focusRing": "oklch(68.622% 0 0)",
+ "accent.border": "oklch(68.622% 0 0)"
+ },
+ "ultradark": {
+ "bg": "oklch(0% 0 0)",
+ "bg.subtle": "oklch(11.492% 0 0)",
+ "bg.muted": "oklch(14.479% 0 0)",
+ "bg.panel": "oklch(14.479% 0 0)",
+ "bg.emphasized": "oklch(19.125% 0 0)",
+ "bg.inset": "oklch(0% 0 0)",
+ "bg.error": "color-mix(in oklab, oklch(71.161% 0.1812 22.84) 14%, oklch(11.492% 0 0))",
+ "bg.success": "color-mix(in oklab, oklch(76.5% 0.16 150.5) 14%, oklch(11.492% 0 0))",
+ "bg.warning": "color-mix(in oklab, oklch(79.5% 0.13 80) 14%, oklch(11.492% 0 0))",
+ "fg": "oklch(88.901% 0.0317 120.83)",
+ "fg.muted": "oklch(66.382% 0.0192 131.96)",
+ "fg.subtle": "oklch(47.041% 0.0184 127.12)",
+ "fg.grid": "oklch(23.929% 0 0)",
+ "fg.error": "oklch(71.161% 0.1812 22.84)",
+ "fg.success": "oklch(76.5% 0.16 150.5)",
+ "fg.warning": "oklch(79.5% 0.13 80)",
+ "border": "oklch(20.904% 0 0)",
+ "border.subtle": "oklch(20.904% 0 0)",
+ "border.muted": "oklch(20.904% 0 0)",
+ "border.emphasized": "oklch(26.862% 0 0)",
+ "border.error": "oklch(71.161% 0.1812 22.84)",
+ "gray.contrast": "oklch(0% 0 0)",
+ "gray.fg": "oklch(88.901% 0.0317 120.83)",
+ "gray.subtle": "oklch(21.779% 0 0)",
+ "gray.muted": "oklch(19.125% 0 0)",
+ "gray.emphasized": "oklch(26.862% 0 0)",
+ "gray.solid": "oklch(88.901% 0.0317 120.83)",
+ "gray.focusRing": "oklch(80.623% 0.1248 228.24)",
+ "gray.border": "oklch(26.862% 0 0)",
+ "brand.solid": "oklch(93.444% 0.19 125.56)",
+ "brand.contrast": "oklch(15.913% 0.0233 128.66)",
+ "brand.fg": "oklch(93.444% 0.19 125.56)",
+ "brand.subtle": "color-mix(in oklab, oklch(93.444% 0.19 125.56) 16%, oklch(11.492% 0 0))",
+ "brand.muted": "color-mix(in oklab, oklch(93.444% 0.19 125.56) 26%, oklch(11.492% 0 0))",
+ "brand.emphasized": "color-mix(in oklab, oklch(93.444% 0.19 125.56) 36%, oklch(11.492% 0 0))",
+ "brand.focusRing": "oklch(80.623% 0.1248 228.24)",
+ "brand.border": "oklch(93.444% 0.19 125.56)",
+ "accent.solid": "oklch(80.623% 0.1248 228.24)",
+ "accent.contrast": "oklch(17.416% 0.0256 235.84)",
+ "accent.fg": "oklch(80.623% 0.1248 228.24)",
+ "accent.subtle": "color-mix(in oklab, oklch(80.623% 0.1248 228.24) 16%, oklch(11.492% 0 0))",
+ "accent.muted": "color-mix(in oklab, oklch(80.623% 0.1248 228.24) 26%, oklch(11.492% 0 0))",
+ "accent.emphasized": "color-mix(in oklab, oklch(80.623% 0.1248 228.24) 36%, oklch(11.492% 0 0))",
+ "accent.focusRing": "oklch(80.623% 0.1248 228.24)",
+ "accent.border": "oklch(80.623% 0.1248 228.24)"
+ }
+}
diff --git a/invokeai/frontend/webv2/src/theme/__tokenResolve.ts b/invokeai/frontend/webv2/src/theme/__tokenResolve.ts
new file mode 100644
index 00000000000..0bdd9a12b38
--- /dev/null
+++ b/invokeai/frontend/webv2/src/theme/__tokenResolve.ts
@@ -0,0 +1,130 @@
+/**
+ * Test-only helper: resolve every semantic color token to its FINAL literal value
+ * (following `var(--chakra-colors-…)` references) for each workbench theme, straight
+ * from `system.getTokenCss()`. This mirrors what the browser computes for a given
+ * ``, so it is the ground truth used by the
+ * legacy-value gate in `system.test.ts`.
+ *
+ * Not imported by any app entry — excluded from the production bundle.
+ */
+
+type Block = Record;
+type Layer = Record;
+
+export interface TokenSystem {
+ getTokenCss: () => Record;
+ tokens: { getByName: (name: string) => { extensions: { cssVar: { var: string } } } | undefined };
+}
+
+const BASE_SEL = '&:where(html, .chakra-theme)';
+const DARK_SEL = '.dark &, .dark .chakra-theme:not(.light) &';
+const LIGHT_SEL = ':root &, .light &';
+const dataSel = (id: string): string => `&:root[data-theme=${id}]`;
+
+/** Selectors that apply to each theme, HIGHEST CSS priority first. */
+export const THEME_SELECTORS: Record = {
+ classic: [DARK_SEL, BASE_SEL], // default theme: no [data-theme=classic] rule
+ light: [dataSel('light'), LIGHT_SEL, BASE_SEL],
+ forest: [dataSel('forest'), DARK_SEL, BASE_SEL],
+ mono: [dataSel('mono'), DARK_SEL, BASE_SEL],
+ ultradark: [dataSel('ultradark'), DARK_SEL, BASE_SEL],
+};
+
+export const THEMES = Object.keys(THEME_SELECTORS);
+
+const VAR_REF = /var\((--chakra-colors-[A-Za-z0-9\\.-]+)\)/g;
+
+const layerOf = (sys: TokenSystem): Layer => sys.getTokenCss()['@layer tokens'];
+
+const resolveVar = (layer: Layer, theme: string, varName: string, seen: Set): string | undefined => {
+ for (const sel of THEME_SELECTORS[theme]) {
+ const block = layer[sel];
+ if (block && varName in block) {
+ return resolveValue(layer, theme, block[varName], seen);
+ }
+ }
+ return undefined;
+};
+
+const resolveValue = (layer: Layer, theme: string, value: string, seen: Set): string =>
+ value.replace(VAR_REF, (whole, varName: string) => {
+ if (seen.has(varName)) {
+ return whole;
+ }
+ const resolved = resolveVar(layer, theme, varName, new Set(seen).add(varName));
+ return resolved ?? whole;
+ });
+
+/** Final literal value of `colors.` in `theme`. */
+export const resolveToken = (sys: TokenSystem, theme: string, tokenName: string): string => {
+ const token = sys.tokens.getByName(`colors.${tokenName}`);
+ if (!token) {
+ throw new Error(`token not found: colors.${tokenName}`);
+ }
+ const value = resolveVar(layerOf(sys), theme, token.extensions.cssVar.var, new Set());
+ if (value === undefined) {
+ throw new Error(`could not resolve colors.${tokenName} for theme ${theme}`);
+ }
+ return value;
+};
+
+/** Resolve every token across every theme → { [theme]: { [token]: value } }. */
+export const resolveAll = (sys: TokenSystem, tokens: readonly string[]): Record> => {
+ const out: Record> = {};
+ for (const theme of THEMES) {
+ out[theme] = {};
+ for (const token of tokens) {
+ out[theme][token] = resolveToken(sys, theme, token);
+ }
+ }
+ return out;
+};
+
+/** The frozen public color-token contract — every name a component/recipe may consume. */
+export const CONSUMER_TOKENS = [
+ 'bg',
+ 'bg.subtle',
+ 'bg.muted',
+ 'bg.panel',
+ 'bg.emphasized',
+ 'bg.inset',
+ 'bg.error',
+ 'bg.success',
+ 'bg.warning',
+ 'fg',
+ 'fg.muted',
+ 'fg.subtle',
+ 'fg.grid',
+ 'fg.error',
+ 'fg.success',
+ 'fg.warning',
+ 'border',
+ 'border.subtle',
+ 'border.muted',
+ 'border.emphasized',
+ 'border.error',
+ 'gray.contrast',
+ 'gray.fg',
+ 'gray.subtle',
+ 'gray.muted',
+ 'gray.emphasized',
+ 'gray.solid',
+ 'gray.focusRing',
+ 'gray.border',
+ 'brand.solid',
+ 'brand.contrast',
+ 'brand.fg',
+ 'brand.subtle',
+ 'brand.muted',
+ 'brand.emphasized',
+ 'brand.focusRing',
+ 'brand.border',
+ 'accent.solid',
+ 'accent.contrast',
+ 'accent.fg',
+ 'accent.subtle',
+ 'accent.muted',
+ 'accent.emphasized',
+ 'accent.focusRing',
+ 'accent.border',
+] as const;
diff --git a/invokeai/frontend/webv2/src/theme/recipes.ts b/invokeai/frontend/webv2/src/theme/recipes.ts
new file mode 100644
index 00000000000..812a5b4e035
--- /dev/null
+++ b/invokeai/frontend/webv2/src/theme/recipes.ts
@@ -0,0 +1,333 @@
+import { defineRecipe, defineSlotRecipe } from '@chakra-ui/react';
+import { slotRecipes as chakraSlotRecipes } from '@chakra-ui/react/theme';
+
+/**
+ * Reusable, theme-aware recipes for the workbench design system.
+ *
+ * Recipes reference semantic tokens only, so every variant automatically tracks
+ * the active theme. Two kinds live here:
+ *
+ * 1. Overrides for Chakra's built-in component recipes (`tooltip`, `menu`,
+ * `select`, `combobox`, `dialog`) — registered in `system.ts` so every
+ * instance app-wide gets the workbench chrome with zero props at the call
+ * site.
+ * 2. Workbench-specific recipes (`panel`, `row`, `chip`, `fieldLabel`,
+ * `themeCard`) — consumed via the wrappers in `workbench/components/ui`
+ * with `useRecipe({ recipe })` / `useSlotRecipe({ recipe })`, which keeps
+ * them fully typed without the Chakra typegen step.
+ *
+ * Either way, this file is the single place where shared component styling is
+ * edited.
+ */
+
+/* -------------------------------------------------------------------------- *
+ * Built-in component overrides (registered in system.ts)
+ * -------------------------------------------------------------------------- */
+
+/** Tooltip chrome: raised surface with a hairline stroke instead of inverted fill. */
+export const tooltipSlotRecipe = defineSlotRecipe({
+ slots: ['content', 'arrowTip'],
+ base: {
+ content: {
+ '--tooltip-bg': 'colors.bg.muted',
+ bg: 'var(--tooltip-bg)',
+ borderColor: 'border.emphasized',
+ borderWidth: '1px',
+ boxShadow: 'lg',
+ color: 'fg',
+ },
+ arrowTip: {
+ borderColor: 'border.emphasized',
+ },
+ },
+});
+
+const dropdownContent = {
+ bg: 'bg.muted',
+ borderColor: 'border.emphasized',
+ borderRadius: 'lg',
+ borderWidth: '1px',
+ boxShadow: 'lg',
+ color: 'fg',
+};
+
+const dropdownItem = {
+ _highlighted: { bg: 'bg.subtle' },
+ _focusVisible: { outline: '2px solid', outlineColor: 'accent.solid', outlineOffset: '-2px' },
+};
+
+const dropdownGroupLabel = {
+ color: 'fg.subtle',
+ fontSize: '2xs',
+ fontWeight: '600',
+ letterSpacing: '0.02em',
+ textTransform: 'uppercase',
+};
+
+/** Menu / context-menu chrome: popover surface with an emphasized stroke. */
+export const menuSlotRecipe = defineSlotRecipe({
+ ...chakraSlotRecipes.menu,
+ base: {
+ ...chakraSlotRecipes.menu.base,
+ content: {
+ ...chakraSlotRecipes.menu.base?.content,
+ ...dropdownContent,
+ },
+ item: {
+ ...chakraSlotRecipes.menu.base?.item,
+ ...dropdownItem,
+ },
+ itemGroupLabel: {
+ ...chakraSlotRecipes.menu.base?.itemGroupLabel,
+ ...dropdownGroupLabel,
+ },
+ separator: {
+ ...chakraSlotRecipes.menu.base?.separator,
+ bg: 'border.subtle',
+ },
+ },
+});
+
+/** Select dropdown chrome: same surface and item treatment as menus. */
+export const selectSlotRecipe = defineSlotRecipe({
+ ...chakraSlotRecipes.select,
+ base: {
+ ...chakraSlotRecipes.select.base,
+ content: {
+ ...chakraSlotRecipes.select.base?.content,
+ ...dropdownContent,
+ },
+ item: {
+ ...chakraSlotRecipes.select.base?.item,
+ ...dropdownItem,
+ },
+ itemGroupLabel: {
+ ...chakraSlotRecipes.select.base?.itemGroupLabel,
+ ...dropdownGroupLabel,
+ },
+ },
+});
+
+/** Combobox chrome: kept aligned with Select for future searchable fields. */
+export const comboboxSlotRecipe = defineSlotRecipe({
+ ...chakraSlotRecipes.combobox,
+ base: {
+ ...chakraSlotRecipes.combobox.base,
+ content: {
+ ...chakraSlotRecipes.combobox.base?.content,
+ ...dropdownContent,
+ },
+ input: {
+ ...chakraSlotRecipes.combobox.base?.input,
+ _hover: { borderColor: 'border.emphasized' },
+ },
+ item: {
+ ...chakraSlotRecipes.combobox.base?.item,
+ ...dropdownItem,
+ },
+ itemGroupLabel: {
+ ...chakraSlotRecipes.combobox.base?.itemGroupLabel,
+ ...dropdownGroupLabel,
+ },
+ },
+});
+
+/** Dialog chrome: panel surface with a hairline stroke. */
+export const dialogSlotRecipe = defineSlotRecipe({
+ ...chakraSlotRecipes.dialog,
+ base: {
+ ...chakraSlotRecipes.dialog.base,
+ content: {
+ ...chakraSlotRecipes.dialog.base?.content,
+ borderColor: 'border.subtle',
+ borderWidth: '1px',
+ },
+ },
+});
+
+/** Slider marks: compact auxiliary labels for dense widget controls. */
+export const sliderSlotRecipe = defineSlotRecipe({
+ ...chakraSlotRecipes.slider,
+ base: {
+ ...chakraSlotRecipes.slider.base,
+ markerLabel: {
+ ...chakraSlotRecipes.slider.base?.markerLabel,
+ color: 'fg.subtle',
+ fontSize: '0.5rem',
+ lineHeight: '1',
+ },
+ },
+});
+
+/** Progress circle: add a compact icon-sized variant for dense chrome like tabs. */
+export const progressCircleSlotRecipe = defineSlotRecipe({
+ ...chakraSlotRecipes.progressCircle,
+ variants: {
+ ...chakraSlotRecipes.progressCircle.variants,
+ size: {
+ ...chakraSlotRecipes.progressCircle.variants?.size,
+ '2xs': {
+ circle: {
+ '--size': '16px',
+ '--thickness': '3px',
+ },
+ valueText: {
+ textStyle: '2xs',
+ },
+ },
+ },
+ },
+});
+
+/* -------------------------------------------------------------------------- *
+ * Workbench recipes (consumed through workbench/components/ui wrappers)
+ * -------------------------------------------------------------------------- */
+
+/** Bordered surface container — panels, cards, wells. */
+export const panelRecipe = defineRecipe({
+ base: {
+ bg: 'bg.subtle',
+ borderColor: 'border.subtle',
+ borderRadius: 'md',
+ borderWidth: '1px',
+ display: 'flex',
+ flexDirection: 'column',
+ minH: '0',
+ minW: '0',
+ },
+ variants: {
+ tone: {
+ surface: {},
+ raised: { bg: 'bg.muted' },
+ inset: { bg: 'bg.inset' },
+ control: { bg: 'bg.emphasized', borderColor: 'transparent' },
+ },
+ density: {
+ none: {},
+ sm: { gap: '1.5', p: '2' },
+ md: { gap: '2', p: '3' },
+ },
+ },
+ defaultVariants: { tone: 'surface', density: 'none' },
+});
+
+/** Interactive list / table row with hover, focus, and active fills. */
+export const rowRecipe = defineRecipe({
+ base: {
+ alignItems: 'center',
+ borderRadius: 'sm',
+ cursor: 'pointer',
+ display: 'flex',
+ gap: '2',
+ textAlign: 'start',
+ transition: 'background var(--wb-motion-duration-fast) ease, color var(--wb-motion-duration-fast) ease',
+ w: 'full',
+ _hover: { bg: 'bg.muted' },
+ _focusVisible: { outline: '2px solid', outlineColor: 'accent.solid', outlineOffset: '-2px' },
+ _disabled: { cursor: 'not-allowed', opacity: 0.5 },
+ },
+ variants: {
+ active: {
+ none: {},
+ muted: { bg: 'bg.muted' },
+ brand: { bg: 'brand.subtle', color: 'brand.fg', _hover: { bg: 'brand.subtle' } },
+ accent: { bg: 'accent.solid', color: 'accent.contrast', _hover: { bg: 'accent.solid' } },
+ },
+ },
+ defaultVariants: { active: 'none' },
+});
+
+/** Compact status chip with intent tones — queue counts, server state, versions. */
+export const chipRecipe = defineRecipe({
+ base: {
+ alignItems: 'center',
+ borderRadius: 'sm',
+ display: 'inline-flex',
+ flexShrink: '0',
+ fontSize: '2xs',
+ fontWeight: '500',
+ gap: '1.5',
+ px: '2',
+ py: '0.5',
+ whiteSpace: 'nowrap',
+ },
+ variants: {
+ tone: {
+ neutral: {},
+ brand: { bg: 'brand.subtle', color: 'brand.fg' },
+ accent: { color: 'accent.solid' },
+ error: { color: 'fg.error' },
+ success: { color: 'fg.success' },
+ warning: { color: 'fg.warning' },
+ },
+ },
+ defaultVariants: { tone: 'neutral' },
+});
+
+/** Compact uppercase field label shared across widget forms and the settings modal. */
+export const fieldLabelRecipe = defineRecipe({
+ base: {
+ color: 'fg.muted',
+ fontSize: '2xs',
+ fontWeight: '600',
+ letterSpacing: '0.02em',
+ textTransform: 'uppercase',
+ },
+});
+
+/** Selectable theme swatch card used by the Settings appearance picker. */
+export const themeCardRecipe = defineSlotRecipe({
+ slots: ['root', 'preview', 'swatch', 'body', 'name', 'description', 'indicator'],
+ base: {
+ root: {
+ alignItems: 'stretch',
+ bg: 'bg.subtle',
+ borderColor: 'border.subtle',
+ borderRadius: 'lg',
+ borderWidth: '1px',
+ cursor: 'pointer',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '2.5',
+ overflow: 'hidden',
+ p: '3',
+ textAlign: 'left',
+ transition:
+ 'border-color var(--wb-motion-duration-fast) ease, background var(--wb-motion-duration-fast) ease, transform var(--wb-motion-duration-fast) ease',
+ _hover: { borderColor: 'border.emphasized' },
+ _focusVisible: { outline: '2px solid', outlineColor: 'accent.solid', outlineOffset: '2px' },
+ },
+ preview: {
+ borderColor: 'border.subtle',
+ borderRadius: 'md',
+ borderWidth: '1px',
+ display: 'flex',
+ h: '8',
+ overflow: 'hidden',
+ },
+ swatch: { flex: '1' },
+ body: { alignItems: 'flex-start', display: 'flex', flexDirection: 'column', gap: '0.5' },
+ name: { color: 'fg', fontSize: 'sm', fontWeight: '600' },
+ description: { color: 'fg.subtle', fontSize: '2xs', lineHeight: '1.3' },
+ indicator: {
+ alignItems: 'center',
+ borderRadius: 'full',
+ color: 'accent.solid',
+ display: 'flex',
+ h: '4',
+ justifyContent: 'center',
+ opacity: 0,
+ w: '4',
+ },
+ },
+ variants: {
+ selected: {
+ true: {
+ root: { borderColor: 'accent.solid', bg: 'bg.muted' },
+ indicator: { opacity: 1 },
+ },
+ false: {},
+ },
+ },
+ defaultVariants: { selected: false },
+});
diff --git a/invokeai/frontend/webv2/src/theme/system.test.ts b/invokeai/frontend/webv2/src/theme/system.test.ts
new file mode 100644
index 00000000000..a4c76d9de1b
--- /dev/null
+++ b/invokeai/frontend/webv2/src/theme/system.test.ts
@@ -0,0 +1,140 @@
+import { describe, expect, it } from 'vitest';
+
+import legacyBaseline from './__fixtures__/legacyTokenBaseline.json';
+import { CONSUMER_TOKENS, resolveToken, THEMES, type TokenSystem } from './__tokenResolve';
+import { progressCircleSlotRecipe } from './recipes';
+import { system } from './system';
+
+const sys = system as unknown as TokenSystem;
+const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const;
+
+const lightnessOf = (oklch: string): number => {
+ const match = /oklch\(\s*([\d.]+)%/.exec(oklch);
+ if (!match) {
+ throw new Error(`not a plain oklch value: ${oklch}`);
+ }
+ return Number(match[1]);
+};
+
+describe('color token contract — legacy-value gate', () => {
+ // Primary gate: every consumer token must resolve to the EXACT value it had
+ // before the ramp refactor. The baseline was captured from the previous system
+ // on the working tree before any change (see __fixtures__/legacyTokenBaseline.json).
+ for (const theme of THEMES) {
+ for (const token of CONSUMER_TOKENS) {
+ it(`${theme}: ${token} is unchanged`, () => {
+ const expected = (legacyBaseline as Record>)[theme][token];
+ expect(resolveToken(sys, theme, token)).toBe(expected);
+ });
+ }
+ }
+});
+
+describe('ramp + mapping structure', () => {
+ it('emits every neutral ramp step for every theme', () => {
+ for (const theme of THEMES) {
+ for (const step of STEPS) {
+ expect(resolveToken(sys, theme, `neutral.${step}`)).toMatch(/^oklch\(/);
+ }
+ }
+ });
+
+ it('maps the light surface ladder to the corrected (non-mirrored) steps', () => {
+ // The point-1 fix: light bg sits at neutral.200, not the lightest step.
+ expect(resolveToken(sys, 'light', 'bg')).toBe(resolveToken(sys, 'light', 'neutral.200'));
+ expect(resolveToken(sys, 'light', 'bg.subtle')).toBe(resolveToken(sys, 'light', 'neutral.50'));
+ expect(resolveToken(sys, 'light', 'bg.muted')).toBe(resolveToken(sys, 'light', 'neutral.100'));
+ expect(resolveToken(sys, 'light', 'border')).toBe(resolveToken(sys, 'light', 'neutral.300'));
+ expect(resolveToken(sys, 'light', 'border.emphasized')).toBe(resolveToken(sys, 'light', 'neutral.400'));
+ });
+
+ it('maps the dark surface ladder onto the ramp', () => {
+ expect(resolveToken(sys, 'classic', 'bg')).toBe(resolveToken(sys, 'classic', 'neutral.950'));
+ expect(resolveToken(sys, 'classic', 'bg.subtle')).toBe(resolveToken(sys, 'classic', 'neutral.900'));
+ expect(resolveToken(sys, 'classic', 'fg')).toBe(resolveToken(sys, 'classic', 'neutral.50'));
+ expect(resolveToken(sys, 'classic', 'border')).toBe(resolveToken(sys, 'classic', 'neutral.600'));
+ });
+
+ it('emits surface/text/border tokens via per-theme conditions only — never the leaky mode selectors', () => {
+ // Chakra's `_light` selector (`:root &, .light &`) matches under EVERY theme via its
+ // `:root &` arm. A surface token placed there leaks its light value into the dark
+ // themes (the classic-renders-light regression). These tokens must live ONLY in the
+ // base + `[data-theme=…]` selectors.
+ const layer = sys.getTokenCss()['@layer tokens'];
+ const LEAKY = [':root &, .light &', '.dark &, .dark .chakra-theme:not(.light) &'];
+ const surfaceTokens = [
+ 'bg',
+ 'bg.subtle',
+ 'bg.muted',
+ 'bg.panel',
+ 'fg',
+ 'fg.muted',
+ 'fg.subtle',
+ 'border',
+ 'border.subtle',
+ 'border.muted',
+ 'border.emphasized',
+ ];
+ for (const name of surfaceTokens) {
+ const token = sys.tokens.getByName(`colors.${name}`);
+ const cssVar = token?.extensions.cssVar.var ?? '';
+ for (const selector of LEAKY) {
+ expect(layer[selector]?.[cssVar], `${name} must not be emitted under "${selector}"`).toBeUndefined();
+ }
+ }
+ });
+
+ it('keeps each ramp strictly decreasing in lightness 50 → 950', () => {
+ for (const theme of THEMES) {
+ const ls = STEPS.map((step) => lightnessOf(resolveToken(sys, theme, `neutral.${step}`)));
+ for (let i = 1; i < ls.length; i++) {
+ expect(ls[i], `${theme} neutral.${STEPS[i]} should be darker than neutral.${STEPS[i - 1]}`).toBeLessThan(
+ ls[i - 1]
+ );
+ }
+ }
+ });
+});
+
+describe('native Chakra integration', () => {
+ it('adds an icon-sized progress circle variant', () => {
+ expect(progressCircleSlotRecipe.variants?.size?.['2xs']).toMatchObject({
+ circle: { '--size': '16px', '--thickness': '3px' },
+ });
+ });
+
+ it('resolves every gray virtual-palette key under both dark and light', () => {
+ const keys = ['contrast', 'fg', 'subtle', 'muted', 'emphasized', 'solid', 'focusRing', 'border'];
+ for (const theme of ['classic', 'light']) {
+ for (const key of keys) {
+ expect(resolveToken(sys, theme, `gray.${key}`)).toBeTruthy();
+ }
+ }
+ });
+
+ it('leaves stock Chakra hue palettes untouched in Phase 1', () => {
+ // Phase 2 will theme these; for now they must remain Chakra defaults so model
+ // badges / destructive actions keep their stock colors.
+ expect(sys.tokens.getByName('colors.red.500')).toBeTruthy();
+ expect(sys.tokens.getByName('colors.blue.500')).toBeTruthy();
+ expect(sys.tokens.getByName('colors.purple.500')).toBeTruthy();
+ });
+});
+
+describe('brand palette derives from its two seeds (like accent)', () => {
+ it('fg/border equal solid; subtle/muted/emphasized mix solid into the surface', () => {
+ for (const theme of THEMES) {
+ const solid = resolveToken(sys, theme, 'brand.solid');
+ const surface = resolveToken(sys, theme, 'bg.subtle');
+ expect(resolveToken(sys, theme, 'brand.fg')).toBe(solid);
+ expect(resolveToken(sys, theme, 'brand.border')).toBe(solid);
+ for (const [key, pct] of [
+ ['subtle', 16],
+ ['muted', 26],
+ ['emphasized', 36],
+ ] as const) {
+ expect(resolveToken(sys, theme, `brand.${key}`)).toBe(`color-mix(in oklab, ${solid} ${pct}%, ${surface})`);
+ }
+ }
+ });
+});
diff --git a/invokeai/frontend/webv2/src/theme/system.ts b/invokeai/frontend/webv2/src/theme/system.ts
new file mode 100644
index 00000000000..86a769dde05
--- /dev/null
+++ b/invokeai/frontend/webv2/src/theme/system.ts
@@ -0,0 +1,290 @@
+import { createSystem, defaultConfig, defineConfig } from '@chakra-ui/react';
+
+import {
+ comboboxSlotRecipe,
+ dialogSlotRecipe,
+ menuSlotRecipe,
+ progressCircleSlotRecipe,
+ selectSlotRecipe,
+ sliderSlotRecipe,
+ tooltipSlotRecipe,
+} from './recipes';
+import { DEFAULT_THEME, DEFAULT_THEME_ID, type NeutralStep, THEMES, type ThemeDefinition } from './themes';
+
+/**
+ * Workbench design system.
+ *
+ * Each theme (`themes.ts`) is authored as one neutral ramp (`neutral.50…neutral.950`,
+ * lightest → darkest) plus a few seeds (brand, accent, status) and four off-ramp
+ * neutrals (`inset`, `fill`, `grid`, `control`). This module turns that into two
+ * token layers:
+ *
+ * 1. The **ramp** is emitted verbatim as conditional semantic tokens
+ * (`neutral.*`), one value per theme keyed on a custom `[data-theme=]`
+ * condition. Chakra base `tokens.colors` only hold plain strings, so anything
+ * that varies per theme must live in `semanticTokens`.
+ * 2. The **semantic contract** (`bg`, `fg.muted`, `border.emphasized`, the
+ * `gray`/`brand`/`accent` palettes, …) is theme-agnostic: it references ramp
+ * steps and flips by light/dark mode only. `bg` is `neutral.950` in dark mode and
+ * `neutral.200` in light — the light ramp is not a mirror of the dark one, because
+ * light panels go *whiter* than the app background.
+ *
+ * `ThemeController` sets `data-theme` (and the `.dark`/`.light` class) on ``,
+ * so switching themes is a single attribute change with zero React re-render.
+ * Components reference only the semantic tokens — never the ramp or a theme.
+ *
+ * Re-pointing Chakra's neutral `gray` palette at the ramp makes every built-in
+ * component (Dialog, Menu, Input, Button, Tooltip, Badge) follow the active theme
+ * with no per-component overrides.
+ */
+
+const NON_DEFAULT_THEMES = THEMES.filter((theme) => theme.id !== DEFAULT_THEME_ID);
+
+/** `light` -> `themeLight`, `ultradark` -> `themeUltradark`. */
+const conditionName = (id: string): string => `theme${id.charAt(0).toUpperCase()}${id.slice(1)}`;
+
+type TokenValue = { value: Record };
+type Compute = (theme: ThemeDefinition) => string;
+
+/** Build a semantic-token value object: default theme as `base`, the rest as `[data-theme]` conditions. */
+const colorToken = (compute: Compute): TokenValue => {
+ const value: Record = { base: compute(DEFAULT_THEME) };
+ for (const theme of NON_DEFAULT_THEMES) {
+ value[`_${conditionName(theme.id)}`] = compute(theme);
+ }
+ return { value };
+};
+
+/** Blend `pct`% of one computed color into another — used for derived hover/tint steps. */
+const mix = (top: Compute, pct: number, bottom: Compute): TokenValue =>
+ colorToken((theme) => `color-mix(in oklab, ${top(theme)} ${pct}%, ${bottom(theme)})`);
+
+const LIGHT_FALLBACK_THEME = THEMES.find((theme) => theme.colorScheme === 'light') ?? DEFAULT_THEME;
+
+/**
+ * Like `colorToken`, but additionally shadows Chakra's `_light`/`_dark`
+ * class-conditional values. Nested palette tokens (`gray.*`) deep-merge with
+ * `defaultConfig` instead of replacing it, so without these keys the default
+ * gray values would survive the merge and outrank our zero-specificity `base`
+ * value whenever `ThemeController` sets the `.dark`/`.light` class. The explicit
+ * `:root[data-theme=…]` conditions still win over both.
+ */
+const grayToken = (compute: Compute): TokenValue => {
+ const token = colorToken(compute);
+ token.value._light = compute(LIGHT_FALLBACK_THEME);
+ token.value._dark = compute(DEFAULT_THEME);
+ return token;
+};
+
+/**
+ * A token that reads a ramp step, chosen per theme by `colorScheme`. Emitted as
+ * per-`[data-theme]` conditions, NOT Chakra's `_light`/`_dark`: the built-in
+ * `_light` selector is `:root &, .light &`, and its `:root &` arm matches under
+ * EVERY theme — so a mode-flip token leaks its light value into the dark themes.
+ * Per-theme conditions sidestep the cascade entirely (this is how the pre-refactor
+ * system worked, and why dark themes rendered correctly).
+ */
+const ref = (step: NeutralStep): string => `{colors.neutral.${step}}`;
+const stepRef = (darkStep: NeutralStep, lightStep: NeutralStep): TokenValue =>
+ colorToken((theme) => ref(theme.colorScheme === 'light' ? lightStep : darkStep));
+
+const STEPS: NeutralStep[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
+
+/** The default panel surface of a theme — `bg.subtle`'s step. Used as the floor for tints. */
+const surface: Compute = (theme) =>
+ theme.colorScheme === 'light' ? theme.colors.neutral[50] : theme.colors.neutral[900];
+
+// Seed accessors.
+const danger: Compute = (theme) => theme.colors.danger;
+const success: Compute = (theme) => theme.colors.success;
+const warning: Compute = (theme) => theme.colors.warning;
+const brandSolid: Compute = (theme) => theme.colors.brand.solid;
+const accentSolid: Compute = (theme) => theme.colors.accent.solid;
+
+/** The neutral ramp, emitted as `neutral.50…neutral.950`, one value per theme. */
+const neutralRamp = Object.fromEntries(STEPS.map((step) => [step, colorToken((theme) => theme.colors.neutral[step])]));
+
+/**
+ * The semantic-token contract. Backgrounds/foregrounds/borders reference ramp
+ * steps (`stepRef`); the four off-ramp neutrals and the status/identity hues read
+ * their per-theme seed directly. Where a Chakra built-in name exists we use it
+ * verbatim so built-ins inherit the theme for free.
+ */
+const semanticColors = {
+ neutral: neutralRamp,
+
+ // Surface ladder. Light panels are whiter than the app bg, so the light steps
+ // are not a mirror of the dark ones.
+ bg: stepRef(950, 200),
+ 'bg.subtle': stepRef(900, 50),
+ 'bg.muted': stepRef(800, 100),
+ 'bg.panel': stepRef(800, 100),
+ 'bg.emphasized': colorToken((theme) => theme.colors.control),
+ 'bg.inset': colorToken((theme) => theme.colors.inset),
+ // Soft status fills for alerts/banners, mixed into the panel surface.
+ 'bg.error': mix(danger, 14, surface),
+ 'bg.success': mix(success, 14, surface),
+ 'bg.warning': mix(warning, 14, surface),
+
+ // Foreground.
+ fg: stepRef(50, 950),
+ 'fg.muted': stepRef(300, 700),
+ 'fg.subtle': stepRef(400, 500),
+ 'fg.grid': colorToken((theme) => theme.colors.grid),
+ 'fg.error': colorToken(danger),
+ 'fg.success': colorToken(success),
+ 'fg.warning': colorToken(warning),
+
+ // Borders.
+ border: stepRef(600, 300),
+ 'border.subtle': stepRef(600, 300),
+ 'border.muted': stepRef(600, 300),
+ 'border.emphasized': stepRef(500, 400),
+ 'border.error': colorToken(danger),
+
+ /**
+ * Chakra's default `colorPalette` is `gray`; re-pointing its virtual-palette
+ * keys at the ramp makes every un-palettized component (ghost buttons, menu
+ * items, badges, …) theme-aware with zero props.
+ *
+ * Palette tokens must stay NESTED — the `colorPalette` virtual-token map is
+ * built from the nested structure and ignores flat dotted keys. Because nesting
+ * deep-merges with the defaults, every gray key shadows the default
+ * `_light`/`_dark` values via `grayToken`.
+ */
+ gray: {
+ contrast: grayToken((theme) =>
+ theme.colorScheme === 'light' ? theme.colors.neutral[200] : theme.colors.neutral[950]
+ ),
+ fg: grayToken((theme) => (theme.colorScheme === 'light' ? theme.colors.neutral[950] : theme.colors.neutral[50])),
+ subtle: grayToken((theme) => theme.colors.fill),
+ muted: grayToken((theme) => theme.colors.control),
+ emphasized: grayToken((theme) =>
+ theme.colorScheme === 'light' ? theme.colors.neutral[400] : theme.colors.neutral[500]
+ ),
+ solid: grayToken((theme) => (theme.colorScheme === 'light' ? theme.colors.neutral[950] : theme.colors.neutral[50])),
+ focusRing: grayToken(accentSolid),
+ border: grayToken((theme) =>
+ theme.colorScheme === 'light' ? theme.colors.neutral[400] : theme.colors.neutral[500]
+ ),
+ },
+ /**
+ * Invoke identity palette (lime). Authored from two seeds (`solid` + `contrast`),
+ * like `accent`; the rest derive. NOTE: `brand.fg` is the bright `solid` fill, so
+ * `brand.fg` on `brand.subtle` reads well on the dark themes but is low-contrast in
+ * the light theme — brand is meant for emphasis fills, not body text.
+ */
+ brand: {
+ solid: colorToken(brandSolid),
+ contrast: colorToken((theme) => theme.colors.brand.contrast),
+ fg: colorToken(brandSolid),
+ subtle: mix(brandSolid, 16, surface),
+ muted: mix(brandSolid, 26, surface),
+ emphasized: mix(brandSolid, 36, surface),
+ focusRing: colorToken(accentSolid),
+ border: colorToken(brandSolid),
+ },
+ /** Selection / focus palette (blue). Use via `accent.solid` or `colorPalette="accent"`. */
+ accent: {
+ solid: colorToken(accentSolid),
+ contrast: colorToken((theme) => theme.colors.accent.contrast),
+ fg: colorToken(accentSolid),
+ subtle: mix(accentSolid, 16, surface),
+ muted: mix(accentSolid, 26, surface),
+ emphasized: mix(accentSolid, 36, surface),
+ focusRing: colorToken(accentSolid),
+ border: colorToken(accentSolid),
+ },
+};
+
+// `:root` raises specificity above the `.dark`/`.light` colorScheme classes so
+// an explicit theme always beats the class-conditional fallback values.
+const themeConditions = Object.fromEntries(
+ NON_DEFAULT_THEMES.map((theme) => [conditionName(theme.id), `:root[data-theme=${theme.id}]`])
+);
+
+const motionDurationToken = (base: string): TokenValue => ({ value: { base, _reduceMotion: '1ms' } });
+const motionAnimationToken = (base: string): TokenValue => ({ value: { base, _reduceMotion: 'none' } });
+
+const config = defineConfig({
+ conditions: { ...themeConditions, reduceMotion: ':root[data-reduce-motion=true]' },
+ globalCss: {
+ 'html, body, #root': {
+ height: '100%',
+ },
+ body: {
+ bg: 'bg',
+ color: 'fg',
+ fontFamily: 'body',
+ margin: 0,
+ minHeight: '720px',
+ minWidth: '960px',
+ overflow: 'hidden',
+ },
+ ':root': {
+ '--wb-motion-duration-fast': '0.12s',
+ '--wb-motion-duration-medium': '0.15s',
+ '--wb-motion-duration-slow': '0.2s',
+ '--wb-motion-animation-iteration-count': 'infinite',
+ },
+ // Keep durations non-zero: Ark presence waits for animation lifecycle events.
+ // Chakra recipe motion is covered by conditional duration/animation tokens below.
+ ':root[data-reduce-motion="true"]': {
+ '--wb-motion-duration-fast': '1ms',
+ '--wb-motion-duration-medium': '1ms',
+ '--wb-motion-duration-slow': '1ms',
+ '--wb-motion-animation-iteration-count': '1',
+ scrollBehavior: 'auto !important',
+ },
+ ':root[data-reduce-motion="true"] .chakra-skeleton': {
+ animation: 'none !important',
+ },
+ },
+ theme: {
+ tokens: {
+ fonts: {
+ body: {
+ value: "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
+ },
+ heading: {
+ value: "Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif",
+ },
+ },
+ },
+ semanticTokens: {
+ animations: {
+ bounce: motionAnimationToken('bounce 1s infinite'),
+ ping: motionAnimationToken('ping 1s cubic-bezier(0, 0, 0.2, 1) infinite'),
+ pulse: motionAnimationToken('pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite'),
+ spin: motionAnimationToken('spin 1s linear infinite'),
+ },
+ colors: semanticColors,
+ durations: {
+ fastest: motionDurationToken('50ms'),
+ faster: motionDurationToken('100ms'),
+ fast: motionDurationToken('150ms'),
+ moderate: motionDurationToken('200ms'),
+ slow: motionDurationToken('300ms'),
+ slower: motionDurationToken('400ms'),
+ slowest: motionDurationToken('500ms'),
+ },
+ },
+ // Chrome-level overrides for Chakra's built-in components, so popover and
+ // dialog surfaces are consistent everywhere without per-instance props.
+ slotRecipes: {
+ combobox: comboboxSlotRecipe,
+ dialog: dialogSlotRecipe,
+ menu: menuSlotRecipe,
+ progressCircle: progressCircleSlotRecipe,
+ select: selectSlotRecipe,
+ slider: sliderSlotRecipe,
+ tooltip: tooltipSlotRecipe,
+ },
+ },
+});
+
+export const system = createSystem(defaultConfig, config);
+
+/** Theme metadata re-exported so UI can import a single module. */
+export { THEMES, THEMES_BY_ID, DEFAULT_THEME, DEFAULT_THEME_ID, previewSwatches } from './themes';
+export type { ThemeColors, ThemeDefinition, NeutralStep } from './themes';
diff --git a/invokeai/frontend/webv2/src/theme/themes.ts b/invokeai/frontend/webv2/src/theme/themes.ts
new file mode 100644
index 00000000000..082823b5b89
--- /dev/null
+++ b/invokeai/frontend/webv2/src/theme/themes.ts
@@ -0,0 +1,265 @@
+import type { WorkbenchThemeId } from '@workbench/types';
+
+/**
+ * Steps of the neutral ramp. `50` is the lightest, `950` the darkest — the same
+ * absolute orientation Chakra/Tailwind use, so the ramp can be aliased onto the
+ * built-in `gray` palette without surprises.
+ */
+export type NeutralStep = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
+
+/**
+ * One workbench theme, expressed as a small set of concrete OKLch values.
+ *
+ * The bulk of a theme is the `base` ramp — a single neutral scale from which the
+ * semantic-token layer in `system.ts` derives every background, foreground, and
+ * border (`bg → neutral.950` in dark mode, `fg → neutral.50`, …). Everything else is a
+ * handful of seeds:
+ *
+ * - `brand` / `accent` — the two identity hues (lime action, blue selection);
+ * - `danger` / `success` / `warning` — status intents;
+ * - `inset` / `fill` / `grid` / `control` — four neutrals whose *elevation rank*
+ * differs from theme to theme, so they cannot sit on a single shared ramp step
+ * (e.g. `inset` recesses below the app background in the light theme but lifts
+ * above it in the dark themes). They are kept as explicit per-theme values and
+ * consumed by name through `bg.inset`, `fg.grid`, `gray.subtle`, `bg.emphasized`.
+ *
+ * Adding a theme is therefore: author one ramp + the seeds. No component changes.
+ */
+export interface ThemeColors {
+ /** Neutral ramp, lightest (`50`) → darkest (`950`). Source of all bg/fg/border. */
+ neutral: Record;
+ /** Invoke identity (lime). Two seeds; the palette's other steps derive in `system.ts`. */
+ brand: { solid: string; contrast: string };
+ /** Selection / focus (blue). */
+ accent: { solid: string; contrast: string };
+ /** Destructive / error intent. */
+ danger: string;
+ /** Positive / success intent. */
+ success: string;
+ /** Caution / warning intent. */
+ warning: string;
+ /** Recessed work-area floor framed by the chrome (`bg.inset`). Off-ramp. */
+ inset: string;
+ /** Subtle neutral fill for hovered/ghost controls (`gray.subtle`). Off-ramp. */
+ fill: string;
+ /** Dot-grid decoration drawn on inset surfaces (`fg.grid`). Off-ramp. */
+ grid: string;
+ /** Control / chip fill inside panels (`bg.emphasized`). Off-ramp. */
+ control: string;
+}
+
+export interface ThemeDefinition {
+ id: WorkbenchThemeId;
+ label: string;
+ description: string;
+ /** Native color-scheme hint for form controls, scrollbars, and `