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..fd7df90a256 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,43 @@ 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:
+ - '.github/workflows/frontend-tests.yml'
+ - '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
+
+ - name: install Chromium
+ if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm exec playwright install chromium'
+ shell: bash
+
+ - name: vitest browser
+ if: ${{ steps.changed-files.outputs.webv2_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm test:browser'
+ 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/docs/superpowers/specs/2026-07-15-canvas-context-menu-groups-design.md b/docs/superpowers/specs/2026-07-15-canvas-context-menu-groups-design.md
new file mode 100644
index 00000000000..b8485c4e75a
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-15-canvas-context-menu-groups-design.md
@@ -0,0 +1,27 @@
+# Canvas Context Menu Groups Design
+
+## Goal
+
+Restore the legacy canvas context-menu organization with labeled layer and canvas groups while keeping Delete as the final action.
+
+## Design
+
+- Canvas-surface context menus use Chakra `Menu.ItemGroup` and `Menu.ItemGroupLabel` primitives with the established uppercase, subtle label styling.
+- A context menu opened over a layer labels the layer-scoped actions with the singular layer type: Raster Layer, Control Layer, Inpaint Mask, or Regional Guidance.
+- Canvas-wide actions are labeled Canvas. Save to Gallery remains in this canvas group and appears before the terminal Delete action.
+- Delete remains a separated standalone danger action at the bottom so no non-destructive action appears beneath it.
+- A context menu opened over empty canvas space shows only the Canvas group.
+- Layer-panel kebab and row context menus remain unchanged because the legacy panel menus were ungrouped.
+
+## Boundaries
+
+- Reuse existing translations for Canvas and singular layer types.
+- Preserve all existing handlers, disabled states, submenu behavior, and menu positioning.
+- Add no state, effects, dependencies, or action-registry coupling.
+
+## Verification
+
+- Add regression coverage for the surface-menu group model and render order.
+- Verify each layer type resolves to the matching legacy label.
+- Verify the optional Canvas group precedes the terminal danger section and panel menus remain ungrouped when labels are omitted.
+- Run the focused context-menu tests, frontend typecheck, formatting check, and full frontend test suite.
diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py
index 3092f5ab71a..67e80ff55e3 100644
--- a/invokeai/app/api/dependencies.py
+++ b/invokeai/app/api/dependencies.py
@@ -42,6 +42,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,
@@ -189,6 +190,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(
@@ -223,6 +225,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 89d2f9a7395..5e6dffce337 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
@@ -7,6 +7,7 @@
from invokeai.app.api.auth_dependencies import AdminUserOrDefault, CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.routers.image_move_maintenance import assert_image_move_maintenance_inactive
+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,
@@ -39,6 +40,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 _get_workflow_call_root_queue_item(queue_item: SessionQueueItem) -> SessionQueueItem:
if queue_item.root_item_id is None:
return queue_item
@@ -64,7 +118,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
@@ -95,6 +149,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",
@@ -154,6 +220,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.
@@ -166,7 +235,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}")
@@ -425,10 +496,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
@@ -446,10 +520,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
@@ -467,13 +544,18 @@ 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. Returns global counts; non-admin users additionally
get their own pending/in_progress counts (so the UI can show an X/Y badge) and cannot see the
current item's identifiers 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:
@@ -575,14 +657,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)
- 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}")
-
- # 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 bdaf641c1c1..19392a107bd 100644
--- a/invokeai/app/api/sockets.py
+++ b/invokeai/app/api/sockets.py
@@ -29,6 +29,7 @@
ModelInstallCompleteEvent,
ModelInstallDownloadProgressEvent,
ModelInstallDownloadsCompleteEvent,
+ ModelInstallDownloadStartedEvent,
ModelInstallErrorEvent,
ModelInstallStartedEvent,
ModelLoadCompleteEvent,
@@ -84,6 +85,7 @@ class BulkDownloadSubscriptionEvent(BaseModel):
DownloadStartedEvent,
ModelLoadStartedEvent,
ModelLoadCompleteEvent,
+ ModelInstallDownloadStartedEvent,
ModelInstallDownloadProgressEvent,
ModelInstallDownloadsCompleteEvent,
ModelInstallStartedEvent,
@@ -95,6 +97,16 @@ class BulkDownloadSubscriptionEvent(BaseModel):
BULK_DOWNLOAD_EVENTS = {BulkDownloadStartedEvent, BulkDownloadCompleteEvent, BulkDownloadErrorEvent}
WORKFLOW_EVENTS = {WorkflowCreatedEvent, WorkflowUpdatedEvent, WorkflowDeletedEvent}
+MODEL_INSTALL_EVENTS = (
+ ModelInstallDownloadStartedEvent,
+ ModelInstallDownloadProgressEvent,
+ ModelInstallDownloadsCompleteEvent,
+ ModelInstallStartedEvent,
+ ModelInstallCompleteEvent,
+ ModelInstallCancelledEvent,
+ ModelInstallErrorEvent,
+)
+
class SocketIO:
_sub_queue = "subscribe_queue"
@@ -490,7 +502,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 ed02d75eae3..e56f2bdb91b 100644
--- a/invokeai/app/api_app.py
+++ b/invokeai/app/api_app.py
@@ -27,6 +27,7 @@
images,
model_manager,
model_relationships,
+ projects,
recall_parameters,
session_queue,
style_presets,
@@ -188,6 +189,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 54f9d82b786..d46289a65e5 100644
--- a/invokeai/app/services/invocation_services.py
+++ b/invokeai/app/services/invocation_services.py
@@ -35,6 +35,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
@@ -79,6 +80,7 @@ def __init__(
style_preset_image_files: "StylePresetImageFileStorageBase",
workflow_thumbnails: "WorkflowThumbnailServiceBase",
client_state_persistence: "ClientStatePersistenceABC",
+ project_records: "ProjectRecordsStorageBase",
users: "UserServiceBase",
image_moves: "ImageMoveService | None" = None,
):
@@ -113,4 +115,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 a07abf10ee1..a3882524fbb 100644
--- a/invokeai/app/services/session_queue/session_queue_base.py
+++ b/invokeai/app/services/session_queue/session_queue_base.py
@@ -44,12 +44,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
@@ -79,6 +79,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.
@@ -207,6 +208,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 51980d68f3b..9f4f82817f7 100644
--- a/invokeai/app/services/session_queue/session_queue_sqlite.py
+++ b/invokeai/app/services/session_queue/session_queue_sqlite.py
@@ -279,7 +279,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,),
)
@@ -310,10 +310,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,
@@ -323,22 +322,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,
@@ -348,10 +353,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
@@ -1117,6 +1129,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
@@ -1130,6 +1143,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)
@@ -1143,20 +1160,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
@@ -1164,18 +1184,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_migrator/migrations/migration_32.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py
index 9036048bced..4d64addc7d9 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
index 6f941154c58..44d54803d1f 100644
--- a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_33.py
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_33.py
@@ -1,70 +1,61 @@
+"""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 IF NOT EXISTS image_subfolder_move_jobs (
- id INTEGER PRIMARY KEY,
- state TEXT NOT NULL CHECK (
- state IN ('planned', 'moving', 'moved', 'committed', 'error')
- ),
+ 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')),
- error_message TEXT
- );
- """
- )
- cursor.execute(
- """--sql
- CREATE TABLE IF NOT EXISTS image_subfolder_move_items (
- job_id INTEGER NOT NULL REFERENCES image_subfolder_move_jobs(id),
- image_name TEXT NOT NULL REFERENCES images(image_name),
- old_subfolder TEXT NOT NULL,
- new_subfolder TEXT NOT NULL,
- is_intermediate BOOLEAN NOT NULL DEFAULT FALSE,
- old_path TEXT,
- new_path TEXT,
- old_thumbnail_path TEXT,
- new_thumbnail_path TEXT,
- state TEXT NOT NULL CHECK (
- state IN ('planned', 'moved', 'committed', 'error')
- ),
- error_message TEXT,
- PRIMARY KEY (job_id, image_name)
+ PRIMARY KEY (user_id, project_id),
+ FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
"""
)
cursor.execute(
"""--sql
- CREATE INDEX IF NOT EXISTS idx_image_subfolder_move_items_job_state
- ON image_subfolder_move_items(job_id, state);
- """
- )
- cursor.execute(
- """--sql
- CREATE INDEX IF NOT EXISTS idx_image_subfolder_move_items_image_name
- ON image_subfolder_move_items(image_name);
- """
- )
- cursor.execute(
- """--sql
- CREATE TRIGGER IF NOT EXISTS tg_image_subfolder_move_jobs_updated_at
- AFTER UPDATE
- ON image_subfolder_move_jobs FOR EACH ROW
+ CREATE TRIGGER tg_projects_updated_at
+ AFTER UPDATE ON projects
+ FOR EACH ROW
BEGIN
- UPDATE image_subfolder_move_jobs
+ UPDATE projects
SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
- WHERE id = old.id;
+ 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,
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_34.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_34.py
new file mode 100644
index 00000000000..82d428ad248
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_34.py
@@ -0,0 +1,72 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration34Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute(
+ """--sql
+ CREATE TABLE IF NOT EXISTS image_subfolder_move_jobs (
+ id INTEGER PRIMARY KEY,
+ state TEXT NOT NULL CHECK (
+ state IN ('planned', 'moving', 'moved', 'committed', 'error')
+ ),
+ 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')),
+ error_message TEXT
+ );
+ """
+ )
+ cursor.execute(
+ """--sql
+ CREATE TABLE IF NOT EXISTS image_subfolder_move_items (
+ job_id INTEGER NOT NULL REFERENCES image_subfolder_move_jobs(id),
+ image_name TEXT NOT NULL REFERENCES images(image_name),
+ old_subfolder TEXT NOT NULL,
+ new_subfolder TEXT NOT NULL,
+ is_intermediate BOOLEAN NOT NULL DEFAULT FALSE,
+ old_path TEXT,
+ new_path TEXT,
+ old_thumbnail_path TEXT,
+ new_thumbnail_path TEXT,
+ state TEXT NOT NULL CHECK (
+ state IN ('planned', 'moved', 'committed', 'error')
+ ),
+ error_message TEXT,
+ PRIMARY KEY (job_id, image_name)
+ );
+ """
+ )
+ cursor.execute(
+ """--sql
+ CREATE INDEX IF NOT EXISTS idx_image_subfolder_move_items_job_state
+ ON image_subfolder_move_items(job_id, state);
+ """
+ )
+ cursor.execute(
+ """--sql
+ CREATE INDEX IF NOT EXISTS idx_image_subfolder_move_items_image_name
+ ON image_subfolder_move_items(image_name);
+ """
+ )
+ cursor.execute(
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_image_subfolder_move_jobs_updated_at
+ AFTER UPDATE
+ ON image_subfolder_move_jobs FOR EACH ROW
+ BEGIN
+ UPDATE image_subfolder_move_jobs
+ SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE id = old.id;
+ END;
+ """
+ )
+
+
+def build_migration_34() -> Migration:
+ return Migration(
+ from_version=33,
+ to_version=34,
+ callback=Migration34Callback(),
+ )
diff --git a/invokeai/backend/image_util/segment_anything/segment_anything_2_pipeline.py b/invokeai/backend/image_util/segment_anything/segment_anything_2_pipeline.py
index 79a7d91a7bf..47fdc6c41e8 100644
--- a/invokeai/backend/image_util/segment_anything/segment_anything_2_pipeline.py
+++ b/invokeai/backend/image_util/segment_anything/segment_anything_2_pipeline.py
@@ -2,8 +2,6 @@
import torch
from PIL import Image
-
-# Import SAM2 components - these should be available in transformers 4.56.0+
from transformers.models.sam2 import Sam2Model
from transformers.models.sam2.processing_sam2 import Sam2Processor
@@ -101,7 +99,6 @@ def segment(
masks = self._sam2_processor.post_process_masks(
masks=outputs.pred_masks,
original_sizes=processed_inputs.original_sizes,
- reshaped_input_sizes=processed_inputs.reshaped_input_sizes,
)
# There should be only one batch.
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 e2801e9e39a..4191149397c 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"],
@@ -2905,6 +2926,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": {
@@ -3795,7 +3834,9 @@
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/EmptyModelCacheResponse"
+ }
}
}
}
@@ -3823,7 +3864,12 @@
}
}
}
- }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
},
"post": {
"tags": ["model_manager"],
@@ -6621,7 +6667,12 @@
}
}
}
- }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
}
},
"/api/v1/app/external_providers/config": {
@@ -6644,7 +6695,12 @@
}
}
}
- }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
}
},
"/api/v1/app/external_providers/config/{provider_id}": {
@@ -7054,6 +7110,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": {
@@ -7632,6 +7706,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": {
@@ -7694,6 +7786,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": {
@@ -7756,6 +7866,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": {
@@ -9697,6 +9825,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"],
@@ -14722,6 +15067,11 @@
"title": "High Watermark",
"default": 0
},
+ "cache_used": {
+ "type": "integer",
+ "title": "Cache Used",
+ "default": 0
+ },
"in_cache": {
"type": "integer",
"title": "In Cache",
@@ -22597,6 +22947,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": {
@@ -59795,6 +60160,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/features/controlLayers/components/StagingArea/__mocks__/mockStagingAreaApp.ts b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/__mocks__/mockStagingAreaApp.ts
index e0b56d5a439..bd848127756 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/__mocks__/mockStagingAreaApp.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/__mocks__/mockStagingAreaApp.ts
@@ -165,6 +165,7 @@ export const createMockQueueItemStatusChangedEvent = (
status: 'completed',
error_type: null,
error_message: null,
+ error_traceback: null,
} as S['QueueItemStatusChangedEvent'],
overrides
);
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index f938b7f0f2c..6ec09bef539 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;
@@ -2704,6 +2727,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;
@@ -5349,6 +5424,11 @@ export type components = {
* @default 0
*/
high_watermark?: number;
+ /**
+ * Cache Used
+ * @default 0
+ */
+ cache_used?: number;
/**
* In Cache
* @default 0
@@ -9325,6 +9405,13 @@ export type components = {
*/
field: string;
};
+ /** EmptyModelCacheResponse */
+ EmptyModelCacheResponse: {
+ /** Models Cleared */
+ models_cleared: number;
+ /** Bytes Freed */
+ bytes_freed: number;
+ };
/** EnqueueBatchResult */
EnqueueBatchResult: {
/**
@@ -25266,6 +25353,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.
@@ -34544,6 +34746,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: {
@@ -35158,7 +35380,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;
};
@@ -35655,7 +35880,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
+ "application/json": components["schemas"]["EmptyModelCacheResponse"];
};
};
};
@@ -37839,6 +38064,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: {
@@ -38206,7 +38433,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 */
@@ -38238,7 +38468,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 */
@@ -38270,7 +38503,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 */
@@ -39422,6 +39658,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/AGENTS.md b/invokeai/frontend/webv2/AGENTS.md
new file mode 100644
index 00000000000..e69de29bb2d
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..ef74665c231
--- /dev/null
+++ b/invokeai/frontend/webv2/index.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+ Invoke V7 Workbench
+
+
+
+
+
+
+
diff --git a/invokeai/frontend/webv2/package.json b/invokeai/frontend/webv2/package.json
new file mode 100644
index 00000000000..e3b8b58ae21
--- /dev/null
+++ b/invokeai/frontend/webv2/package.json
@@ -0,0 +1,70 @@
+{
+ "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 .",
+ "i18n:report": "node scripts/i18n-report.mjs",
+ "i18n:report:verbose": "node scripts/i18n-report.mjs --verbose",
+ "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 --config vitest.config.mts",
+ "test:all": "pnpm run test && pnpm run test:browser",
+ "test:browser": "vitest run --config vitest.browser.config.mts"
+ },
+ "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",
+ "ag-psd": "^31.0.1",
+ "i18next": "^25.7.3",
+ "i18next-http-backend": "^3.0.2",
+ "lucide-react": "^1.21.0",
+ "perfect-freehand": "^1.2.3",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "react-hook-tanstack-virtual": "^0.0.4",
+ "react-i18next": "^16.5.0",
+ "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",
+ "@vitest/browser-playwright": "4.1.9",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxfmt": "^0.56.0",
+ "oxlint": "^1.71.0",
+ "playwright": "1.61.1",
+ "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..5dcfad18af3
--- /dev/null
+++ b/invokeai/frontend/webv2/pnpm-lock.yaml
@@ -0,0 +1,3949 @@
+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)
+ ag-psd:
+ specifier: ^31.0.1
+ version: 31.0.1
+ i18next:
+ specifier: ^25.7.3
+ version: 25.10.10(typescript@6.0.3)
+ i18next-http-backend:
+ specifier: ^3.0.2
+ version: 3.0.6
+ lucide-react:
+ specifier: ^1.21.0
+ version: 1.21.0(react@19.2.7)
+ perfect-freehand:
+ specifier: ^1.2.3
+ version: 1.2.3
+ 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-i18next:
+ specifier: ^16.5.0
+ version: 16.6.6(i18next@25.10.10(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
+ 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))
+ '@vitest/browser-playwright':
+ specifier: 4.1.9
+ version: 4.1.9(playwright@1.61.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))(vitest@4.1.9)
+ 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
+ playwright:
+ specifier: 1.61.1
+ version: 1.61.1
+ 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)(@vitest/browser-playwright@4.1.9)(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'}
+
+ '@blazediff/core@1.9.1':
+ resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==}
+
+ '@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'}
+
+ '@polka/url@1.0.0-next.29':
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
+ '@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/browser-playwright@4.1.9':
+ resolution: {integrity: sha512-Bq1rOGf9waevzG3EOkO/dene6bvKTUsZMVg8S1i+WH3JcMjuXEjiahP9rAqZRELUqjBySOJsvvSWqK/B3wjKQw==}
+ peerDependencies:
+ playwright: '*'
+ vitest: 4.1.9
+
+ '@vitest/browser@4.1.9':
+ resolution: {integrity: sha512-j1BKtWmPcqpMhmx/L9EPLgAJpCb0zKfwoWLmqBbxaogCXHjOwHFSEoHCBfnGtx93xKQwilZ26m+UOsHqHMkRNg==}
+ peerDependencies:
+ vitest: 4.1.9
+
+ '@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==}
+
+ ag-psd@31.0.1:
+ resolution: {integrity: sha512-/byWVGIrKv0Jqq3lleaRji2QdrCsAhbG4Ansc9nWkH1ghOBbujnhdlRXMfK2A750mInXbPLsgpHhrtfq9lLWkA==}
+
+ 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==}
+
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+ 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'}
+
+ cross-fetch@4.1.0:
+ resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==}
+
+ 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.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ 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==}
+
+ html-parse-stringify@3.0.1:
+ resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
+
+ i18next-http-backend@3.0.6:
+ resolution: {integrity: sha512-mBOqy8993jtqAoj6XaI1XeC/8/9v6EPS+681ziegrPvTB0DoaCY7PpTS0SpY56qLMoS4OI1TZEM2Zf59zNh05w==}
+
+ i18next@25.10.10:
+ resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==}
+ peerDependencies:
+ typescript: ^5 || ^6
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ 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==}
+
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
+ 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-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: 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
+
+ pako@2.1.0:
+ resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
+
+ 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'}
+
+ playwright-core@1.61.1:
+ resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.61.1:
+ resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ pngjs@7.0.0:
+ resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
+ engines: {node: '>=14.19.0'}
+
+ 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-i18next@16.6.6:
+ resolution: {integrity: sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==}
+ peerDependencies:
+ i18next: '>= 25.10.9'
+ react: '>= 16.8.0'
+ react-dom: '*'
+ react-native: '*'
+ typescript: ^5 || ^6
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ typescript:
+ optional: true
+
+ 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==}
+
+ sirv@3.0.2:
+ resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
+ engines: {node: '>=18'}
+
+ 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'}
+
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+ 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
+
+ void-elements@3.1.0:
+ resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
+ engines: {node: '>=0.10.0'}
+
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+ 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
+
+ '@blazediff/core@1.9.1': {}
+
+ '@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': {}
+
+ '@polka/url@1.0.0-next.29': {}
+
+ '@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/browser-playwright@4.1.9(playwright@1.61.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))(vitest@4.1.9)':
+ dependencies:
+ '@vitest/browser': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))(vitest@4.1.9)
+ '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))
+ playwright: 1.61.1
+ tinyrainbow: 3.1.0
+ vitest: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))
+ transitivePeerDependencies:
+ - bufferutil
+ - msw
+ - utf-8-validate
+ - vite
+
+ '@vitest/browser@4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))(vitest@4.1.9)':
+ dependencies:
+ '@blazediff/core': 1.9.1
+ '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))
+ '@vitest/utils': 4.1.9
+ magic-string: 0.30.21
+ pngjs: 7.0.0
+ sirv: 3.0.2
+ tinyrainbow: 3.1.0
+ vitest: 4.1.9(@types/node@26.0.1)(@vitest/browser-playwright@4.1.9)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))
+ ws: 8.21.0
+ transitivePeerDependencies:
+ - bufferutil
+ - msw
+ - utf-8-validate
+ - vite
+
+ '@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': {}
+
+ ag-psd@31.0.1:
+ dependencies:
+ base64-js: 1.5.1
+ pako: 2.1.0
+
+ 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
+
+ base64-js@1.5.1: {}
+
+ 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
+
+ cross-fetch@4.1.0:
+ dependencies:
+ node-fetch: 2.7.0
+ transitivePeerDependencies:
+ - encoding
+
+ 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.2:
+ optional: true
+
+ 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
+
+ html-parse-stringify@3.0.1:
+ dependencies:
+ void-elements: 3.1.0
+
+ i18next-http-backend@3.0.6:
+ dependencies:
+ cross-fetch: 4.1.0
+ transitivePeerDependencies:
+ - encoding
+
+ i18next@25.10.10(typescript@6.0.3):
+ dependencies:
+ '@babel/runtime': 7.29.7
+ optionalDependencies:
+ typescript: 6.0.3
+
+ 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
+
+ mrmime@2.0.1: {}
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.15: {}
+
+ node-fetch@2.7.0:
+ dependencies:
+ whatwg-url: 5.0.0
+
+ 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
+
+ pako@2.1.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: {}
+
+ playwright-core@1.61.1: {}
+
+ playwright@1.61.1:
+ dependencies:
+ playwright-core: 1.61.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
+ pngjs@7.0.0: {}
+
+ 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-i18next@16.6.6(i18next@25.10.10(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3):
+ dependencies:
+ '@babel/runtime': 7.29.7
+ html-parse-stringify: 3.0.1
+ i18next: 25.10.10(typescript@6.0.3)
+ react: 19.2.7
+ use-sync-external-store: 1.6.0(react@19.2.7)
+ optionalDependencies:
+ react-dom: 19.2.7(react@19.2.7)
+ typescript: 6.0.3
+
+ 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: {}
+
+ sirv@3.0.2:
+ dependencies:
+ '@polka/url': 1.0.0-next.29
+ mrmime: 2.0.1
+ totalist: 3.0.1
+
+ 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: {}
+
+ totalist@3.0.1: {}
+
+ tr46@0.0.3: {}
+
+ 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)(@vitest/browser-playwright@4.1.9)(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
+ '@vitest/browser-playwright': 4.1.9(playwright@1.61.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7))(vitest@4.1.9)
+ transitivePeerDependencies:
+ - msw
+
+ void-elements@3.1.0: {}
+
+ webidl-conversions@3.0.1: {}
+
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
+ 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/public/locales/ar.json b/invokeai/frontend/webv2/public/locales/ar.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/ar.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/az.json b/invokeai/frontend/webv2/public/locales/az.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/az.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/bg.json b/invokeai/frontend/webv2/public/locales/bg.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/bg.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/de.json b/invokeai/frontend/webv2/public/locales/de.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/de.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/en-GB.json b/invokeai/frontend/webv2/public/locales/en-GB.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/en-GB.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/en.json b/invokeai/frontend/webv2/public/locales/en.json
new file mode 100644
index 00000000000..844d3da2bd9
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/en.json
@@ -0,0 +1,1654 @@
+{
+ "common": {
+ "actions": "Actions",
+ "active": "Active",
+ "add": "Add",
+ "all": "All",
+ "apply": "Apply",
+ "archived": "Archived",
+ "asc": "Asc",
+ "assets": "Assets",
+ "cancel": "Cancel",
+ "cancelFailed": "Cancel failed",
+ "canceled": "Canceled",
+ "clear": "Clear",
+ "clearSearch": "Clear search",
+ "close": "Close",
+ "configure": "Configure",
+ "copy": "Copy",
+ "crop": "Crop",
+ "clipboardBlocked": "Clipboard access was blocked by the browser.",
+ "couldNotCopy": "Could not copy",
+ "countOfTotal": "{{count}} of {{total}}",
+ "created": "Created",
+ "delete": "Delete",
+ "desc": "Desc",
+ "discard": "Discard",
+ "discardAll": "Discard All",
+ "done": "Done",
+ "duplicate": "Duplicate",
+ "edit": "Edit",
+ "error": "Error",
+ "export": "Export",
+ "failed": "Failed",
+ "generating": "Generating",
+ "graph": "Graph",
+ "images": "Images",
+ "import": "Import",
+ "id": "ID",
+ "item": "Item",
+ "json": "JSON",
+ "lastSaved": "Last saved",
+ "loadingApplication": "Loading Application",
+ "negative": "Negative",
+ "name": "Name",
+ "nothingHereYet": "Nothing here yet.",
+ "none": "None",
+ "notYet": "Not yet",
+ "open": "Open",
+ "pageNumber": "Page {{page}}",
+ "pages": "Pages",
+ "previousPage": "Previous page",
+ "prompt": "Prompt",
+ "project": "Project",
+ "rename": "Rename",
+ "required": "Required",
+ "refresh": "Refresh",
+ "reload": "Reload",
+ "reset": "Reset",
+ "retry": "Retry",
+ "saved": "Saved",
+ "save": "Save",
+ "seed": "Seed",
+ "settings": "Settings",
+ "nextPage": "Next page",
+ "somethingWentWrong": "Something went wrong.",
+ "swap": "Swap",
+ "status": {
+ "canceled": "Canceled",
+ "completed": "Completed",
+ "failed": "Failed",
+ "pending": "Pending"
+ },
+ "total": "Total",
+ "unknownTime": "Unknown time",
+ "view": "View",
+ "viewJson": "View JSON",
+ "widgetViewUnavailable": "Widget view unavailable."
+ },
+ "app": {
+ "name": "Invoke AI",
+ "nameWithVersion": "{{name}} {{version}}"
+ },
+ "auth": {
+ "account": "Account",
+ "accountSettings": "Account settings",
+ "accountUpdated": "Account updated",
+ "administrator": "Administrator",
+ "changePassword": "Change password",
+ "confirmNewPassword": "Confirm new password",
+ "confirmPassword": "Confirm password",
+ "couldNotCreateAdmin": "Could not create the administrator account.",
+ "couldNotUpdateAccount": "Could not update your account.",
+ "createAdminAccount": "Create administrator account",
+ "currentPassword": "Current password",
+ "displayNameHelp": "Optional — shown instead of your email.",
+ "hidePassword": "Hide password",
+ "keepSignedIn": "Keep me signed in for a week",
+ "leaveBlankKeepPassword": "Leave blank to keep your password.",
+ "password": "Password",
+ "passwordGeneratedDescription": "Reveal it with the eye icon and store it somewhere safe.",
+ "passwordStrength": {
+ "moderate": "Moderate",
+ "strong": "Strong",
+ "weak": "Weak"
+ },
+ "profileDisplayNameHelp": "Shown instead of your email across the workspace.",
+ "sessionExpired": "Your session expired. Sign in again to continue.",
+ "setupFooter": "You can add more users later from the user menu.",
+ "setupSubtitle": "Create the administrator account to get started.",
+ "setupTitle": "Set up Invoke",
+ "showPassword": "Show password",
+ "signIn": "Sign in",
+ "signInFailed": "Sign-in failed. Check your email and password.",
+ "signInSubtitle": "Sign in to your workspace to continue.",
+ "signedInAs": "Signed in as {{email}}",
+ "signOut": "Sign out",
+ "welcomeTitle": "Welcome to Invoke",
+ "yourPassword": "Your password"
+ },
+ "splash": {
+ "artworkBy": "Artwork by {{artist}}",
+ "copyright": "© {{year}} Invoke AI | All rights reserved",
+ "loadingApplication": "Loading Application",
+ "loadingWorkspace": "Loading workspace",
+ "openingProject": "Opening project",
+ "tagline": "Image Generation for Creatives"
+ },
+ "notifications": {
+ "empty": "Successful operations, errors, and system messages appear here.",
+ "labelWithCount": "Notifications: {{label}}",
+ "newCount_one": "{{count}} new",
+ "newCount_other": "{{count}} new",
+ "totalCount_one": "{{count}} total",
+ "totalCount_other": "{{count}} total",
+ "kind": {
+ "error": "Error",
+ "info": "Info",
+ "success": "Success"
+ },
+ "markRead": "Mark Read"
+ },
+ "projects": {
+ "allSavedAlreadyOpen": "All saved projects are already open.",
+ "closeProjectLabel": "Close {{name}}",
+ "couldNotOpen": "Could not open project",
+ "couldNotOpenDescription": "\"{{name}}\" could not be loaded from the server.",
+ "createNewProject": "Create new project",
+ "deleteFailed": "Delete failed",
+ "deleteProject": "Delete project",
+ "deleteProjectQuestion": "Delete project?",
+ "deleteProjectCardBody": "Delete \"{{name}}\"? The project is removed from the server permanently.",
+ "deleteProjectTabBody": "Delete \"{{name}}\"? The project and its saved copy on the server are removed permanently. To keep it in your library, close the tab instead.",
+ "deleteProjectWithEllipsis": "Delete project…",
+ "duplicateFailed": "Duplicate failed",
+ "editedRelative": "Edited {{time}}",
+ "exportFailed": "Export failed",
+ "failedToLoad": "Failed to load your projects.",
+ "importFailed": "Import failed",
+ "importWithEllipsis": "Import…",
+ "newProject": "New project",
+ "noSavedProjects": "No saved projects yet.",
+ "openProject": "Open project",
+ "openProjectLabel": "Open {{name}}",
+ "projectDetails": "Project details",
+ "renameFailed": "Rename failed",
+ "renameWithEllipsis": "Rename…",
+ "projectDuplicated": "Project duplicated",
+ "projectDuplicatedDescription": "\"{{name}}\" was created."
+ },
+ "users": {
+ "accountCount_one": "{{count}} account in this workspace.",
+ "accountCount_other": "{{count}} accounts in this workspace.",
+ "activeLabel": "{{name}} active",
+ "addUser": "Add user",
+ "admin": "Admin",
+ "adminOnly": "User management is available to administrators only.",
+ "administrator": "Administrator",
+ "administratorHelp": "Can manage users and all workspace settings.",
+ "backendRejectedChange": "The backend rejected the change.",
+ "backendRejectedRequest": "The backend rejected the request.",
+ "cannotChangeSelfRole": "You cannot change your own role.",
+ "cannotDeactivateSelf": "You cannot deactivate your own account.",
+ "cannotDeleteSelf": "You cannot delete your own account.",
+ "couldNotActivate": "Could not activate the user",
+ "couldNotCreate": "Could not create the user.",
+ "couldNotDeactivate": "Could not deactivate the user",
+ "couldNotDelete": "Could not delete the user",
+ "couldNotGeneratePassword": "Could not generate a password",
+ "couldNotLoad": "Could not load users.",
+ "couldNotUpdate": "Could not update the user.",
+ "createUser": "Create user",
+ "created": "User created",
+ "deleted": "User deleted",
+ "deleteBody": "Delete {{name}}? Their account is removed permanently.",
+ "deleteTitle": "Delete user?",
+ "deleteUser": "Delete user",
+ "deleteUserNamed": "Delete {{name}}",
+ "description": "Manage workspace accounts and roles.",
+ "displayName": "Display name",
+ "displayNameHelp": "Optional — shown instead of the email.",
+ "editUser": "Edit user",
+ "editUserNamed": "Edit {{name}}",
+ "email": "Email",
+ "generate": "Generate",
+ "lastSignIn": "Last sign-in",
+ "leaveBlankPassword": "Leave blank to keep the current password.",
+ "manageUsers": "Manage users",
+ "management": "User management",
+ "never": "Never",
+ "newPassword": "New password",
+ "password": "Password",
+ "passwordGenerated": "Password generated",
+ "passwordGeneratedDescription": "Reveal it with the eye icon and share it with the user securely.",
+ "role": "Role",
+ "saveChanges": "Save changes",
+ "thisUser": "this user",
+ "title": "Users",
+ "updated": "User updated",
+ "user": "User",
+ "you": "You"
+ },
+ "graphPreview": {
+ "graphJsonLabel": "{{title}} graph JSON",
+ "inputCount_one": "{{count}} input",
+ "inputCount_other": "{{count}} inputs",
+ "invokeRoute": "Invoke {{route}}",
+ "noCompiledGraph": "No compiled graph is available for \"{{graphId}}\" yet.",
+ "nodes": "Nodes",
+ "title": "{{title}} Graph Preview"
+ },
+ "hotkeys": {
+ "addHotkey": "Add hotkey",
+ "bindings": "Hotkey bindings",
+ "cancelEdit": "Cancel hotkey edit",
+ "categories": {
+ "app": "App",
+ "canvas": "Canvas",
+ "gallery": "Gallery",
+ "viewer": "Viewer",
+ "workflows": "Workflows"
+ },
+ "conflictsWith": "Conflicts with {{title}}",
+ "custom": "Custom",
+ "deleteHotkey": "Delete hotkey",
+ "description": "Account-bound keybinds. Project-level overrides can layer on later without changing hotkey ids.",
+ "disable": "Disable",
+ "disabled": "Disabled",
+ "duplicateBinding": "Duplicate binding in this hotkey",
+ "pending": "Pending",
+ "pressKeys": "Press keys",
+ "resetAll": "Reset all",
+ "resetHotkey": "Reset hotkey",
+ "saveEdit": "Save hotkey edit",
+ "searchPlaceholder": "Search hotkeys",
+ "title": "Hotkeys"
+ },
+ "models": {
+ "accessToken": "Access Token",
+ "accessTokenForDownload": "Access token for this download",
+ "accessTokenHelp": "Attached to the next Pull only. For saved HuggingFace, Civitai, and provider keys, use the API Keys tab.",
+ "accessTokenPlaceholder": "Only needed for protected sources",
+ "actions": "Model actions",
+ "addImage": "Add image",
+ "addModels": "Add Models",
+ "addModelsResults": "Add models results",
+ "addTriggerPhrase": "Add a trigger phrase...",
+ "allBases": "All bases",
+ "allModels": "All models",
+ "apiKey": "API key",
+ "apiKeyConfigured": "API key configured",
+ "apiKeyFor": "{{title}} API key",
+ "apiKeys": "API Keys",
+ "apiKeysDescription": "Keys are used when installing protected models and when generating with external API models.",
+ "base": "Base",
+ "baseArchitecture": "Base Architecture",
+ "baseUrlFor": "{{title}} base URL",
+ "backendDisconnectedProgressStale": "Backend disconnected. Progress may be stale.",
+ "bundleInstallQueued": "Bundle install queued",
+ "bundleInstallQueuedDescription_one": "{{name}}: {{count}} install queued.",
+ "bundleInstallQueuedDescription_other": "{{name}}: {{count}} installs queued.",
+ "bytePlusApiKeyPlaceholder": "API key from BytePlus console",
+ "bulkDeleteBody_one": "Delete {{count}} selected model? Database records are removed, and files inside the InvokeAI models directory are deleted.",
+ "bulkDeleteBody_other": "Delete {{count}} selected models? Database records are removed, and files inside the InvokeAI models directory are deleted.",
+ "bulkDeleteConfirm_one": "Delete {{count}} Model",
+ "bulkDeleteConfirm_other": "Delete {{count}} Models",
+ "bulkDeleteFailed": "Bulk delete failed",
+ "bulkDeletePartialDescription": "{{deleted}} deleted; {{failed}} failed: {{error}}",
+ "cacheEmptied": "Model cache emptied",
+ "cleanupOrphaned": "Clean up orphaned models…",
+ "clearSelection": "Clear selection",
+ "convert": "Convert",
+ "convertBody": "Convert \"{{name}}\" to the diffusers format in place? The original checkpoint file is replaced by a diffusers folder.",
+ "convertToDiffusers": "Convert to diffusers",
+ "couldNotLoad": "Could not load models",
+ "couldNotLoadInstallQueue": "Could not load the install queue",
+ "couldNotLoadStarterModels": "Could not load starter models",
+ "customizedForThisModel": "Customized for this model",
+ "customizeDefaultField": "Customize {{field}} for this model",
+ "civitaiKey": "Civitai key",
+ "civitaiKeyDescription": "Attached automatically when installing civitai.com models that require login. Stored in this browser only.",
+ "civitaiKeyPlaceholder": "32-character key from civitai.com/user/account",
+ "cancelAllInstalls": "Cancel all installs",
+ "cancelFailed": "Cancel failed",
+ "cancelInstall": "Cancel install",
+ "clearFinished": "Clear finished",
+ "defaultFieldInherited": {
+ "cfgRescale": "App default: 0 (off)",
+ "cfgScale": "App default: 7",
+ "guidance": "Uses the generator default",
+ "height": "App default: 1024",
+ "preprocessor": "Chosen automatically per image",
+ "scheduler": "App default: euler_a",
+ "steps": "App default: 30",
+ "vaePrecision": "App default: fp16",
+ "weight": "App default: 0.75",
+ "width": "App default: 1024"
+ },
+ "defaultFields": {
+ "cfgRescale": "CFG Rescale",
+ "cfgScale": "CFG Scale",
+ "guidance": "Guidance",
+ "height": "Height",
+ "preprocessor": "Preprocessor",
+ "scheduler": "Scheduler",
+ "steps": "Steps",
+ "vaePrecision": "VAE Precision",
+ "weight": "Weight",
+ "width": "Width"
+ },
+ "defaultSettings": "Default Settings",
+ "defaultSettingsHelp": "Applied automatically when this model is selected. Off = use the app default.",
+ "defaultSettingsSaved": "Default settings saved",
+ "deleteBody": "Delete \"{{name}}\"? The database record is removed, and the model files are deleted if they live inside the InvokeAI models directory.",
+ "deleteModel": "Delete model",
+ "deleted": "Models deleted",
+ "deletedDescription_one": "{{count}} model deleted.",
+ "deletedDescription_other": "{{count}} models deleted.",
+ "deleteSelectedTitle": "Delete selected models",
+ "deleteSelectedWithBytes": "Delete Selected ({{bytes}})",
+ "description": "Description",
+ "details": "Model Details",
+ "dismissResults": "Dismiss results",
+ "dismissScanResults": "Dismiss scan results",
+ "downloadProgress": "Download progress",
+ "editing": "Editing",
+ "emptyCache": "Empty model cache",
+ "failedToEmptyCache": "Failed to empty model cache",
+ "failedToLinkModels": "Failed to link models.",
+ "failedToLoadExternalProviders": "Failed to load external providers.",
+ "failedToLoadRelatedModels": "Failed to load related models.",
+ "failedToRemoveModelImage": "Failed to remove model image.",
+ "failedToSaveDefaults": "Failed to save default settings.",
+ "failedToScanOrphaned": "Failed to scan for orphaned models.",
+ "failedToUnlink": "Failed to unlink models.",
+ "failedToUpdateTriggerPhrases": "Failed to update trigger phrases.",
+ "failedToUploadModelImage": "Failed to upload model image.",
+ "fileNotFoundOnDisk": "The file for this model was not found on disk.",
+ "fileCount_one": "{{count}} file",
+ "fileCount_other": "{{count}} files",
+ "filesInRepo_one": "{{count}} file in {{repo}}",
+ "filesInRepo_other": "{{count}} files in {{repo}}",
+ "fileSize": "File Size",
+ "filterAndSortStarterModels": "Filter and sort starter models",
+ "filterFiles": "Filter files",
+ "filterAndSort": "Filter and sort models",
+ "filterResults": "Filter results",
+ "hash": "Hash",
+ "huggingFaceKeyDescription": "For gated repos like FLUX. Stored on the InvokeAI server and verified with HuggingFace.",
+ "huggingFaceRejectedToken": "HuggingFace rejected this token. Check that it has read access.",
+ "huggingFaceToken": "HuggingFace token",
+ "installAll": "Install all",
+ "installAllCount": "Install all ({{count}})",
+ "installFailed": "Install failed",
+ "installJobs": "Install jobs",
+ "installJobSummary_one": "{{count}} job · no active installs",
+ "installJobSummary_other": "{{count}} jobs · no active installs",
+ "installInPlace": "Install in place",
+ "installQueue": "Install Queue",
+ "installed": "Installed",
+ "installsDependencies_one": " (installs {{count}} dependency)",
+ "installsDependencies_other": " (installs {{count}} dependencies)",
+ "invalidDefaultSettings": "Invalid default settings.",
+ "invalidTriggerPhrase": "Invalid trigger phrase.",
+ "keyStatus": {
+ "configured": "Configured",
+ "invalid": "Invalid",
+ "saved": "Saved",
+ "unknown": "Not set",
+ "valid": "Valid"
+ },
+ "library": "Model library",
+ "libraryMaintenance": "Model library maintenance",
+ "manager": "Model Manager",
+ "manageApiKeys": "Manage API keys",
+ "missingFilesCount": "Missing files ({{count}})",
+ "modelAndDependenciesQueued_one": "{{name}} and {{count}} dependency",
+ "modelAndDependenciesQueued_other": "{{name}} and {{count}} dependencies",
+ "modelCoverAlt": "{{name}} cover",
+ "modelImage": "Model image",
+ "modelImageUpdated": "Model image updated",
+ "modelInstallQueued": "Model install queued",
+ "modelManager": "Model manager",
+ "modelUpdated": "Model updated",
+ "modelType": "Model Type",
+ "noInstallableModelFiles": "This HuggingFace repo has no installable model files.",
+ "noInstallsDescription": "Model installs appear here while they download and import.",
+ "noInstallsYet": "No installs yet",
+ "noModelFilesFound": "No model files found in {{path}}.",
+ "noModelFilesFoundTitle": "No model files found",
+ "noRelatedModels": "No related models linked yet.",
+ "noStarterModelsPull": "No starter models match - press Pull to install the source you entered.",
+ "noStarterModelsSearch": "No starter models match your search.",
+ "noneInstalled": "No models installed",
+ "noneInstalledDescription": "Use Add Models to install your first model.",
+ "noneMatchFilters": "No models match your filters",
+ "noneMatchFiltersDescription": "Try clearing the search or filters.",
+ "noOrphaned": "No orphaned model folders found — your library is clean.",
+ "orphanedCleanup": "Orphaned model cleanup",
+ "orphanedCleanupFailed": "Orphaned model cleanup failed",
+ "orphanedCleanupPartialDescription": "{{deleted}} deleted, {{failed}} failed: {{error}}",
+ "orphanedDeletedDescription_one": "{{count}} orphaned model folder deleted.",
+ "orphanedDeletedDescription_other": "{{count}} orphaned model folders deleted.",
+ "orphanedDescription": "Folders in the models directory with no database record — usually leftovers from failed installs or external deletes.",
+ "orphanedTitle": "Orphaned Models",
+ "predictionType": "Prediction Type",
+ "overrideBaseUrl": "Override base URL",
+ "path": "Path",
+ "pauseAllDownloads": "Pause all downloads",
+ "pauseAllInstalls": "Pause all installs",
+ "pauseDownload": "Pause download",
+ "pauseFailed": "Pause failed",
+ "pauseInstall": "Pause install",
+ "plusMore": " +{{count}} more",
+ "pruneFailed": "Prune failed",
+ "press": "Press",
+ "pull": "Pull",
+ "queueActionFailed": "Queue action failed",
+ "refreshList": "Refresh model list",
+ "reidentify": "Re-identify model",
+ "relatedModels": "Related Models",
+ "relatedModelsHelp": "Link the models that pair well with this one - they are suggested together elsewhere in the app.",
+ "removeImage": "Remove image",
+ "removeModelImage": "Remove model image",
+ "removeTriggerPhrase": "Remove trigger phrase {{phrase}}",
+ "restartFile": "Restart file",
+ "restartFileFailed": "Restart file failed",
+ "restartThisFile": "Restart this file",
+ "resumeAllInstalls": "Resume all installs",
+ "resumeAllPausedDownloads": "Resume all paused downloads",
+ "resumeDownload": "Resume download",
+ "resumeFailed": "Resume failed",
+ "resumeInstall": "Resume install",
+ "resumeRequired": "Resume required",
+ "retryFailed": "Retry failed",
+ "retryFailedDownload": "Retry failed download",
+ "retryInstall": "Retry install",
+ "saveDefaults": "Save Defaults",
+ "scan": "Scan",
+ "scanFailed": "Scan failed",
+ "scanFolderTooltip": "Scan this folder for model files",
+ "scanSummary_one": "{{count}} file in {{path}} · {{notInstalled}} not installed",
+ "scanSummary_other": "{{count}} files in {{path}} · {{notInstalled}} not installed",
+ "searchModels": "Search models",
+ "searchModelsPlaceholder": "Search models…",
+ "searchCompatibleToLink": "Search compatible models to link...",
+ "searchOrAdd": "Search or add a model",
+ "searchOrAddPlaceholder": "Search models, or paste a URL, file path, or Hugging Face repo",
+ "selectAllCount": "Select all ({{count}})",
+ "selectedCount_one": "{{count}} selected",
+ "selectedCount_other": "{{count}} selected",
+ "selectModel": "Select a model",
+ "selectModelDescription": "Pick a model from the library to view details, edit metadata, set per-model defaults, and manage trigger phrases.",
+ "someCouldNotBeDeleted": "Some models could not be deleted",
+ "sortBase": "Base",
+ "sortDefault": "Default",
+ "sortFormat": "Format",
+ "sortName": "Name",
+ "sortSize": "Size",
+ "sort": {
+ "base": "Base",
+ "default": "Default",
+ "format": "Format",
+ "name": "Name",
+ "size": "Size"
+ },
+ "sortBy": "Sort By",
+ "source": "Source",
+ "sourceUrl": "Source URL",
+ "sourceUrlHelp": "Model page or download URL.",
+ "title": "Models",
+ "toFindModelsInFolder": "to find models in this folder.",
+ "toInstallFrom": "to install from {{source}}.",
+ "type": "Type",
+ "triggerPhraseDuplicate": "That trigger phrase is already on this model.",
+ "triggerPhrases": "Trigger Phrases",
+ "triggerPhrasesHelp": "Phrases that activate this model, surfaced in the prompt editor.",
+ "unlink": "Unlink",
+ "unlinkNamed": "Unlink {{name}}",
+ "uploadCoverImageFor": "Upload cover image for {{name}}",
+ "useSupportedImage": "Use a PNG, JPEG, or WebP image.",
+ "variant": "Variant",
+ "variantHelp": "Architecture variant, e.g. inpaint or dev.",
+ "waitingForDownload": "Waiting for download"
+ },
+ "nodes": {
+ "addCustomNodes": "Add custom nodes",
+ "addNodes": "Add Nodes",
+ "couldNotReloadCustomNodes": "Could not reload custom nodes.",
+ "couldNotLoadPacks": "Could not load custom node packs",
+ "customNodesReloaded": "Custom nodes reloaded",
+ "dependenciesRequired": "Dependencies required",
+ "dependenciesRequiredDescription": "{{name}} requires dependencies from {{dependencyFile}}. Install them, then restart InvokeAI.",
+ "details": "Node Pack Details",
+ "fieldName": "Field: {{name}}",
+ "fieldType": "Type: {{type}}",
+ "gitUrl": "Git URL",
+ "gitUrlHelp": "Paste a public Git repository URL. InvokeAI clones it into your custom nodes directory.",
+ "input": "Input",
+ "install": "Install",
+ "installActivity": "Custom node install activity",
+ "installFailed": "Install failed.",
+ "installing": "Installing",
+ "installedPacks": "Installed custom node packs",
+ "manager": "Nodes Manager",
+ "message": "Message",
+ "nodeCategory": "Category: {{category}}",
+ "nodeCount_one": "{{count}} node",
+ "nodeCount_other": "{{count}} nodes",
+ "nodePack": "node pack",
+ "nodePacks": "Node Packs",
+ "nodeType": "Type: {{type}}",
+ "nodeVersion": "Version: {{version}}",
+ "nodesDirectory": "Nodes Directory",
+ "nodesInPack": "Nodes in this pack",
+ "noPacks": "No custom node packs",
+ "noPacksDescription": "Install from a Git URL or place packs in your custom nodes directory, then reload.",
+ "noPacksMatch": "No packs match",
+ "noPreviews": "No node previews available",
+ "noPreviewsDescription": "The backend has no node definitions for this pack yet. Reload nodes or restart InvokeAI to load them.",
+ "noRecentActivity": "No recent activity",
+ "noRecentActivityDescription": "Install or uninstall a node pack and the outcome shows up here.",
+ "output": "Output",
+ "recentActivitySummary_one": "{{count}} recent activity",
+ "recentActivitySummary_other": "{{count}} recent activities",
+ "reloadFailed": "Reload failed",
+ "reloading": "Reloading",
+ "scanFolder": "Scan Folder",
+ "scanFolderDescription": "Add node packs directly to the custom nodes directory, then reload nodes. InvokeAI scans this folder on startup and reload.",
+ "searchPacks": "Search node packs",
+ "searchPacksPlaceholder": "Search packs…",
+ "selectPack": "Select a node pack",
+ "selectPackDescription": "Choose a pack from the library to see its path, manage it, and preview every node it adds.",
+ "status": "Status",
+ "trustWarning": "Custom nodes execute Python code. Only install packs from authors you trust.",
+ "tryDifferentSearch": "Try a different search.",
+ "uninstall": "Uninstall",
+ "uninstallBody": "Remove this pack from the custom nodes directory? A restart is required for removal to fully apply.",
+ "uninstallPack": "Uninstall Node Pack",
+ "uninstallTitle": "Uninstall {{name}}?",
+ "uninstalled": "Uninstalled"
+ },
+ "launchpad": {
+ "projectsGreeting": "Welcome to Invoke",
+ "projectsGreetingWithName": "Welcome back, {{name}}",
+ "projectsSubtitle": "Pick up where you left off, or start something new.",
+ "sections": {
+ "models": "Models",
+ "nodes": "Nodes",
+ "projects": "Projects",
+ "users": "Users"
+ },
+ "sectionsLabel": "Launchpad sections"
+ },
+ "settings": {
+ "title": "Settings",
+ "descriptionGlobal": "Global preferences for this single-user install. Project settings apply only to the active project.",
+ "descriptionUser": "Preferences for the signed-in user. Project settings apply only to the active project.",
+ "enableHighlightFocusedRegions": "Highlight focused region",
+ "language": "Language",
+ "languageDescription": "Interface language. New workbench copy falls back to English until translators add coverage.",
+ "tabs": {
+ "appearance": "Appearance",
+ "behavior": "Behavior",
+ "developer": "Developer",
+ "hotkeys": "Hotkeys",
+ "project": "Project",
+ "queue": "Queue",
+ "workflow": "Workflow",
+ "workspace": "Workspace"
+ }
+ },
+ "shell": {
+ "addCurrentLayout": "Add current layout...",
+ "builtInPresets": "Built-in presets",
+ "customLayout": "Custom layout {{number}}",
+ "customPresets": "Custom presets",
+ "deleteLayoutPreset": "Delete preset",
+ "deleteLayoutPresetBody": "Delete \"{{name}}\"? This only removes the saved preset.",
+ "deleteLayoutPresetQuestion": "Delete layout preset?",
+ "layoutPresetName": "Preset name",
+ "renameLayoutPreset": "Rename layout preset"
+ },
+ "widgets": {
+ "actionsLabel": "{{label}} actions",
+ "removeWidget": "Remove {{label}}",
+ "resizePanel": "Resize {{region}} widget panel",
+ "settingsLabel": "{{label}} settings",
+ "visibilityLabel": "{{region}} widget visibility",
+ "groupLabel": "Widgets",
+ "labels": {
+ "autosaveStatus": "Autosave",
+ "canvas": "Canvas",
+ "diagnostics": "Diagnostics",
+ "gallery": "Gallery",
+ "generate": "Generate",
+ "layers": "Layers",
+ "notifications": "Notifications",
+ "preview": "Preview",
+ "project": "Project",
+ "queue": "Queue",
+ "serverStatus": "Server Status",
+ "versionStatus": "Version",
+ "workflow": "Workflow"
+ },
+ "generate": {
+ "addPromptTrigger": "Add prompt trigger",
+ "activeCount_one": "{{count}} active",
+ "activeCount_other": "{{count}} active",
+ "advanced": "Advanced",
+ "addConcept": "Add concept",
+ "addConceptsHelp": "Add LoRAs/concepts here to blend them into this generation.",
+ "aspectRatio": "Aspect ratio",
+ "cfgRescale": "CFG rescale",
+ "clipSkip": "CLIP skip",
+ "colorCompensation": "Color compensation",
+ "components": "Components",
+ "compositing": "Compositing",
+ "concepts": "Concepts",
+ "conceptWeight": "{{name}} weight",
+ "compatibleEmbeddings": "Compatible embeddings",
+ "couldNotExpandPrompt": "Could not expand the prompt.",
+ "couldNotGeneratePromptFromImage": "Could not generate a prompt from the selected image.",
+ "deletePromptHistoryItem": "Delete prompt history item",
+ "denoisingStrength": "Denoising strength",
+ "denoisingStrengthHelp": "How much the generation reworks the existing canvas content. Only applies to image-to-image (when content fills the generation frame).",
+ "dimensions": "Dimensions",
+ "disableConcept": "Disable {{name}}",
+ "enableNegativePrompt": "Enable negative prompt",
+ "enableConcept": "Enable {{name}}",
+ "expand": "Expand",
+ "expandPrompt": "Expand prompt",
+ "generatePrompt": "Generate Prompt",
+ "imageToPrompt": "Image to prompt",
+ "height": "Height",
+ "incompatible": "Incompatible",
+ "incompatibleSettingsCleared": "Incompatible settings cleared",
+ "incompatibleSettingsClearedDescription_one": "{{labels}} was not compatible with {{name}}.",
+ "incompatibleSettingsClearedDescription_other": "{{labels}} were not compatible with {{name}}.",
+ "installTextLlmToExpandPrompts": "Install a Text LLM model to expand prompts.",
+ "installVisionModelToGeneratePrompts": "Install a LLaVA OneVision model to generate prompts from images.",
+ "lockAspectRatio": "Lock aspect ratio",
+ "mainModel": "Main model",
+ "model": "Model",
+ "modelDefault": "Model default",
+ "multipleOf": "Multiple of {{value}}",
+ "negativePrompt": "Negative prompt",
+ "noMatchingPrompts": "No matching prompts.",
+ "noMatchingTriggers": "No matching triggers.",
+ "noPromptHistoryYet": "No prompt history yet.",
+ "noPromptTriggersAvailable": "No prompt triggers available.",
+ "noTextLlmInstalled": "No Text LLM installed",
+ "noVisionModelInstalled": "No vision model installed",
+ "offCount_one": "{{count}} off",
+ "offCount_other": "{{count}} off",
+ "positivePrompt": "Positive prompt",
+ "promptHistory": "Prompt history",
+ "promptHistoryEntries": "Prompt history entries",
+ "promptHistoryKeyboardHelp": "Alt+Up/Down switches between prompts while focused.",
+ "promptTemplate": "Prompt Template",
+ "promptTriggerOptions": "Prompt trigger options",
+ "random": "Random",
+ "activeSteps": "Active steps",
+ "clearReferenceImages": "Remove all",
+ "collapseReferenceImage": "Collapse reference image",
+ "cropReferenceImage": "Crop reference image",
+ "cropReferenceImageHelp": "Drag the crop box to move it, or drag its handles to resize. Apply to create a cropped reference image.",
+ "disableReferenceImage": "Disable reference image",
+ "enableReferenceImage": "Enable reference image",
+ "expandReferenceImage": "Expand reference image",
+ "imageInfluence": "Image influence",
+ "mode": "Mode",
+ "modeBoth": "Both",
+ "modeComposition": "Composition",
+ "modeStyle": "Style",
+ "pullBboxIntoReferenceImage": "Pull Bbox into Reference Image",
+ "referenceImage": "Reference Image",
+ "referenceImageModel": "Reference image model",
+ "referenceImages": "Reference images",
+ "referenceImagesHelp": "Upload an image or drag images from Gallery here.",
+ "referenceImagesUnsupported": "Reference images are not supported by the selected model.",
+ "removeReferenceImage": "Remove reference image",
+ "styleVariant": "Style variant",
+ "styleVariantPrecise": "Precise",
+ "styleVariantPreciseDesc": "Applies a precise visual style, eliminating subject influence.",
+ "styleVariantStandard": "Standard",
+ "styleVariantStandardDesc": "Applies visual style (colors, textures) without considering its layout. Previously called Style Only.",
+ "styleVariantStrong": "Strong",
+ "styleVariantStrongDesc": "Applies a strong visual style, with a slightly reduced composition influence.",
+ "useSize": "Use size",
+ "removeConcept": "Remove concept",
+ "removeConceptNamed": "Remove {{name}}",
+ "resizeNegativePrompt": "Resize negative prompt",
+ "resizePositivePrompt": "Resize positive prompt",
+ "scheduler": "Scheduler",
+ "searchPromptHistory": "Search prompt history",
+ "searchPromptTriggers": "Search prompt triggers",
+ "searchCompatibleConcepts": "Search compatible concepts...",
+ "seamlessTiling": "Seamless tiling",
+ "selectImageFirst": "Select an image in Gallery or Preview first.",
+ "selectMainModelBeforeConcepts": "Select a main model before adding concepts.",
+ "selectModel": "Select a model…",
+ "selectModelFirst": "Select a model first",
+ "selectTextLlm": "Select Text LLM",
+ "selectVisionModel": "Select vision model",
+ "setOptimalSize": "Set optimal size",
+ "setOptimalSizeDescription": "Set size to the model's optimal pixel count",
+ "shuffleSeed": "Shuffle seed",
+ "showDynamicPrompts": "Show dynamic prompts",
+ "steps": "Steps",
+ "swapWidthAndHeight": "Swap width and height",
+ "tileX": "Tile X",
+ "tileY": "Tile Y",
+ "unlockAspectRatio": "Unlock aspect ratio",
+ "useModelDefault": "Use model default",
+ "useConceptDefaultWeight": "Use concept default weight",
+ "useModelDefaultCfgRescale": "Use model default CFG rescale",
+ "useModelDefaultField": "Use model default {{field}}",
+ "useModelDefaultHeight": "Use model default height",
+ "useModelDefaultScheduler": "Use model default scheduler",
+ "useModelDefaultSteps": "Use model default steps",
+ "useModelDefaultVae": "Use model default VAE",
+ "useModelDefaultVaePrecision": "Use model default VAE precision",
+ "useModelDefaultWidth": "Use model default width",
+ "usePrompt": "Use prompt",
+ "usingBundledVae": "Using the VAE bundled with the model.",
+ "upload": "Upload",
+ "vae": "VAE",
+ "vaePrecision": "VAE precision",
+ "weight": "Weight",
+ "width": "Width",
+ "xAxis": "X axis",
+ "yAxis": "Y axis",
+ "compositingOptions": {
+ "infillMethod": "Infill method",
+ "infillMethods": {
+ "patchmatch": "PatchMatch",
+ "lama": "LaMa",
+ "cv2": "OpenCV",
+ "color": "Solid color",
+ "tile": "Tile"
+ },
+ "coherenceMode": "Coherence pass mode",
+ "coherenceModes": {
+ "Gaussian Blur": "Gaussian Blur",
+ "Box Blur": "Box Blur",
+ "Staged": "Staged"
+ },
+ "coherenceEdgeSize": "Coherence edge size",
+ "coherenceMinDenoise": "Coherence min denoise",
+ "maskBlur": "Mask blur"
+ }
+ },
+ "graph": {
+ "setSource": "Set Source",
+ "viewGraph": "View Graph"
+ },
+ "project": {
+ "copyId": "Copy project id",
+ "deleteRecovery": "Delete recovery",
+ "deleteRecoveryAria": "Delete {{name}}",
+ "deleteRecoveryBody": "Delete \"{{name}}\"? The recovery copy is removed permanently.",
+ "deleteRecoveryTitle": "Delete recovery?",
+ "events": "Events",
+ "graphNodes": "Graph nodes",
+ "idCopied": "Project id copied",
+ "nameHelp": "Saved with the project.",
+ "nameLabel": "Project name",
+ "openOriginal": "Open original \"{{name}}\"",
+ "openRecovery": "Open {{name}}",
+ "queueItems": "Queue items",
+ "recoveries": "Recoveries",
+ "recoveryCopy": "Recovery copy",
+ "recoveryForkedDescription": "Forked {{time}} after this project was changed elsewhere.",
+ "sync": "Sync",
+ "syncOffline": "Offline — saved in this browser",
+ "syncSynced": "Synced (revision {{revision}})",
+ "syncWaiting": "Waiting to sync"
+ },
+ "preview": {
+ "commands": {
+ "deletePreviewImage": "Delete preview image",
+ "nextComparisonMode": "Next comparison mode",
+ "swapComparisonImages": "Swap comparison images",
+ "togglePreview": "Toggle preview"
+ },
+ "compare": "Compare",
+ "emptyDescription": "Generate to Gallery, then select a thumbnail in the Gallery widget to inspect it here.",
+ "exitCompare": "Exit Compare",
+ "hideInProgressDiffusion": "Hide in-progress diffusion",
+ "imageCount_one": "{{count}} image",
+ "imageCount_other": "{{count}} images",
+ "loadingBoard": "Loading board",
+ "nextImageInBoard": "Next image in board",
+ "noGallerySelection": "No gallery selection",
+ "previousImageInBoard": "Previous image in board",
+ "showInProgressDiffusion": "Show in-progress diffusion",
+ "sideBySide": "Side by Side",
+ "slider": "Slider",
+ "sourceRun": "Source run {{id}}",
+ "viewing": "Viewing"
+ },
+ "queue": {
+ "allScopedItemsRequested": "All scoped queue items were requested.",
+ "actionConfirmationTitle": "{{action}}?",
+ "backendItem": "Backend queue item {{id}}.",
+ "batch": "Batch",
+ "cancelAllExceptCurrent": "Cancel all except current item",
+ "cancelAllExceptCurrentRequested": "All pending scoped items except the current item were requested.",
+ "cancelAllItems": "Cancel All Items",
+ "cancelConfirmationBody": "This requests cancellation for queue work in the current scope. In-progress items may stop after the backend acknowledges cancellation.",
+ "cancelCurrent": "Cancel Current",
+ "cancelCurrentItem": "Cancel Current Item",
+ "cancelItem": "Cancel queue item #{{id}}",
+ "cancellationRequested": "Cancellation requested",
+ "clearAllItems": "Clear All Items",
+ "clearFailedConfirmationBody": "This permanently removes failed queue items in the current queue scope.",
+ "clearFailedItems": "Clear Failed Items",
+ "clearFailedTitle": "Clear failed queue items?",
+ "clearQueue": "Clear Queue",
+ "clearQueueConfirmationBody": "This permanently clears queue items in the current queue scope.",
+ "clearQueueTitle": "Clear queue?",
+ "couldNotCancelCurrentItem": "Could not cancel the current item.",
+ "couldNotCancelItem": "Could not cancel this item.",
+ "couldNotCancelItems": "Could not cancel scoped queue items.",
+ "couldNotChangeProcessor": "Could not change the queue processor.",
+ "couldNotClearFailedItems": "Could not clear failed queue items.",
+ "couldNotClearQueue": "Could not clear the queue.",
+ "currentBatch": "Current Batch",
+ "failedItemsCleared": "Failed queue items cleared",
+ "failedToCancelCurrentItem": "Failed to cancel current item",
+ "failedToCancelItems": "Failed to cancel queue items",
+ "failedToClearItems": "Failed to clear queue items",
+ "failedToClearQueue": "Failed to clear queue",
+ "failedToPauseProcessor": "Failed to pause processor",
+ "failedToResumeProcessor": "Failed to resume processor",
+ "filterRecentByStatus": "Filter recent queue items by status",
+ "generating_one": "{{count}} generating",
+ "generating_other": "{{count}} generating",
+ "itemJsonLabel": "Queue item {{id}} JSON",
+ "itemProgress": "Queue item progress",
+ "itemTitle": "Queue item #{{id}}",
+ "loadSettingsIntoGenerate": "Load these settings into Generate",
+ "loading": "Loading queue…",
+ "modelCache": {
+ "clear": "Clear cache",
+ "clearFailed": "Clear cache failed",
+ "cleared": "Model cache cleared",
+ "clearedDescription_one": "Cleared {{count}} cached model and freed {{bytes}}.",
+ "clearedDescription_other": "Cleared {{count}} cached models and freed {{bytes}}.",
+ "couldNotClear": "Could not clear the model cache.",
+ "hits": "Hits",
+ "label": "Model Cache",
+ "loaded": "Loaded",
+ "misses": "Misses",
+ "noModelsCleared": "No cached models cleared",
+ "noModelsClearedDescription": "No unlocked cached models were available to clear. Active models stay loaded until they are no longer in use.",
+ "usage": "Model cache usage",
+ "usedOfTotal": "{{used}} of {{total}} used"
+ },
+ "noCurrentItem": "No current item",
+ "noCurrentItemDescription": "The scoped queue has no in-progress item to cancel.",
+ "noPrompt": "No prompt",
+ "openQueue": "Open Queue",
+ "pauseProcessor": "Pause Processor",
+ "processorPaused": "Processor paused",
+ "processorResumed": "Processor resumed",
+ "queueCancellationRequested": "Queue cancellation requested",
+ "queueCleared": "Queue cleared",
+ "queued_one": "{{count}} queued",
+ "queued_other": "{{count}} queued",
+ "recallAll": "Recall All",
+ "recallFromGallery": "Recall this generation from the gallery",
+ "resumeProcessor": "Resume Processor",
+ "sendToCanvas": "Send to canvas",
+ "sendToCanvasComingSoon": "Sending queue results to the canvas is coming soon.",
+ "settingsRecalled": "Settings recalled",
+ "settingsRecalledDescription": "Loaded these settings into Generate.",
+ "took": "Took",
+ "waiting_one": "{{count}} waiting",
+ "waiting_other": "{{count}} waiting"
+ },
+ "autosaveStatus": {
+ "chipLabel": "Autosave: {{status}}",
+ "label": "Autosave",
+ "lastSaved": "Last saved: {{time}}",
+ "status": "Status: {{status}}"
+ },
+ "canvas": {
+ "acceptToLayer": "Accept to Layer",
+ "candidateCount": "{{current}} of {{total}}",
+ "commands": {
+ "decreaseBrushSize": "Decrease brush/eraser size",
+ "deleteLayer": "Delete layer",
+ "deleteSelected": "Delete selected canvas item",
+ "deselect": "Deselect",
+ "duplicateLayer": "Duplicate layer",
+ "fitBbox": "Fit generation frame",
+ "increaseBrushSize": "Increase brush/eraser size",
+ "invertSelection": "Invert selection",
+ "layerBackward": "Send layer backward",
+ "layerForward": "Bring layer forward",
+ "layerToBack": "Send layer to back",
+ "layerToFront": "Bring layer to front",
+ "mergeDown": "Merge layer down",
+ "nextEntity": "Next canvas entity",
+ "nudgeDown": "Nudge layer down",
+ "nudgeDownLarge": "Nudge layer down 10px",
+ "nudgeLeft": "Nudge layer left",
+ "nudgeLeftLarge": "Nudge layer left 10px",
+ "nudgeRight": "Nudge layer right",
+ "nudgeRightLarge": "Nudge layer right 10px",
+ "nudgeUp": "Nudge layer up",
+ "nudgeUpLarge": "Nudge layer up 10px",
+ "previousEntity": "Previous canvas entity",
+ "redo": "Redo canvas edit",
+ "reorderLayer": "Reorder layer",
+ "resetSelected": "Clear selected mask",
+ "selectAll": "Select all",
+ "selectBboxTool": "Select generation frame tool",
+ "selectBrushTool": "Select brush tool",
+ "selectEraserTool": "Select eraser tool",
+ "selectGradientTool": "Select gradient tool",
+ "selectLassoTool": "Select lasso tool",
+ "selectMoveTool": "Select move tool",
+ "selectShapeTool": "Select shape tool",
+ "selectTextTool": "Select text tool",
+ "selectTransformTool": "Select transform tool",
+ "selectViewTool": "Select view tool",
+ "undo": "Undo canvas edit"
+ },
+ "contextMenu": {
+ "empty": "This region has no visible raster content.",
+ "notReady": "The canvas is still preparing image data.",
+ "saveBboxToGallery": "Save BBox to Gallery",
+ "saveCanvasToGallery": "Save Canvas to Gallery",
+ "saveError": "Couldn't save to gallery",
+ "saveToGallery": "Save to Gallery",
+ "saved": "Saved to gallery",
+ "savedDescription": "{{name}} was added to your gallery.",
+ "stale": "The canvas changed while saving. Try again."
+ },
+ "controls": {
+ "fitBboxToLayers": "Fit frame to layers",
+ "fitBboxToMasks": "Fit frame to masks",
+ "fitToView": "Fit to view",
+ "frameSize": "Generation frame size",
+ "newCanvas": "New canvas",
+ "newCanvasConfirm": "Clear all layers and start a fresh canvas? This cannot be undone.",
+ "newSession": "New session",
+ "zoomLevel": "Zoom level"
+ },
+ "hideStagedResultPreview": "Hide staged result preview",
+ "hideStagingThumbnails": "Hide staging thumbnails",
+ "import": {
+ "blocked": "Finish the current canvas operation before adding images.",
+ "control": "Control Layer",
+ "dropControl": "Add as Control Layer",
+ "dropInpaintMask": "Add as Inpaint Mask",
+ "dropRaster": "Add as Raster Layer",
+ "dropRegionalReference": "Add as Regional Reference Image",
+ "dropResizedControl": "Add as Resized Control Layer",
+ "empty": "No gallery images were available to add.",
+ "failed": "Couldn't add images to canvas",
+ "inpaintMask": "Inpaint Mask",
+ "newLayerFromImage": "New Layer from Image",
+ "partial_one": "Added {{successCount}} image; {{failedCount}} could not be prepared.",
+ "partial_other": "Added {{successCount}} images; {{failedCount}} could not be prepared.",
+ "raster": "Raster Layer",
+ "regionalGuidance": "Regional Guidance",
+ "regionalReference": "Regional Reference Image",
+ "resizedControl": "Resized Control Layer",
+ "staleDocument": "The canvas changed while the images were being prepared. Try again.",
+ "staleProject": "The destination project was closed before the images were ready.",
+ "success_one": "Added {{count}} image as canvas layers",
+ "success_other": "Added {{count}} images as canvas layers"
+ },
+ "nextStagedCandidate": "Next staged candidate",
+ "previousStagedCandidate": "Previous staged candidate",
+ "selectStagedCandidate": "Select staged candidate {{number}}",
+ "settings": {
+ "bboxOverlay": "Show bbox overlay",
+ "checkerboard": "Checkerboard background",
+ "clearCaches": "Clear caches",
+ "clearHistory": "Clear history",
+ "grid": "Show grid",
+ "invertBrushScroll": "Invert scroll for brush size",
+ "label": "Canvas settings",
+ "logDebugInfo": "Log debug info",
+ "ruleOfThirds": "Rule of thirds",
+ "sections": {
+ "behavior": "Behavior",
+ "debug": "Debug",
+ "display": "Display",
+ "grid": "Grid"
+ },
+ "showBbox": "Show generation frame",
+ "showProgressOnCanvas": "Show progress on canvas",
+ "snapToGrid": "Snap to grid"
+ },
+ "showStagedResultPreview": "Show staged result preview",
+ "showStagingThumbnails": "Show staging thumbnails",
+ "staging": {
+ "autoSwitch": "Auto-switch",
+ "autoSwitchLatest": "Newest",
+ "autoSwitchOff": "Off",
+ "autoSwitchProgress": "In progress",
+ "generating": "Generating…",
+ "saveError": "Couldn't save to gallery",
+ "saveToGallery": "Save to gallery",
+ "saved": "Saved to gallery",
+ "savedDescription": "{{name}} added to your gallery."
+ },
+ "surface": "Canvas surface",
+ "toolOptions": {
+ "applyTransform": "Apply",
+ "aspectRatio": "Aspect ratio",
+ "brushColor": "Brush color",
+ "brushSize": "Brush size",
+ "cancelTransform": "Cancel",
+ "deselect": "Deselect",
+ "eraseSelection": "Erase",
+ "eraserSize": "Eraser size",
+ "fillSelection": "Fill",
+ "frameHeight": "H",
+ "frameWidth": "W",
+ "gradientAngle": "Angle",
+ "gradientEdit": "Edit gradient",
+ "gradientEnd": "End",
+ "gradientEndOpacity": "End opacity",
+ "gradientKind": "Gradient type",
+ "gradientLinear": "Linear",
+ "gradientRadial": "Radial",
+ "gradientStart": "Start",
+ "gradientStartOpacity": "Start opacity",
+ "invertSelection": "Invert",
+ "lassoHint": "Draw a shape to select. Shift adds, Alt subtracts.",
+ "lockAspect": "Lock aspect ratio",
+ "movePosition": "Move layer",
+ "opacity": "Opacity",
+ "positionX": "X",
+ "positionY": "Y",
+ "pressureSensitivity": "Pressure sensitivity",
+ "rotation": "Rotation",
+ "scaleHeight": "H %",
+ "scaleWidth": "W %",
+ "selectionAdd": "Add",
+ "selectionIntersect": "Intersect",
+ "selectionMode": "Selection mode",
+ "selectionReplace": "Replace",
+ "selectionSubtract": "Subtract",
+ "setFrame": "Set generation frame",
+ "shapeEdit": "Edit shape",
+ "shapeEllipse": "Ellipse",
+ "shapeFill": "Fill",
+ "shapeHint": "Drag to draw a shape.",
+ "shapeKind": "Shape type",
+ "shapeRect": "Rectangle",
+ "shapeStroke": "Stroke",
+ "shapeStrokeWidth": "Stroke width",
+ "textAlignCenter": "Align center",
+ "textAlignLeft": "Align left",
+ "textAlignRight": "Align right",
+ "textColor": "Text color",
+ "textEdit": "Edit text",
+ "textFont": "Font",
+ "textLineHeight": "Line height",
+ "textSize": "Font size",
+ "textWeight": "Weight"
+ },
+ "tools": {
+ "bbox": "Generation frame",
+ "brush": "Brush",
+ "eraser": "Eraser",
+ "gradient": "Gradient",
+ "label": "Tools",
+ "lasso": "Lasso select",
+ "move": "Move",
+ "shape": "Shape",
+ "text": "Text",
+ "transform": "Transform",
+ "view": "View"
+ }
+ },
+ "workflow": {
+ "about": "About",
+ "addNode": "Add node",
+ "author": "Author",
+ "class": "Class",
+ "connectorNode": "Connector node",
+ "contact": "Contact",
+ "copyJson": "Copy workflow JSON",
+ "copyJsonFailed": "Failed to copy workflow JSON",
+ "copyJsonSuccess": "Workflow JSON copied",
+ "currentImageNode": "Current Image node",
+ "declaredOutputs": "Declared outputs",
+ "description": "Description",
+ "details": "Details",
+ "detailsWithEllipsis": "Workflow details…",
+ "exportJson": "Export workflow JSON",
+ "form": "Form",
+ "graphHistorySnapshots": "Graph History Snapshots",
+ "importJsonWithEllipsis": "Import workflow JSON…",
+ "inspectorTabs": {
+ "data": "Data",
+ "details": "Details",
+ "outputs": "Outputs",
+ "template": "Template"
+ },
+ "library": "Workflow library",
+ "name": "Workflow name",
+ "nodeData": "Node data",
+ "nodeInspector": "Node Inspector",
+ "nodeNotes": "Node notes",
+ "nodeNotesPlaceholder": "Notes about this node…",
+ "nodeTemplate": "Node template",
+ "notes": "Notes",
+ "notesNode": "Notes node",
+ "noTemplateKnown": "No template is known for \"{{type}}\" on this backend.",
+ "pack": "Pack",
+ "panelContent": "Workflow panel content",
+ "resizeNodeInspector": "Resize node inspector",
+ "runOutputsNotRecorded": "Run outputs are not recorded per node yet; results currently route to the Gallery or Canvas. This tab will show the node's outputs from its most recent run once run records land.",
+ "newWorkflowWithEllipsis": "New workflow…",
+ "saveGraphSnapshot": "Save graph snapshot",
+ "selectNodeToInspect": "Select a node in the Workflow editor to inspect it.",
+ "selectedNodeInspector": "Selected node inspector",
+ "tags": "Tags",
+ "title": "Title",
+ "type": "Type",
+ "version": "Version",
+ "workflowJson": "Workflow JSON",
+ "untitled": "Untitled Workflow"
+ },
+ "layers": {
+ "actions": {
+ "addControlLayer": "Control layer",
+ "addDenoiseLimit": "Add denoise limit",
+ "addImageNoise": "Add image noise",
+ "addInpaintMask": "Inpaint mask",
+ "addNegativePrompt": "Add negative prompt",
+ "addPositivePrompt": "Add positive prompt",
+ "addRasterLayer": "Raster layer",
+ "addReferenceImage": "Add reference image",
+ "addRegionalGuidance": "Regional guidance",
+ "addRegionalReferenceImage": "Regional reference image",
+ "actionFailed": "Layer action failed",
+ "adjustments": "Adjustments",
+ "blendMode": "Blend mode",
+ "busy": "The canvas is busy. Try again when the current action finishes.",
+ "copyFailed": "The layer could not be copied.",
+ "copyLayerToClipboard": "Copy layer to clipboard",
+ "copyToControl": "Copy to control layer",
+ "copyToInpaintMask": "Copy to inpaint mask",
+ "copyToRasterLayer": "Copy to raster layer",
+ "copyToRegionalGuidance": "Copy to regional guidance",
+ "convertToControl": "Convert to control layer",
+ "convertToInpaintMask": "Convert to inpaint mask",
+ "convertToRaster": "Convert to raster layer",
+ "convertToRegionalGuidance": "Convert to regional guidance",
+ "cropBusy": "Finish the current canvas action before cropping this layer.",
+ "cropFailed": "The layer could not be cropped.",
+ "cropLayerToBbox": "Crop layer to bbox",
+ "cropUnsupported": "This layer cannot be cropped.",
+ "cropped": "Layer cropped to bbox.",
+ "cutaway": "Cutaway with layer below",
+ "cutout": "Cutout with layer below",
+ "delete": "Delete layer",
+ "disableAutoNegative": "Disable auto-negative",
+ "disableTransparencyEffect": "Disable transparency effect",
+ "disabled": "Enable the layer before using this action.",
+ "duplicate": "Duplicate layer",
+ "empty": "This layer has no content.",
+ "enableAutoNegative": "Enable auto-negative",
+ "enableTransparencyEffect": "Enable transparency effect",
+ "exclude": "Exclude layer below",
+ "extractMaskedArea": "Extract masked area",
+ "fitToBbox": "Fit to bbox",
+ "hide": "Hide layer",
+ "intersect": "Intersect with layer below",
+ "lock": "Lock layer",
+ "locked": "Unlock the layer before using this action.",
+ "mergeDown": "Merge down",
+ "missing": "The layer no longer exists.",
+ "moveBackward": "Move backward",
+ "moveForward": "Move forward",
+ "moveToBack": "Move to back",
+ "moveToFront": "Move to front",
+ "notReady": "The layer content is still loading. Try again shortly.",
+ "opacity": "Opacity",
+ "rasterize": "Rasterize layer",
+ "rename": "Rename layer",
+ "reorder": "Reorder layer",
+ "runWorkflow": "Run workflow",
+ "saveLayerToAssets": "Save layer to assets",
+ "selectObject": "Select object",
+ "show": "Show layer",
+ "toggleAutoNegative": "Toggle auto-negative",
+ "toggleLock": "Toggle lock",
+ "toggleTransparencyEffect": "Toggle transparency effect",
+ "toggleVisibility": "Toggle visibility",
+ "transform": "Transform",
+ "unsupported": "This action is not supported for this layer.",
+ "unlock": "Unlock layer"
+ },
+ "control": {
+ "apply": "Apply",
+ "applyFilter": "Apply filter",
+ "beginStep": "Begin step",
+ "cancel": "Cancel",
+ "endStep": "End step",
+ "filter": "Filter",
+ "filterParams": {
+ "amount": "Amount",
+ "autoScale": "Autoscale",
+ "blur_type": "Blur type",
+ "channel": "Channel",
+ "coarse": "Coarse",
+ "distance_threshold": "Distance threshold",
+ "draw_body": "Draw body",
+ "draw_face": "Draw face",
+ "draw_hands": "Draw hands",
+ "high_threshold": "High threshold",
+ "low_threshold": "Low threshold",
+ "max_faces": "Max faces",
+ "min_confidence": "Min confidence",
+ "model": "Image-to-image model",
+ "model_size": "Model size",
+ "quantize_edges": "Quantize edges",
+ "scale_factor": "Scale factor",
+ "scale": "Scale",
+ "scale_values": "Multiply values",
+ "score_threshold": "Score threshold",
+ "scribble": "Scribble",
+ "tile_size": "Tile size",
+ "noise_color": "Color noise",
+ "noise_type": "Noise type",
+ "radius": "Radius",
+ "size": "Size",
+ "value": "Value"
+ },
+ "filterOptions": {
+ "blur_type": {
+ "box": "Box",
+ "gaussian": "Gaussian"
+ },
+ "channel": {
+ "alpha": "Alpha (RGBA)",
+ "black": "Black (CMYK)",
+ "blue": "Blue (RGBA)",
+ "cb": "Cb (YCbCr)",
+ "cr": "Cr (YCbCr)",
+ "cyan": "Cyan (CMYK)",
+ "green": "Green (RGBA)",
+ "hue": "Hue (HSV)",
+ "lab_a": "A (LAB)",
+ "lab_b": "B (LAB)",
+ "luminosity": "Luminosity (LAB)",
+ "magenta": "Magenta (CMYK)",
+ "red": "Red (RGBA)",
+ "saturation": "Saturation (HSV)",
+ "value": "Value (HSV)",
+ "y": "Y (YCbCr)",
+ "yellow": "Yellow (CMYK)"
+ },
+ "model_size": {
+ "base": "Base",
+ "large": "Large",
+ "small": "Small",
+ "small_v2": "Small v2"
+ },
+ "noise_type": {
+ "gaussian": "Gaussian",
+ "salt_and_pepper": "Salt and pepper"
+ }
+ },
+ "filters": {
+ "adjust_image": "Adjust image",
+ "canny_edge_detection": "Canny",
+ "color_map": "Color map",
+ "content_shuffle": "Content shuffle",
+ "depth_anything_depth_estimation": "Depth Anything",
+ "dw_openpose_detection": "DW Openpose",
+ "hed_edge_detection": "HED",
+ "img_blur": "Blur",
+ "img_noise": "Noise",
+ "lineart_anime_edge_detection": "Lineart Anime",
+ "lineart_edge_detection": "Lineart",
+ "mediapipe_face_detection": "Mediapipe Face",
+ "mlsd_detection": "MLSD",
+ "normal_map": "Normal map",
+ "pidi_edge_detection": "PiDiNet",
+ "spandrel_filter": "Spandrel"
+ },
+ "kind": "Adapter type",
+ "kinds": {
+ "control_lora": "Control LoRA",
+ "controlnet": "ControlNet",
+ "t2i_adapter": "T2I Adapter",
+ "z_image_control": "Z-Image ControlNet"
+ },
+ "mode": "Control mode",
+ "model": "Model",
+ "modes": {
+ "balanced": "Balanced",
+ "more_control": "More control",
+ "more_prompt": "More prompt",
+ "unbalanced": "Unbalanced"
+ },
+ "preview": "Preview filter",
+ "selectModel": "Select a model",
+ "stepRange": "Step range",
+ "transparencyEffect": "Transparency effect",
+ "validation": {
+ "control_lora_limit": "Only one Control LoRA can be used at a time.",
+ "flux_fill_control_lora": "Control LoRA is not compatible with FLUX Fill.",
+ "incompatible_adapter": "The selected model does not match this adapter type.",
+ "incompatible_base": "The control model is incompatible with the selected base model.",
+ "invalid_adapter_values": "Control weight and step range must be valid before generating.",
+ "missing_model": "Select a control model before generating.",
+ "unsupported_adapter": "This control adapter is not supported by the selected base model.",
+ "z_image_control_limit": "Only one Z-Image control can be used at a time."
+ },
+ "weight": "Weight"
+ },
+ "rasterFilter": {
+ "autoProcess": "Auto",
+ "autoProcessDescription": "Process automatically when settings change",
+ "busy": "Finish the current canvas interaction, then try again.",
+ "cancel": "Cancel",
+ "commitFailure": "The filtered pixels could not be committed: {{message}}",
+ "copy": "Copy",
+ "durabilityFailure": "The filtered image could not be preserved: {{message}}",
+ "graphFailure": "The filter failed: {{message}}",
+ "locked": "Unlock the layer before applying the filter.",
+ "preview": "Preview",
+ "replace": "Replace",
+ "running": "Running filter…",
+ "statusCommitting": "Applying filter…",
+ "statusError": "Filter needs attention.",
+ "stale": "The layer changed while filtering. Preview it again.",
+ "title": "Filter",
+ "unsupported": "This layer content cannot be filtered."
+ },
+ "selectObject": {
+ "autoProcess": "Auto Process",
+ "bbox": "BBox",
+ "bboxActive": "Bounding box active",
+ "bboxInactive": "No bounding box",
+ "errorDecode": "The object preview could not be decoded.",
+ "errorEmpty": "The layer has no visible content to select.",
+ "errorInvalid": "Add an object prompt, point, or bounding box before processing.",
+ "errorLocked": "Select Object cannot finish while canvas editing is locked.",
+ "errorNoOutput": "Select Object finished without producing an image.",
+ "errorNotReady": "The layer source is not ready. Try again when it finishes loading.",
+ "errorOutputDimension": "The Select Object result dimensions do not match the layer source.",
+ "errorQueue": "Select Object processing failed on the backend.",
+ "errorReconcile": "The completed Select Object result could not be retrieved.",
+ "errorUnknown": "Select Object could not finish.",
+ "errorUpload": "The layer source could not be uploaded for Select Object.",
+ "exclude": "Exclude",
+ "excludeCount": "Exclude {{count}}",
+ "include": "Include",
+ "includeCount": "Include {{count}}",
+ "invert": "Invert",
+ "isolatedPreview": "Isolated Preview",
+ "model": "Model",
+ "modelHuge": "SAM Huge",
+ "modelSam2Large": "SAM 2 Large",
+ "mode": "Selection mode",
+ "prompt": "Object prompt",
+ "promptGuidance": "Describe the object you want to select.",
+ "promptMode": "Prompt",
+ "pointType": "Point type",
+ "process": "Process",
+ "refine": "Refine",
+ "reset": "Reset",
+ "saveAs": "Save As",
+ "saveAs_control": "Control layer",
+ "saveAs_inpaint_mask": "Inpaint mask",
+ "saveAs_raster": "Raster layer",
+ "saveAs_regional_guidance": "Regional guidance",
+ "saveAs_selection": "Selection",
+ "settings": "Select Object settings",
+ "sourceLayerLabel": "{{name}} · {{type}} · {{width}} × {{height}}",
+ "statusCommitting": "Applying object result…",
+ "statusError": "Select Object needs attention.",
+ "statusPreparingSource": "Preparing layer source…",
+ "statusProcessingSam": "Finding object…",
+ "statusReady": "Ready",
+ "statusRenderingPreview": "Rendering object preview…",
+ "statusScheduled": "Waiting to process object…",
+ "statusUploading": "Uploading layer source…",
+ "technicalDetails": "Technical details",
+ "title": "Select Object",
+ "visual": "Visual",
+ "visualGuidance": "Click to add · drag to move · Shift flips type"
+ },
+ "runWorkflow": {
+ "aborted": "Workflow run was canceled.",
+ "busy": "Finish the current canvas interaction, then try again.",
+ "cancel": "Cancel",
+ "copyRaster": "Copy as raster layer",
+ "copySuccess": "Added the workflow result as a raster layer.",
+ "destination": "Destination",
+ "disabled": "Enable the layer before running the workflow.",
+ "durabilityFailure": "The workflow output could not be preserved: {{message}}",
+ "empty": "This layer has no content to run through the workflow.",
+ "failed": "Workflow run failed: {{message}}",
+ "gallery": "Gallery",
+ "gallerySuccess": "Saved the workflow result to Gallery.",
+ "graphFailure": "The active workflow failed: {{message}}",
+ "hydrationFailure": "The workflow output could not be loaded: {{message}}",
+ "input": "Workflow image input",
+ "locked": "Unlock the layer before running the workflow.",
+ "missing": "The source layer no longer exists.",
+ "noBindings": "The active workflow has no compatible image input and output.",
+ "notReady": "The active workflow is not ready with this layer input.",
+ "output": "Workflow image output",
+ "replace": "Replace layer",
+ "replaceSuccess": "Replaced the layer with the workflow result.",
+ "run": "Run",
+ "running": "Running workflow…",
+ "staging": "Canvas staging",
+ "stagingSuccess": "Added the workflow result to canvas staging.",
+ "stale": "The layer or project changed while the workflow was running. Run it again.",
+ "title": "Run workflow from layer",
+ "unsupported": "This layer cannot be used as a workflow image input."
+ },
+ "addLayer": "Add layer",
+ "adjustments": {
+ "brightness": "Brightness",
+ "channel": "Channel",
+ "channels": {
+ "b": "Blue",
+ "g": "Green",
+ "r": "Red"
+ },
+ "contrast": "Contrast",
+ "curves": "Curves",
+ "curvesHint": "Drag points to bend the curve. Double-click empty space to add a point; double-click a point to remove it.",
+ "reset": "Reset adjustments",
+ "saturation": "Saturation",
+ "title": "Adjustments",
+ "transparencyLock": "Lock transparency"
+ },
+ "blendModes": {
+ "color": "Color",
+ "color-burn": "Color burn",
+ "color-dodge": "Color dodge",
+ "darken": "Darken",
+ "difference": "Difference",
+ "exclusion": "Exclusion",
+ "hard-light": "Hard light",
+ "hue": "Hue",
+ "lighten": "Lighten",
+ "luminosity": "Luminosity",
+ "multiply": "Multiply",
+ "normal": "Normal",
+ "overlay": "Overlay",
+ "saturation": "Saturation",
+ "screen": "Screen",
+ "soft-light": "Soft light"
+ },
+ "denoisingStrength": "Denoising strength",
+ "denoisingStrengthHelp": "How much the canvas image is changed. Lower keeps more of the original; higher allows bigger changes.",
+ "empty": "No layers yet. Add a layer or paint on the canvas to get started.",
+ "groupActions": {
+ "collapse": "Collapse group",
+ "expand": "Expand group",
+ "exportNothing": "No raster layer content to export",
+ "exportNotReady": "Layers are still loading — try exporting again in a moment",
+ "exportOverBudget": "Not enough raster memory is available to export this PSD",
+ "exportStale": "The canvas changed during export — try again",
+ "exportAborted": "PSD export was canceled",
+ "exportFailed": "PSD export failed",
+ "exportPsd": "Export to PSD",
+ "exportTooLarge": "Canvas is too large to export to PSD",
+ "hideAll": "Hide all",
+ "mergeNotReady": "Layers are still loading — try merging again in a moment",
+ "mergeVisible": "Merge visible",
+ "new": "New layer",
+ "showAll": "Show all",
+ "toggleVisibility": "Toggle group visibility"
+ },
+ "groups": {
+ "control": "Control Layers",
+ "inpaint_mask": "Inpaint Masks",
+ "raster": "Raster Layers",
+ "regional_guidance": "Regional Guidance"
+ },
+ "maskFill": {
+ "color": "Fill color",
+ "denoiseLimit": "Denoise limit",
+ "fill": "Fill",
+ "invert": "Invert mask",
+ "noiseLevel": "Noise level",
+ "style": "Fill style",
+ "styles": {
+ "crosshatch": "Crosshatch",
+ "diagonal": "Diagonal",
+ "grid": "Grid",
+ "horizontal": "Horizontal",
+ "solid": "Solid",
+ "vertical": "Vertical"
+ }
+ },
+ "menuGroups": {
+ "layers": "Layers",
+ "regional": "Regional"
+ },
+ "menu": {
+ "add": "Add",
+ "addModifiers": "Add modifiers",
+ "arrange": "Arrange",
+ "booleanOperations": "Boolean operations",
+ "convertTo": "Convert to",
+ "copyTo": "Copy to"
+ },
+ "options": "Layer options",
+ "properties": "Layer properties",
+ "regionalGuidance": {
+ "addReferenceImage": "Add reference image",
+ "autoNegative": "Auto-Negative",
+ "clearReferenceImage": "Clear reference image",
+ "method": "Method",
+ "methods": {
+ "composition": "Composition",
+ "full": "Full",
+ "style": "Style",
+ "style_precise": "Style (precise)",
+ "style_strong": "Style (strong)"
+ },
+ "model": "Model",
+ "negativePrompt": "Negative prompt",
+ "negativePromptPlaceholder": "Region negative prompt",
+ "positivePrompt": "Positive prompt",
+ "positivePromptPlaceholder": "Region positive prompt",
+ "referenceImage": "Reference image",
+ "referenceImageHelp": "Upload or drag an image from the gallery.",
+ "referenceImages": "Reference images",
+ "removeReferenceImage": "Remove reference image",
+ "selectModel": "Select a model",
+ "setReferenceImage": "Set reference image",
+ "weight": "Weight"
+ },
+ "types": {
+ "control": "Control",
+ "image": "Image",
+ "inpaint_mask": "Inpaint",
+ "paint": "Paint",
+ "regional_guidance": "Region"
+ }
+ },
+ "gallery": {
+ "alwaysShowDimensions": "Always show dimensions",
+ "archiveBoard": "Archive Board",
+ "boardActionsForBoard": "Board actions for {{name}}",
+ "boardGroups": {
+ "boards": "Boards",
+ "byDate": "By Date"
+ },
+ "boardItemCounts": "{{images}} images | {{assets}} assets",
+ "boardName": "Board name",
+ "commands": {
+ "clearSelection": "Clear gallery selection",
+ "deleteSelection": "Delete gallery selection",
+ "navigationDown": "Gallery navigation down",
+ "navigationLeft": "Gallery navigation left",
+ "navigationRight": "Gallery navigation right",
+ "navigationUp": "Gallery navigation up",
+ "selectAllOnPage": "Select all gallery images on page",
+ "toggleStarImage": "Toggle gallery image star"
+ },
+ "createBoardNamed": "Create board \"{{name}}\"",
+ "deleteBoard": "Delete Board",
+ "deleteBoardAndImages": "Delete Board and Images",
+ "deleteBoardDescription": "Deleted boards cannot be restored. Choose whether the board's images move to Uncategorized or are permanently deleted with it.",
+ "deleteBoardOnly": "Delete Board Only",
+ "deleteBoardQuestion": "Delete board \"{{name}}\"?",
+ "downloadBoard": "Download Board",
+ "dropImagesToUploadToBoard": "Drop images to upload to {{name}}",
+ "generationProgress": "Generation progress",
+ "generationProgressPercent": "Generation {{percentage}}%",
+ "hideArchivedBoards": "Hide archived boards",
+ "hideDateBoards": "Hide date boards",
+ "imageDensity": "Image density",
+ "imagesAriaLabel": "Gallery images",
+ "imageSort": "Image Sort",
+ "infinite": "Infinite",
+ "loadingBackendGallery": "Loading backend gallery...",
+ "loadingBoard": "Loading board...",
+ "newest": "Newest",
+ "noBoardsMatchSearch": "No boards match this search.",
+ "noImagesMatch": "No images match this board, tab, or search.",
+ "oldest": "Oldest",
+ "pagination": "Pagination",
+ "renameBoard": "Rename board",
+ "searchImagesAriaLabel": "Search gallery images",
+ "searchImagesPlaceholder": "Search images",
+ "searchOrCreateBoards": "Search or create boards",
+ "selectImageForPreview": "Select {{name}} for preview",
+ "selectedBoardFallback": "selected board",
+ "settings": "Gallery settings",
+ "settingsGridDensity": "Grid Density",
+ "settingsThumbnails": "Thumbnails",
+ "showArchivedBoards": "Show archived boards",
+ "showDateBoards": "Show date boards",
+ "showStarredImagesFirst": "Show starred images first",
+ "sortBoardsAscending": "Sort boards ascending",
+ "sortBoardsDescending": "Sort boards descending",
+ "starImage": "Star {{name}}",
+ "thumbnailFitAspect": "Aspect",
+ "thumbnailFitSquare": "Square",
+ "uncategorized": "Uncategorized",
+ "unarchiveBoard": "Unarchive Board",
+ "unknownBoard": "Unknown board",
+ "unstarImage": "Unstar {{name}}",
+ "uploadImagesToBoard": "Upload images to {{name}}",
+ "uploadsUnavailableForDateBoards": "Uploads are unavailable for date boards"
+ },
+ "serverStatus": {
+ "connected": "Connected to Server",
+ "connecting": "Connecting to Server",
+ "disconnected": "Disconnected from Server",
+ "label": "Server Status",
+ "labelWithError": "{{label}}: {{error}}"
+ },
+ "versionStatus": {
+ "chipLabel": "Version 7.0",
+ "description": "Invoke V7 shell version 7.0.",
+ "label": "Version"
+ }
+ }
+}
diff --git a/invokeai/frontend/webv2/public/locales/es.json b/invokeai/frontend/webv2/public/locales/es.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/es.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/fi.json b/invokeai/frontend/webv2/public/locales/fi.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/fi.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/fr.json b/invokeai/frontend/webv2/public/locales/fr.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/fr.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/he.json b/invokeai/frontend/webv2/public/locales/he.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/he.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/hu.json b/invokeai/frontend/webv2/public/locales/hu.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/hu.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/it.json b/invokeai/frontend/webv2/public/locales/it.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/it.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/ja.json b/invokeai/frontend/webv2/public/locales/ja.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/ja.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/ko.json b/invokeai/frontend/webv2/public/locales/ko.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/ko.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/mn.json b/invokeai/frontend/webv2/public/locales/mn.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/mn.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/nl.json b/invokeai/frontend/webv2/public/locales/nl.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/nl.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/pl.json b/invokeai/frontend/webv2/public/locales/pl.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/pl.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/pt-BR.json b/invokeai/frontend/webv2/public/locales/pt-BR.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/pt-BR.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/pt.json b/invokeai/frontend/webv2/public/locales/pt.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/pt.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/ro.json b/invokeai/frontend/webv2/public/locales/ro.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/ro.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/ru.json b/invokeai/frontend/webv2/public/locales/ru.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/ru.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/sv.json b/invokeai/frontend/webv2/public/locales/sv.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/sv.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/tr.json b/invokeai/frontend/webv2/public/locales/tr.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/tr.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/uk.json b/invokeai/frontend/webv2/public/locales/uk.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/uk.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/vi.json b/invokeai/frontend/webv2/public/locales/vi.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/vi.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/zh-CN.json b/invokeai/frontend/webv2/public/locales/zh-CN.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/zh-CN.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/public/locales/zh-Hant.json b/invokeai/frontend/webv2/public/locales/zh-Hant.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/invokeai/frontend/webv2/public/locales/zh-Hant.json
@@ -0,0 +1 @@
+{}
diff --git a/invokeai/frontend/webv2/scripts/i18n-report.mjs b/invokeai/frontend/webv2/scripts/i18n-report.mjs
new file mode 100644
index 00000000000..4c16f00b7de
--- /dev/null
+++ b/invokeai/frontend/webv2/scripts/i18n-report.mjs
@@ -0,0 +1,51 @@
+/* eslint-disable no-console */
+
+import { readdir, readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+
+const localeDir = join(process.cwd(), 'public/locales');
+const verbose = process.argv.includes('--verbose');
+
+const flattenKeys = (value, prefix = '') => {
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
+ return prefix ? [prefix] : [];
+ }
+
+ return Object.entries(value).flatMap(([key, child]) => flattenKeys(child, prefix ? `${prefix}.${key}` : key));
+};
+
+const readJson = async (fileName) => JSON.parse(await readFile(join(localeDir, fileName), 'utf8'));
+
+const localeFiles = (await readdir(localeDir)).filter((fileName) => fileName.endsWith('.json')).sort();
+const english = await readJson('en.json');
+const englishKeys = flattenKeys(english);
+const englishKeySet = new Set(englishKeys);
+let missingCount = 0;
+
+for (const fileName of localeFiles) {
+ if (fileName === 'en.json') {
+ continue;
+ }
+
+ const locale = await readJson(fileName);
+ const localeKeys = new Set(flattenKeys(locale));
+ const missing = englishKeys.filter((key) => !localeKeys.has(key));
+ const extra = [...localeKeys].filter((key) => !englishKeySet.has(key));
+
+ missingCount += missing.length;
+ console.log(`\n${fileName}`);
+ console.log(` missing: ${missing.length}`);
+ console.log(` extra: ${extra.length}`);
+
+ if (verbose) {
+ for (const key of missing) {
+ console.log(` - ${key}`);
+ }
+
+ for (const key of extra) {
+ console.log(` + ${key}`);
+ }
+ }
+}
+
+console.log(`\nTotal missing keys: ${missingCount}`);
diff --git a/invokeai/frontend/webv2/src/App.tsx b/invokeai/frontend/webv2/src/App.tsx
new file mode 100644
index 00000000000..9826a01e033
--- /dev/null
+++ b/invokeai/frontend/webv2/src/App.tsx
@@ -0,0 +1,17 @@
+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 { I18nController } from './workbench/i18n';
+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/appMetadata.ts b/invokeai/frontend/webv2/src/appMetadata.ts
new file mode 100644
index 00000000000..c3e51ede928
--- /dev/null
+++ b/invokeai/frontend/webv2/src/appMetadata.ts
@@ -0,0 +1 @@
+export const APP_VERSION = '7.0';
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/i18n.ts b/invokeai/frontend/webv2/src/i18n.ts
new file mode 100644
index 00000000000..9d014a8cbb1
--- /dev/null
+++ b/invokeai/frontend/webv2/src/i18n.ts
@@ -0,0 +1,20 @@
+import i18n from 'i18next';
+import Backend from 'i18next-http-backend';
+import { initReactI18next } from 'react-i18next';
+
+void i18n
+ .use(Backend)
+ .use(initReactI18next)
+ .init({
+ backend: {
+ loadPath: `${import.meta.env.BASE_URL}locales/{{lng}}.json`,
+ },
+ debug: false,
+ fallbackLng: 'en',
+ interpolation: {
+ escapeValue: false,
+ },
+ returnNull: false,
+ });
+
+export default i18n;
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..6c81cc73d19
--- /dev/null
+++ b/invokeai/frontend/webv2/src/main.tsx
@@ -0,0 +1,18 @@
+import '@fontsource/inter/index.css';
+import './i18n';
+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..141bb865a3e
--- /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..a3ca67514b1
--- /dev/null
+++ b/invokeai/frontend/webv2/src/theme/recipes.ts
@@ -0,0 +1,361 @@
+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',
+ },
+ },
+ defaultVariants: {
+ ...chakraSlotRecipes.menu.defaultVariants,
+ size: 'sm',
+ },
+});
+
+/** 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.03em',
+ },
+});
+
+/** 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 `