From bdf5827ff9e37e34b7e9b003fd679fb33f9486e5 Mon Sep 17 00:00:00 2001 From: joshistoast Date: Tue, 9 Jun 2026 15:02:09 -0600 Subject: [PATCH 001/153] feat(v7): initial shell setup --- invokeai/frontend/webv2/.gitignore | 3 + invokeai/frontend/webv2/.oxfmtrc.json | 13 + invokeai/frontend/webv2/.oxlintrc.json | 71 + invokeai/frontend/webv2/.prettierrc.json | 9 + invokeai/frontend/webv2/index.html | 12 + invokeai/frontend/webv2/package.json | 41 + invokeai/frontend/webv2/pnpm-lock.yaml | 2780 +++++++++++++++++ invokeai/frontend/webv2/src/App.tsx | 13 + invokeai/frontend/webv2/src/lucide-icons.d.ts | 6 + invokeai/frontend/webv2/src/main.tsx | 16 + invokeai/frontend/webv2/src/theme/system.ts | 92 + .../webv2/src/workbench/WorkbenchContext.tsx | 126 + .../webv2/src/workbench/WorkbenchShell.tsx | 89 + .../src/workbench/components/BottomPanel.tsx | 40 + .../src/workbench/components/CenterArea.tsx | 170 + .../components/GraphPreviewDialog.tsx | 82 + .../components/GraphSurfaceActions.tsx | 84 + .../workbench/components/InvokeControl.tsx | 216 ++ .../workbench/components/LayoutPresetMenu.tsx | 78 + .../webv2/src/workbench/components/Panels.tsx | 51 + .../src/workbench/components/ProjectTabs.tsx | 95 + .../components/ShellErrorSurface.tsx | 60 + .../src/workbench/components/StatusBar.tsx | 206 ++ .../webv2/src/workbench/components/TopBar.tsx | 140 + .../src/workbench/components/WidgetBar.tsx | 174 ++ .../components/WidgetFailureBoundary.tsx | 56 + .../src/workbench/components/WidgetFrames.tsx | 184 ++ .../workbench/components/WidgetRenderer.tsx | 22 + .../webv2/src/workbench/graphSurfaces.ts | 24 + .../webv2/src/workbench/iconResolver.tsx | 54 + .../webv2/src/workbench/invocation.ts | 106 + .../webv2/src/workbench/layoutPresets.ts | 59 + .../webv2/src/workbench/persistence.ts | 53 + .../frontend/webv2/src/workbench/types.ts | 298 ++ .../webv2/src/workbench/widgetRegistry.ts | 76 + .../AutosaveStatusWidgetView.tsx | 37 + .../widgets/autosave-status/index.ts | 14 + .../widgets/canvas/CanvasWidgetView.tsx | 63 + .../src/workbench/widgets/canvas/index.ts | 14 + .../widgets/gallery/GalleryWidgetView.tsx | 50 + .../src/workbench/widgets/gallery/index.ts | 13 + .../widgets/generate/GenerateWidgetView.tsx | 15 + .../src/workbench/widgets/generate/index.ts | 14 + .../HistoryControlsWidgetView.tsx | 82 + .../widgets/history-controls/index.ts | 13 + .../widgets/layers/LayersWidgetView.tsx | 58 + .../src/workbench/widgets/layers/index.ts | 13 + .../LayoutActionsWidgetView.tsx | 69 + .../workbench/widgets/layout-actions/index.ts | 13 + .../widgets/queue/QueueWidgetView.tsx | 51 + .../src/workbench/widgets/queue/index.ts | 13 + .../server-status/ServerStatusWidgetView.tsx | 22 + .../workbench/widgets/server-status/index.ts | 14 + .../VersionStatusWidgetView.tsx | 22 + .../workbench/widgets/version-status/index.ts | 14 + .../widgets/workflow/WorkflowWidgetView.tsx | 33 + .../src/workbench/widgets/workflow/index.ts | 18 + .../webv2/src/workbench/workbenchState.ts | 750 +++++ invokeai/frontend/webv2/tsconfig.json | 21 + invokeai/frontend/webv2/tsconfig.node.json | 11 + invokeai/frontend/webv2/vite.config.mts | 32 + 61 files changed, 7068 insertions(+) create mode 100644 invokeai/frontend/webv2/.gitignore create mode 100644 invokeai/frontend/webv2/.oxfmtrc.json create mode 100644 invokeai/frontend/webv2/.oxlintrc.json create mode 100644 invokeai/frontend/webv2/.prettierrc.json create mode 100644 invokeai/frontend/webv2/index.html create mode 100644 invokeai/frontend/webv2/package.json create mode 100644 invokeai/frontend/webv2/pnpm-lock.yaml create mode 100644 invokeai/frontend/webv2/src/App.tsx create mode 100644 invokeai/frontend/webv2/src/lucide-icons.d.ts create mode 100644 invokeai/frontend/webv2/src/main.tsx create mode 100644 invokeai/frontend/webv2/src/theme/system.ts create mode 100644 invokeai/frontend/webv2/src/workbench/WorkbenchContext.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/WorkbenchShell.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/BottomPanel.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/CenterArea.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/GraphPreviewDialog.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/GraphSurfaceActions.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/InvokeControl.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/LayoutPresetMenu.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/Panels.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/ProjectTabs.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/ShellErrorSurface.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/StatusBar.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/TopBar.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/WidgetBar.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/WidgetFailureBoundary.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/WidgetFrames.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/components/WidgetRenderer.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/graphSurfaces.ts create mode 100644 invokeai/frontend/webv2/src/workbench/iconResolver.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/invocation.ts create mode 100644 invokeai/frontend/webv2/src/workbench/layoutPresets.ts create mode 100644 invokeai/frontend/webv2/src/workbench/persistence.ts create mode 100644 invokeai/frontend/webv2/src/workbench/types.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgetRegistry.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/autosave-status/AutosaveStatusWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/autosave-status/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/canvas/CanvasWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/canvas/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/gallery/GalleryWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/gallery/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/generate/GenerateWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/generate/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/history-controls/HistoryControlsWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/history-controls/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/layers/LayersWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/layers/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/layout-actions/LayoutActionsWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/layout-actions/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/queue/QueueWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/queue/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/server-status/ServerStatusWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/server-status/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/version-status/VersionStatusWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/version-status/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/workflow/WorkflowWidgetView.tsx create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/workflow/index.ts create mode 100644 invokeai/frontend/webv2/src/workbench/workbenchState.ts create mode 100644 invokeai/frontend/webv2/tsconfig.json create mode 100644 invokeai/frontend/webv2/tsconfig.node.json create mode 100644 invokeai/frontend/webv2/vite.config.mts 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..2f6438ef842 --- /dev/null +++ b/invokeai/frontend/webv2/.oxfmtrc.json @@ -0,0 +1,13 @@ +{ + "$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" +} diff --git a/invokeai/frontend/webv2/.oxlintrc.json b/invokeai/frontend/webv2/.oxlintrc.json new file mode 100644 index 00000000000..45c40135d20 --- /dev/null +++ b/invokeai/frontend/webv2/.oxlintrc.json @@ -0,0 +1,71 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "react", "import", "unicorn", "oxc"], + "categories": { + "correctness": "error" + }, + "rules": { + "curly": "error", + "eqeqeq": "error", + "import/no-cycle": "error", + "import/no-duplicates": "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", + "react/jsx-curly-brace-presence": [ + "error", + { + "props": "never", + "children": "never" + } + ], + "react-hooks/exhaustive-deps": "error", + "react-hooks/rules-of-hooks": "error", + "require-await": "error", + "typescript/consistent-type-imports": [ + "error", + { + "fixStyle": "separate-type-imports", + "prefer": "type-imports" + } + ], + "typescript/no-empty-interface": [ + "error", + { + "allowSingleExtends": true + } + ] + }, + "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/.prettierrc.json b/invokeai/frontend/webv2/.prettierrc.json new file mode 100644 index 00000000000..3395dfb9391 --- /dev/null +++ b/invokeai/frontend/webv2/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/prettierrc", + "trailingComma": "es5", + "printWidth": 120, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "endOfLine": "auto" +} diff --git a/invokeai/frontend/webv2/index.html b/invokeai/frontend/webv2/index.html new file mode 100644 index 00000000000..c10d0d23c82 --- /dev/null +++ b/invokeai/frontend/webv2/index.html @@ -0,0 +1,12 @@ + + + + + + Invoke V7 Workbench + + +
+ + + diff --git a/invokeai/frontend/webv2/package.json b/invokeai/frontend/webv2/package.json new file mode 100644 index 00000000000..346764388ea --- /dev/null +++ b/invokeai/frontend/webv2/package.json @@ -0,0 +1,41 @@ +{ + "name": "@invoke-ai/invoke-ai-webv2", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "dev:host": "vite dev --host", + "build": "pnpm run lint && vite build", + "fix": "pnpm run lint:oxc:fix && pnpm run format:write", + "format:check": "oxfmt --check .", + "format:write": "oxfmt --write .", + "lint": "pnpm run format:check && pnpm run lint:oxc && pnpm run lint:tsc", + "lint:oxc": "oxlint --react-plugin --import-plugin --deny-warnings .", + "lint:oxc:fix": "oxlint --react-plugin --import-plugin --fix .", + "preview": "vite preview", + "lint:tsc": "tsc --noEmit" + }, + "dependencies": { + "@chakra-ui/react": "^3.28.0", + "@emotion/react": "^11.14.0", + "lucide-react": "^1.17.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-icons": "^5.5.0" + }, + "devDependencies": { + "@types/node": "^22.15.1", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react-swc": "^3.9.0", + "oxfmt": "^0.54.0", + "oxlint": "^1.69.0", + "typescript": "^5.8.3", + "vite": "^7.0.5" + }, + "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..0bb24af1f3f --- /dev/null +++ b/invokeai/frontend/webv2/pnpm-lock.yaml @@ -0,0 +1,2780 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@chakra-ui/react': + specifier: ^3.28.0 + version: 3.35.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) + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@19.2.17)(react@19.2.7) + lucide-react: + specifier: ^1.17.0 + version: 1.17.0(react@19.2.7) + react: + specifier: ^19.1.0 + version: 19.2.7 + react-dom: + specifier: ^19.1.0 + version: 19.2.7(react@19.2.7) + react-icons: + specifier: ^5.5.0 + version: 5.6.0(react@19.2.7) + devDependencies: + '@types/node': + specifier: ^22.15.1 + version: 22.19.20 + '@types/react': + specifier: ^19.1.8 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react-swc': + specifier: ^3.9.0 + version: 3.11.0(@swc/helpers@0.5.23)(vite@7.3.5(@types/node@22.19.20)) + oxfmt: + specifier: ^0.54.0 + version: 0.54.0 + oxlint: + specifier: ^1.69.0 + version: 1.69.0 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^7.0.5 + version: 7.3.5(@types/node@22.19.20) + +packages: + + '@ark-ui/react@5.36.2': + resolution: {integrity: sha512-2lrZ7+Qtlj7hGx4qU2jZkE892JNrkULg/fUxqUuqmQfv9UGAXhdcw1Hr3N+zBgMDVz3aqip0Qa4v0Mox09MMvg==} + 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/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + 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-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/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@chakra-ui/react@3.35.0': + resolution: {integrity: sha512-qzfRNLwxKjxx2IXjBj6uz1nYI+pKsq6uwHxO619+hx1OzNNuwLIjEHJxnDfBzoynO7sPCBlubMwFWb1e1PrXzw==} + peerDependencies: + '@emotion/react': '>=11' + react: '>=18' + react-dom: '>=18' + + '@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==} + + '@internationalized/date@3.12.0': + resolution: {integrity: sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==} + + '@internationalized/number@3.6.5': + resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@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==} + + '@oxfmt/binding-android-arm-eabi@0.54.0': + resolution: {integrity: sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.54.0': + resolution: {integrity: sha512-B4VZfBUlKK1rmMChsssNZbkZjE8+FzG3avMjGgMDwbGxXRoXkoeXiAZ+78Oa+eyDPHvDCiUb4zH/vmCOUSafLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.54.0': + resolution: {integrity: sha512-i02vF75b+ePsQP3tHqSxVYI5S6b8X/xqdPu7/mDHXtpgXLTYXi3jJmfHU0j+dnZZDKaYTx/ioCK7QYJmtiJR2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.54.0': + resolution: {integrity: sha512-8VMFvGvooXj7mswkbrhdVZ2/sgiDaBzWpkkbtO+qGDLV4EfJd67nQadHkQC0ZNbaWA9ajXfqI6i7PZLIeDzxEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.54.0': + resolution: {integrity: sha512-0cRHnp43WN1Jrc5s0BdbdKgR1XirdvHy7TAFi3JEsoEVQVJxTXMbpVd76sxXlgRswNMDhVFSJw+y7Eb8mEavFQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': + resolution: {integrity: sha512-JyQAk3hK/OEtup7Rw6kZwfdzbKqTVD5jXXb8Xpfay29suwZyfBDMVW/bj4RqEPySYWc6zCp198pOluf8n5uYzg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.54.0': + resolution: {integrity: sha512-qnvLatTpM8vtvjOfcckBOzJjk+n6ce/wwpP8OFeUrD5aNLYcKyWAitwj+Rk3PK9jGanbZvKsJnv14JGQ6XqFdw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.54.0': + resolution: {integrity: sha512-SMkhnCzIYZYDk9vw3W/80eeYKmrMpGF0Giuxt4HruFlCH7jEtnPeb3SdQKMfgYi/dgtaf+hZAb5XWPYnxqCQ3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxfmt/binding-linux-arm64-musl@0.54.0': + resolution: {integrity: sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxfmt/binding-linux-ppc64-gnu@0.54.0': + resolution: {integrity: sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxfmt/binding-linux-riscv64-gnu@0.54.0': + resolution: {integrity: sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxfmt/binding-linux-riscv64-musl@0.54.0': + resolution: {integrity: sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxfmt/binding-linux-s390x-gnu@0.54.0': + resolution: {integrity: sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxfmt/binding-linux-x64-gnu@0.54.0': + resolution: {integrity: sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxfmt/binding-linux-x64-musl@0.54.0': + resolution: {integrity: sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxfmt/binding-openharmony-arm64@0.54.0': + resolution: {integrity: sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.54.0': + resolution: {integrity: sha512-J0SSB8Z1Fre2sxRolYcW6Rl1RQmKdQ2hnHyq4YJrfBRiXTObLw4DXnIVraM/UyqGqwOi7yTrQA4VT7DPxlHVKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.54.0': + resolution: {integrity: sha512-O61UDVj8zz6yXJjkHPf05VaMLOXmEF8P5kf/N0W7AQMmd6bcQogl+KJc7rMutKTL524oE9iH32JXZClBFmEQIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.54.0': + resolution: {integrity: sha512-1MDpqJPiFqxWtIHas8vkb1VZ7f7eKyTffAwmO8isxQYMaG1OFKsH666BWLeXQLO+IWNfiMssLD55hbR1lIPTqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.69.0': + resolution: {integrity: sha512-DKQQbD5cZ/MYfDgDI7YGyGD9FSxABlsBsYFo5p26lloob543tP9+4N3guwdXIYJN+7HSZxLe8YJuwcOWw5qnHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.69.0': + resolution: {integrity: sha512-lEhb+I5pr4inux+JFwfCa1HRq3Os7NirEFQ0H1I35SVEHPm6byX0Ah47xmRha3qi6LAkxUcxViL8o/9PivjzBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.69.0': + resolution: {integrity: sha512-GY2YE8lOZW59BW1Ia1y+1gR0XyjrZRvVWHAr8LGeGhYHE0OQJ/7cRKXTkx1P+E9/6awEc3SX8a68SFTjh/E//A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.69.0': + resolution: {integrity: sha512-ax1oZnOjHX3LB7myQyHEaQkDwfLb6str3/nSP6O7EVUviQGNkEGzGV0EqcBJWK+Ufwx0l4xPgyYayurvhAdl2Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.69.0': + resolution: {integrity: sha512-kHWeHv4g2h8NY+mpCxzCtY4uerMJWTN/TSnNj1CPbakFpHEJ6cTya2wWV0pDSYWOJ2+0UiEbhn3AtXxHtsnKjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.69.0': + resolution: {integrity: sha512-gq84vM1a1oEehXo27YCDzGVcxPsZDI1yswZwz2Da1/cbnWtrL16XZZnz0G/+gIU8edtHpfjxq5c+vWEHqJfWoQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.69.0': + resolution: {integrity: sha512-kIqEa98JQ0VRyrcncxA417m2AzasqTlD+FyVT1AksjvjkqQcvm7pBWYvoW3/mpyOP2XYvi5nSCCTIe6De1yu5g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.69.0': + resolution: {integrity: sha512-j+xYiXozxGWx2cpjCrwwGR4awTxPFsRv3JZrv23RCogEPMc4R7UqjHW47p/RG0aRlbWiROCJ8coUfCwy0dvzHA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-arm64-musl@1.69.0': + resolution: {integrity: sha512-xEPpNppTfN1l/nM7gYSf9iocscu/as+p/7vxkLeLEKnYU+09Dm+5V6IhDYDh+Uz6FajEupWwCLt5SOG0y1PCKg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-ppc64-gnu@1.69.0': + resolution: {integrity: sha512-Ug0+eU7HJBlek+SjklYH62IlOMirEJsdxpihH0kSqX0XdrDD4NdHpQc10fK1JC35yn6KrrcN+uYzlHD38XAf8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxlint/binding-linux-riscv64-gnu@1.69.0': + resolution: {integrity: sha512-iEyI3GIg0l/s3G4qy2TlaaWKdzj4PJJStwtlocpDTC00PY9hZueotf6OKUj9+yfQh0lrpBW/pLMgTztbAHKJEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-riscv64-musl@1.69.0': + resolution: {integrity: sha512-NjHjpiI4WIKSMwuoJSZi5VToPeoYOS1FR52HLIDG6lidMdqquusgtODb4iLk0+lb1q3Z0nv2/aPRcC/olmpQGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-s390x-gnu@1.69.0': + resolution: {integrity: sha512-Ai/prDewoItkDXbp38gwGZi41DycZbUTZJ3UidwoHgQC0/DaqC2TGdtBTQLJ6hSD+SAxASzh8+/eSBPmxfOacA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxlint/binding-linux-x64-gnu@1.69.0': + resolution: {integrity: sha512-Gt3KHgp46mRKz4sJeaASmKvD8ayXookRw07RMf+NowhEztGGDZ7VrXpoW96XuKJLjFukWizOFVNjmYb/u7caNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-linux-x64-musl@1.69.0': + resolution: {integrity: sha512-7tQhJ2+p/oHv1zcfnjYI7YVzC/7iBaVOfIvFYtxdJ5F45mWgEdrCyXZXZGfiLey5t/5JhOhsaMnnv1kAzckd7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-openharmony-arm64@1.69.0': + resolution: {integrity: sha512-vmWz6TKp/3hfA4lksR0zHBv/6xuX1jhym6eqOjdH2DXsDDHZWcp2f0KG0VCAnlVbIrjk29G4wAWMXb/Hn1YobA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.69.0': + resolution: {integrity: sha512-9RExaLgmaw6IoIkU9cTpT71mLfI0xZ86iZH8x518LVsOkjquJMYqb9P7KpC8lgd1t0Dxs41p2pxynq4XR3Ttzw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.69.0': + resolution: {integrity: sha512-1907kRPF8/PrcIw1E7LMs9JbVrpgnt/MvFdss3an8oDkYNAACXzTntV3t3869ZZhMZxb2AzRGbz1pA/jdFatXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.69.0': + resolution: {integrity: sha512-w8SOXv3mT9Fi6jY8OXdXCfnvX/3KNLXGNr4HEz2TA7S4Mv/PYAOmpB8y/ge40mxvBMgGNaSaaDwZpAsQn7HtWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@pandacss/is-valid-prop@1.11.3': + resolution: {integrity: sha512-YaHK+p5DaN8AUpsRx5OqqGxaZzn8uNIdVhP+K1cjvjv3+Qa9D/75/A1dPyLmfKrSRJc8UoR9WN9fxQX0rVzhzQ==} + engines: {node: '>=20'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.61.1': + resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.1': + resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.1': + resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.1': + resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.1': + resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.1': + resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.61.1': + resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.61.1': + resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.61.1': + resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.61.1': + resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.61.1': + resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.1': + resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.1': + resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.1': + resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==} + cpu: [x64] + os: [win32] + + '@swc/core-darwin-arm64@1.15.40': + resolution: {integrity: sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.40': + resolution: {integrity: sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.40': + resolution: {integrity: sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.40': + resolution: {integrity: sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.40': + resolution: {integrity: sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-ppc64-gnu@1.15.40': + resolution: {integrity: sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/core-linux-s390x-gnu@1.15.40': + resolution: {integrity: sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.40': + resolution: {integrity: sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.40': + resolution: {integrity: sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.40': + resolution: {integrity: sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.40': + resolution: {integrity: sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.40': + resolution: {integrity: sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.40': + resolution: {integrity: sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.19.20': + resolution: {integrity: sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==} + + '@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==} + + '@vitejs/plugin-react-swc@3.11.0': + resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7 + + '@zag-js/accordion@1.40.0': + resolution: {integrity: sha512-YDdyvZJ6fr92RZazyXQq+juT3ZA0ubjDISptb5YPgMoTPdnjKNiICPpMeCeVj1ncYRDkHXrOdChS/5CtuX/K6g==} + + '@zag-js/anatomy@1.40.0': + resolution: {integrity: sha512-oiB4uAaV//L38JluLVPtOHO3xvqambrfrXVOoq4kmNrBv1LLlCmFvrXA2HOR9lakn4ExK27XSUrKhUN7YlKjfQ==} + + '@zag-js/angle-slider@1.40.0': + resolution: {integrity: sha512-6X6bOBoCyYG0/lFY0Y+AXJZZG6CeYQiWkcMXvegxCC2zxthodqOVzkVOASW+6rzLjn2bru+V5O9RMjNgmCumKg==} + + '@zag-js/aria-hidden@1.40.0': + resolution: {integrity: sha512-lNWujEIlfGKwMQIcgfXuOZSsJD2avrgPsQHrXNVF9mkXygjLFcIRKz2pEexTSCqFh/HuUZJ6rG4pM/hJ/BiVCw==} + + '@zag-js/async-list@1.40.0': + resolution: {integrity: sha512-hLGUTtwRFl6FIdYxSIYSeLQjJeG4isKpdmGCUvtWNnKr7ayf1yAkkSwX10SdBMWOCldbtvKCZXumKvP6dDwNvw==} + + '@zag-js/auto-resize@1.40.0': + resolution: {integrity: sha512-eZC+AGKUip7UMu41/ApeT1wCIgn2fmo63FJeGAdMMD8E9M8M7QLsfISMIoieNNGBAYWhSyqELQ3jPgkUf6xReA==} + + '@zag-js/avatar@1.40.0': + resolution: {integrity: sha512-DayZDsNXbipT+1GUkX29tVhO4hZonDnidwE3SjEQv9Ic9vCdnwP95+B0FPEuaca03F5ZXFqVXjnPmRVbRMyDYQ==} + + '@zag-js/carousel@1.40.0': + resolution: {integrity: sha512-9svWc2jjvUP8iQ0afuu/ZAI75PuPLm4qB7h+10rmDrAgUPn7fwUBVzyATKubJPdtmaYQQvTTIiZU2B8mV88oGg==} + + '@zag-js/cascade-select@1.40.0': + resolution: {integrity: sha512-0fkE0Fd2VQ4QsaWXHdgQxHWiaef3UWW0l6Jd47frtMNnrvg5t5Xfqowa7c2S23hcduOUfz2WC0xEuGXnO4UVDQ==} + + '@zag-js/checkbox@1.40.0': + resolution: {integrity: sha512-oFCgnkOjrUDejB1wEp5s3cyJ+uFe/GoI3+wqNyckqOtcdKL1MBxy193GYVdj0LDfuCNrk8V0aIJGTdusCD2b4A==} + + '@zag-js/clipboard@1.40.0': + resolution: {integrity: sha512-QbFhJMwwUxTKcbWyb9ZrKgAp13U4+IzfHSLhPxbDVSQ15mIrjIkjW68gS6ElzhRDwGr1qawkZVApsqcToUqSaQ==} + + '@zag-js/collapsible@1.40.0': + resolution: {integrity: sha512-xDLY4j9D3gdoTirkwzMaCtelfCjnMhBzPyY6c/mh4oPvD3RB6dr3V3kI80i3yxHaUUeDCIUm/XAxK0InPsRBug==} + + '@zag-js/collection@1.40.0': + resolution: {integrity: sha512-+3o1nvbcA9Kz2hDDFf8Kngpd+of33S4TS5Tb9KvrHlU5ieQdvEUtc7/pWG2aCTkGpmgda+j91akB6ZB8+oVkvA==} + + '@zag-js/color-picker@1.40.0': + resolution: {integrity: sha512-lT93xd1BlNBbitl2RxST8ARYE6q/HZD5a0QhMIT1RbndB8F4e9j/NxkStgE9f0QqgpC/rO+nKHLoR+H1xs/EkA==} + + '@zag-js/color-utils@1.40.0': + resolution: {integrity: sha512-PZihcGheb5bn0/cEUwozjJjPoKkEwlJNpTA5mUxj/+sOElLaZM+zY2AnGYeMl6w5zIyZZUDoJMIT5rcb5sN87g==} + + '@zag-js/combobox@1.40.0': + resolution: {integrity: sha512-5IVCDrB8m7XrKBu28j7bIRE5KiyKJLPDZB3AJ+PLJyL69D+9z1anhLDmkUYcPseyCasszLKzIejby+kYQJgHlA==} + + '@zag-js/core@1.40.0': + resolution: {integrity: sha512-0YcqCh7TmhSonkbKM/7NWolxlaQgvvXgqedocW9oeRYiDJIpBZyRqnHPoGAS2XwbBPkCnrqSosxSF5yBjhZpgw==} + + '@zag-js/date-input@1.40.0': + resolution: {integrity: sha512-/VU8g3dugggC5xW2OJW1KONWzPkEbK/yLA0lPxymW/Uo0ixh2mKJUVTOTqDFWf1b0vzLX2XlYoLL+I2ryUyPvA==} + peerDependencies: + '@internationalized/date': '>=3.0.0' + + '@zag-js/date-picker@1.40.0': + resolution: {integrity: sha512-Nm3aSKn/5tGOZk8rIddLyBk+oeE0zr/ZsJuuTc3rysd04owVy1UhmUh6X9CqfTJtwTDpUZe+orHaIvKlE3Rd0w==} + peerDependencies: + '@internationalized/date': '>=3.0.0' + + '@zag-js/date-utils@1.40.0': + resolution: {integrity: sha512-nuB1QM3X7yY0k2JiZbHHm6wigY+Cl1QK6sRlh+C7mOyzEKnNEqNSVIqgSionCtWO6zAZh1R8Znp5ZeCdbbc27w==} + peerDependencies: + '@internationalized/date': '>=3.0.0' + + '@zag-js/dialog@1.40.0': + resolution: {integrity: sha512-1FHxR7/Kuu+9K2dxH7dKlSckCZ26n5ec79qWr0aMSSs2DF+ypQf5GUlaS6z2UqroZvIoJCvABVMm9OMko/qxlA==} + + '@zag-js/dismissable@1.40.0': + resolution: {integrity: sha512-bBkFvPg/zbYn31ZgEfx8not6s2Ekx7zU2sO8tGXb8rYPnHBfGDYEzVQansUStJn0Atzw+y7XR7B3G3u5AFQJKw==} + + '@zag-js/dom-query@1.40.0': + resolution: {integrity: sha512-4J3EO2gHpZ1VZiGLuMlH6G1Tsp4gKB8PPt2yKeNQWYGEXyrHUXrvMhRUzv7Z4/2I1s1tnxlFG4F8ovB3kTpz/Q==} + + '@zag-js/drawer@1.40.0': + resolution: {integrity: sha512-N2OR5ZYuTsWkYYmwsNgmL+wfuM3qUxB8GAfo53AWvOh07QUVz1Dvh1WP4km5L6Tkz4UBQZACu8T/ZLyeZ+PdWg==} + + '@zag-js/editable@1.40.0': + resolution: {integrity: sha512-X23wOg42BPvFWfJQi3yd8HiL8xtisrpL5ouFEzba56SQIxWZHDRpeWoqXqyLODq2/z2+SsZ0wV3laRD3ZH0C2g==} + + '@zag-js/file-upload@1.40.0': + resolution: {integrity: sha512-hUZlJYjSGk7SAflTmQIjZv6M+icujaHS6I+dik2LM48rLWwNa/GYTNx+uY4zJLd9oW1eEj+6NcCYZpPWzKku4Q==} + + '@zag-js/file-utils@1.40.0': + resolution: {integrity: sha512-BGny4rafiBQ5TPCBXfzbH7lSyFdnoix7brq/+FllKpDqpWPQz0tIsgSZueF/Z8GPTrAkwMKOFI99P7OVhAhRig==} + + '@zag-js/floating-panel@1.40.0': + resolution: {integrity: sha512-e2QXwapCbjLJnU+MAz06CoByj4XJ3sdSBgWF+PSe2X2T8dd/FkZUnaDPaX0yyfyTWKzBbyRRNyon2LMAs8ndHw==} + + '@zag-js/focus-trap@1.40.0': + resolution: {integrity: sha512-Q6W+DU7pix5rtRwoDnYzTYMkUV2kMWrFV0/EdNN3spFSvnUSkDWRmcNpzf+56AuCNeqsAZxaLJpsHLZkcT2xrw==} + + '@zag-js/focus-visible@1.40.0': + resolution: {integrity: sha512-63byl/kLVzDYlnHFma4HKEKrqB1Vx2zg0sBmUSENPyh+Ia1xhEVVC5vu6GX7nu4t/8QRy3Jn0q7T5og81FGb1A==} + + '@zag-js/highlight-word@1.40.0': + resolution: {integrity: sha512-+aeVn3S5NPG6Tk4Sanl0VZk/0atjnF7Xy7POPs1HD5SBui29/6i3vn3bUBNXJXrnhUoNrUhuySVYVhgkffcQ7w==} + + '@zag-js/hover-card@1.40.0': + resolution: {integrity: sha512-lkuLaikPLBIOnR0X75kSXdDYgv3ritAsn4TF1eGs12iYnZVX4PTL3J39tVNm9QrEXZ+iKcA1D2cUXNhEteCTyA==} + + '@zag-js/i18n-utils@1.40.0': + resolution: {integrity: sha512-8D3ki9V81gMKZvtRfNVoHCBDVYjr+WJLBvdfSv3cdOsVM2/E8//xAfYbYzl5Fdmeny3H71fxBNqOX05GN4K6OA==} + + '@zag-js/image-cropper@1.40.0': + resolution: {integrity: sha512-bpTCaiUXM0Mh6ddoJ1fA1B/YXp5Fc8LA0hg8CuEByDwGRVKPJ0KotL6QXMF6cEJZ1fcHF3Lcmpbj5Xotfkr4mA==} + + '@zag-js/interact-outside@1.40.0': + resolution: {integrity: sha512-Fws+O4uD9vS0I5KVcf3U2tNjLKvqlv+RExFbTywckDLOCJ145M/pMQWTr1FHil04jk5PFyM1iGfsbom8tozHpQ==} + + '@zag-js/json-tree-utils@1.40.0': + resolution: {integrity: sha512-7zEzU59Gz76nV7n3l70uMB5yAOOQMmt1PTAni6S97uw7/6KzPktsEWBcw7ocC4IIA42PKdT7akpq721H0vthbA==} + + '@zag-js/listbox@1.40.0': + resolution: {integrity: sha512-zB33y+dk6/e0ZTs3wun2KsuPaH/wygOuD8scnH2a2Y/W9a2P1rq503Kgm5d5kVXBKQLxOBwievWJ8Blajv8LnA==} + + '@zag-js/live-region@1.40.0': + resolution: {integrity: sha512-i1Dx02KGcQOAZGNhkFe8kz26gYJcn7KsT/M1UovjS9RTbl9diY8ShiyfIAhqruoaHQyqsHMRh/f7Idu45HdiDA==} + + '@zag-js/marquee@1.40.0': + resolution: {integrity: sha512-XfvAwSNYXV3fEIRc44a9sAsoJoLKt+CWbpSPgQBpiFPpWh0rZ8frUZCslevTzBB3ifIWoSg+svDHQOGsDa8wGA==} + + '@zag-js/menu@1.40.0': + resolution: {integrity: sha512-FRBqwsOjxBi0eSwqwrOw2td1rd0Xxl0f41J2lGc8E7z+2PabbBcJ/poqSiEn8YoaCT4mAWNjt4QQU/Pe1bRJ/g==} + + '@zag-js/navigation-menu@1.40.0': + resolution: {integrity: sha512-aJkEGYH8P9NfsQOjxMzxuF4YrrV2N1GQj6Y5Ow19MKuLh42o35bUhwoGsYjFbxgEcImabINtZJqtAPAkOdJXmQ==} + + '@zag-js/number-input@1.40.0': + resolution: {integrity: sha512-WffdeqSOpsKmgPzBkNZl9nAolQPlyl9dIabaPguGgXdYtZW/OGCGj8jCYqyEu4VL3kDPPVVQRWEqC/XzwzVCRg==} + + '@zag-js/pagination@1.40.0': + resolution: {integrity: sha512-Ykotky0A/7rswb6BfOD9aXL1EssKwUYfBRbdWGe52uhVc7dGagMSTUDRVeNhVsP/MEdtwqys7urvDbAlEqq+GA==} + + '@zag-js/password-input@1.40.0': + resolution: {integrity: sha512-mD4tbA4m82oV+0NbJ+P00Q4Gwz+zf1kZEZ3Z48ohICfK/WO1KhCgviY7vu/7bCMnRiD3dbi+nEeym8Kb29wRHw==} + + '@zag-js/pin-input@1.40.0': + resolution: {integrity: sha512-iJIXDJC+9DUx+A3sRdTmHV7vPZXCw9O6le3R0lKf/8kQOgj7FKjbVw2SkUMAoOZ0u5J7Zwg2oZc7ddt1pwUk9w==} + + '@zag-js/popover@1.40.0': + resolution: {integrity: sha512-bjvOep1YNlsvIYGh/rPsFCHjH2cCt2aKsVLyRvzTT1jhGZJvBdQKQBJjSuG5Nh4y1PUqtrrz69ZMWRrJGQ3rNg==} + + '@zag-js/popper@1.40.0': + resolution: {integrity: sha512-rCkgqgwlpgMwcnuSVrZK2xXl1Mvptpuw3cZy6rC2C5F3yE1GmWohdts5VkeQNro+sd/xHTdVovOqY6cU9Htj1w==} + + '@zag-js/presence@1.40.0': + resolution: {integrity: sha512-P0bAuzEIDuMglE1xfmW5xTuSBlWjNZ8nOGXoIksKOKb+b+jy2Vys6WjZjKipV/jop4u85wfzKchcPc3C+cXuog==} + + '@zag-js/progress@1.40.0': + resolution: {integrity: sha512-V61a5CHEs8suevQVS+/1ENj1RDVYNOUUTawK6uriCA6Ol59xe30DmF+eV6Y9miM7L/pN3YjZRq9uEDJMXXK32g==} + + '@zag-js/qr-code@1.40.0': + resolution: {integrity: sha512-xD37tVrQ46CeqVLqkSm61kURoJ4Z/uOFcB8z7Hu3UX+1OFTfkhgrns6iLUneoRjO3hsqQaTaVkxVOQeLYWb+wA==} + + '@zag-js/radio-group@1.40.0': + resolution: {integrity: sha512-sFJCdyOKzQC9hylSP19R71yv44by/C78D9EHfsxQJtvOgDv9E+h13NNX4n9wWyubC20xftlxkja8sNT5NfJKUw==} + + '@zag-js/rating-group@1.40.0': + resolution: {integrity: sha512-UMBI3xAMcm7otpAczMGPEA7jC1hvV8NhnZ4mN3oftJB0bc1winoXxJdCkrXN58TTNWrGNSRzjtm048G+HPCdpw==} + + '@zag-js/react@1.40.0': + resolution: {integrity: sha512-2TFS1HYABYGc0lurC+4WEXvKkpxsVv6vKm+t8QAL7wfoeZnw6HDQWLc91kINp89vln+A2kwCfYqIq8HSm+9EeA==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@zag-js/rect-utils@1.40.0': + resolution: {integrity: sha512-ikgLuE4rLlACm4mGLp6Ga8sJA44uFwohA1nVmb95sQ+VIyx2naf91CEF7SMrZVEwFKHaHpxdKVQSZLRjJqO/dw==} + + '@zag-js/remove-scroll@1.40.0': + resolution: {integrity: sha512-f6EgODnJMRtkbgdJCgyllND8jui+RtPrCZy6JYhhOg7KQ+bFfV36KzWQMty38ZdOyrh23UUO7MJ3WGcFXPvk3g==} + + '@zag-js/scroll-area@1.40.0': + resolution: {integrity: sha512-7EtWETRIn8dY7xqAeMOlnEuzhOrtc65mN/0YvT3XYcBz/CzmHzyZTmos3UXBJGnKHSGj61aEpP9g3RK+x/w63A==} + + '@zag-js/scroll-snap@1.40.0': + resolution: {integrity: sha512-XtjeOd+pwGX0+K7NvsQncrKwV8CTSzHfVVJrdQ+MweiWBpGNeAh43ySN4L+KSTgtnUiZbuwBIxlKK0tX+WupgQ==} + + '@zag-js/select@1.40.0': + resolution: {integrity: sha512-auMI9SvocVvKHNWF2DobyQN6+1k3OO6UsQTdkofvbHxX7maosy8ZXA6k1r9Ndt4qLUu7CbdAAQ+qJ4VkgJyvxA==} + + '@zag-js/signature-pad@1.40.0': + resolution: {integrity: sha512-L0LTxcpdckaGdDDXcQCr4AG+J9xUHH+lsenH7NG4ZI7rSr4nRmHMdDH0GR7nBa6MMdPIIimjWIE/TwZ1OuHzCQ==} + + '@zag-js/slider@1.40.0': + resolution: {integrity: sha512-xZGycm+ghGFG3kTYq8g0t1Av1moxg45WiFz5E3bRgP7YU9beSTaFZI8h6f65NiC5P3YuwA0RoYxA46GH22qoZg==} + + '@zag-js/splitter@1.40.0': + resolution: {integrity: sha512-64KNKwlIjyUIjp7i/whDCpREiSFrNI/cF7MpBJvBGRPUWq8NpNxMGKWD+vBCV+JC61QF9xg/NgNoigFycS9sYw==} + + '@zag-js/steps@1.40.0': + resolution: {integrity: sha512-5sVFzcIYubCn1nJSQIx9WWNlJuFoOJMpkD/ZMwNp0LzpnmnspsCOmdnQUWEftMQ1KdwZ+qNgfo/+kHclb9cBjg==} + + '@zag-js/store@1.40.0': + resolution: {integrity: sha512-EmgYIdbNZ4TN4Qht/jugY4UVkaWx69l8P1qiX23U4YwqNLq10tyOJmcXWbvsrprU1dGb24B+xq0WBm/RIjw4WA==} + + '@zag-js/switch@1.40.0': + resolution: {integrity: sha512-hUH3AF79ndSFZxt7Plw7mVZV0QlM0kFqKwrAGBEOE77P3rKpOsMJ3wWgMb3w6nwlxGQsbwmMgAFvYUslLpM4Lg==} + + '@zag-js/tabs@1.40.0': + resolution: {integrity: sha512-xqfPC2nQ6Bn4nqy1L+1CVcQcg/Z7K2q753OvsX2C8Wtu+7tF//HyMbOpF6fGikqlLkUzCkvjkqDjdOXcfWN9ZQ==} + + '@zag-js/tags-input@1.40.0': + resolution: {integrity: sha512-3cB7nPlUvzZNZwQw5AaTuxwcRn1n2qkDCjLEb2NEwtmI+YxHbK3k1MtXjTccjcYjU8cAkv+jaeyZPs6KFKQcHA==} + + '@zag-js/timer@1.40.0': + resolution: {integrity: sha512-Rvet226fhUtZnItjHpUYV7MH0uEFZfXT9PSRrX5jdiU4/P0eWKbirwi//AVeqcWFexXvw6ajYSfQN7EVyr2x4w==} + + '@zag-js/toast@1.40.0': + resolution: {integrity: sha512-EDH43zdiH4Bz30cE6YI9g//qXGOOfWObM3dFLG8I0q/cJRf7/6jO82rwZAHPwfOSfKhUDxStirD8F6eoY6BWXA==} + + '@zag-js/toggle-group@1.40.0': + resolution: {integrity: sha512-+JKcnfEbdQnr5p7uRvYLdivhUsM6iio71UC10tK74nXYRnYm0/Uvxg3oQzvbNTq9WdcU/DIh3gZVZ2Vex9nBnQ==} + + '@zag-js/toggle@1.40.0': + resolution: {integrity: sha512-DW7682lzTP2eDlMvrS7tUX3zAm7ufrrKr7VDiX8BB6oXBRETXrVIxCYNuoIdqjwXebdjAoxaCiUZEreRVucYQg==} + + '@zag-js/tooltip@1.40.0': + resolution: {integrity: sha512-pyrvit+nB8dIwVNTGBRlHPsh7yMJGAxxM1zfY7HOTJqF+n6+6xYTQ4gQ/Ocy1Q7I5kO88+m16naEh0qLFiTZww==} + + '@zag-js/tour@1.40.0': + resolution: {integrity: sha512-VczYGFQM9xsSbfy5N0NP91GdKxbYvfPCDAguD+WQSs1umEIgAAozSKPUdV3NNCX5Pq6B1F3dBxi6gYPdNqrAHg==} + + '@zag-js/tree-view@1.40.0': + resolution: {integrity: sha512-v/20ekjbM+HXDEkpHAz6k8WpoZRmZmdCApDIkIgXVHPRQk+kwAiiIPY20ZDG+DjRu7Lh0MUdQavdZtGj6Ihwkw==} + + '@zag-js/types@1.40.0': + resolution: {integrity: sha512-LVvxEyqFv/u9SEe5xdivvG2vYb9cCmbkD+5r6s+IGljpDLaRgv4BYyxEh40ri1ai070tL08ZKmoLfx2/xfvY/A==} + + '@zag-js/utils@1.40.0': + resolution: {integrity: sha512-XUpqDtXfHe7CySjOhLPLj9H8rxbiFUJAGgmBzNdpsGPP4wx12cpOXrpSjRXZ2kMwooMPz/P7RPDBteto8sqhAQ==} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + 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'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + 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==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lucide-react@1.17.0: + resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + oxfmt@0.54.0: + resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==} + 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.69.0: + resolution: {integrity: sha512-ypZkK/aDc5NQV8zIR6s2H2Tl3aNW8FmJ1m9+2qsaYuRenl8vgnHNCGwTHviWJdUQzglOlHFchgopdtGhSy17Rw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.22.1' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + perfect-freehand@1.2.3: + resolution: {integrity: sha512-bHZSfqDHGNlPpgH2yxXgPHlQSPpEbo+qg7li0M78J9vNAi2yjwLeA4x79BEQhX44lEWpCLSFCeRZwpw0niiXPA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + proxy-compare@3.0.1: + resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} + + proxy-memoize@3.0.1: + resolution: {integrity: sha512-VDdG/VYtOgdGkWJx7y0o7p+zArSf2383Isci8C+BP3YXgMYDoPd3cCBjw0JdWb6YBb9sFiOPbAADDVTPJnh+9g==} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-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 + + rollup@4.61.1: + resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + 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'} + + 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'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uqr@0.1.2: + resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} + + vite@7.3.5: + resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.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 + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + +snapshots: + + '@ark-ui/react@5.36.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@internationalized/date': 3.12.0 + '@zag-js/accordion': 1.40.0 + '@zag-js/anatomy': 1.40.0 + '@zag-js/angle-slider': 1.40.0 + '@zag-js/async-list': 1.40.0 + '@zag-js/auto-resize': 1.40.0 + '@zag-js/avatar': 1.40.0 + '@zag-js/carousel': 1.40.0 + '@zag-js/cascade-select': 1.40.0 + '@zag-js/checkbox': 1.40.0 + '@zag-js/clipboard': 1.40.0 + '@zag-js/collapsible': 1.40.0 + '@zag-js/collection': 1.40.0 + '@zag-js/color-picker': 1.40.0 + '@zag-js/color-utils': 1.40.0 + '@zag-js/combobox': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/date-input': 1.40.0(@internationalized/date@3.12.0) + '@zag-js/date-picker': 1.40.0(@internationalized/date@3.12.0) + '@zag-js/date-utils': 1.40.0(@internationalized/date@3.12.0) + '@zag-js/dialog': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/drawer': 1.40.0 + '@zag-js/editable': 1.40.0 + '@zag-js/file-upload': 1.40.0 + '@zag-js/file-utils': 1.40.0 + '@zag-js/floating-panel': 1.40.0 + '@zag-js/focus-trap': 1.40.0 + '@zag-js/focus-visible': 1.40.0 + '@zag-js/highlight-word': 1.40.0 + '@zag-js/hover-card': 1.40.0 + '@zag-js/i18n-utils': 1.40.0 + '@zag-js/image-cropper': 1.40.0 + '@zag-js/json-tree-utils': 1.40.0 + '@zag-js/listbox': 1.40.0 + '@zag-js/marquee': 1.40.0 + '@zag-js/menu': 1.40.0 + '@zag-js/navigation-menu': 1.40.0 + '@zag-js/number-input': 1.40.0 + '@zag-js/pagination': 1.40.0 + '@zag-js/password-input': 1.40.0 + '@zag-js/pin-input': 1.40.0 + '@zag-js/popover': 1.40.0 + '@zag-js/presence': 1.40.0 + '@zag-js/progress': 1.40.0 + '@zag-js/qr-code': 1.40.0 + '@zag-js/radio-group': 1.40.0 + '@zag-js/rating-group': 1.40.0 + '@zag-js/react': 1.40.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@zag-js/scroll-area': 1.40.0 + '@zag-js/select': 1.40.0 + '@zag-js/signature-pad': 1.40.0 + '@zag-js/slider': 1.40.0 + '@zag-js/splitter': 1.40.0 + '@zag-js/steps': 1.40.0 + '@zag-js/switch': 1.40.0 + '@zag-js/tabs': 1.40.0 + '@zag-js/tags-input': 1.40.0 + '@zag-js/timer': 1.40.0 + '@zag-js/toast': 1.40.0 + '@zag-js/toggle': 1.40.0 + '@zag-js/toggle-group': 1.40.0 + '@zag-js/tooltip': 1.40.0 + '@zag-js/tour': 1.40.0 + '@zag-js/tree-view': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + 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/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-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-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@chakra-ui/react@3.35.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.36.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) + + '@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': {} + + '@internationalized/date@3.12.0': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/number@3.6.5': + 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/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 + + '@oxfmt/binding-android-arm-eabi@0.54.0': + optional: true + + '@oxfmt/binding-android-arm64@0.54.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.54.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.54.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.54.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.54.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.54.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.54.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.54.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.54.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.54.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.69.0': + optional: true + + '@oxlint/binding-android-arm64@1.69.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.69.0': + optional: true + + '@oxlint/binding-darwin-x64@1.69.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.69.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.69.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.69.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.69.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.69.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.69.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.69.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.69.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.69.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.69.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.69.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.69.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.69.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.69.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.69.0': + optional: true + + '@pandacss/is-valid-prop@1.11.3': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.61.1': + optional: true + + '@rollup/rollup-android-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-x64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.61.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.61.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.61.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.61.1': + optional: true + + '@swc/core-darwin-arm64@1.15.40': + optional: true + + '@swc/core-darwin-x64@1.15.40': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.40': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.40': + optional: true + + '@swc/core-linux-arm64-musl@1.15.40': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.40': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.40': + optional: true + + '@swc/core-linux-x64-gnu@1.15.40': + optional: true + + '@swc/core-linux-x64-musl@1.15.40': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.40': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.40': + optional: true + + '@swc/core-win32-x64-msvc@1.15.40': + optional: true + + '@swc/core@1.15.40(@swc/helpers@0.5.23)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.26 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.40 + '@swc/core-darwin-x64': 1.15.40 + '@swc/core-linux-arm-gnueabihf': 1.15.40 + '@swc/core-linux-arm64-gnu': 1.15.40 + '@swc/core-linux-arm64-musl': 1.15.40 + '@swc/core-linux-ppc64-gnu': 1.15.40 + '@swc/core-linux-s390x-gnu': 1.15.40 + '@swc/core-linux-x64-gnu': 1.15.40 + '@swc/core-linux-x64-musl': 1.15.40 + '@swc/core-win32-arm64-msvc': 1.15.40 + '@swc/core-win32-ia32-msvc': 1.15.40 + '@swc/core-win32-x64-msvc': 1.15.40 + '@swc/helpers': 0.5.23 + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@swc/types@0.1.26': + dependencies: + '@swc/counter': 0.1.3 + + '@types/estree@1.0.9': {} + + '@types/node@22.19.20': + dependencies: + undici-types: 6.21.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 + + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.23)(vite@7.3.5(@types/node@22.19.20))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.27 + '@swc/core': 1.15.40(@swc/helpers@0.5.23) + vite: 7.3.5(@types/node@22.19.20) + transitivePeerDependencies: + - '@swc/helpers' + + '@zag-js/accordion@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/anatomy@1.40.0': {} + + '@zag-js/angle-slider@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/rect-utils': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/aria-hidden@1.40.0': + dependencies: + '@zag-js/dom-query': 1.40.0 + + '@zag-js/async-list@1.40.0': + dependencies: + '@zag-js/core': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/auto-resize@1.40.0': + dependencies: + '@zag-js/dom-query': 1.40.0 + + '@zag-js/avatar@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/carousel@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/scroll-snap': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/cascade-select@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/collection': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-visible': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/rect-utils': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/checkbox@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-visible': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/clipboard@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/collapsible@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/collection@1.40.0': + dependencies: + '@zag-js/utils': 1.40.0 + + '@zag-js/color-picker@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/color-utils': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/color-utils@1.40.0': + dependencies: + '@zag-js/utils': 1.40.0 + + '@zag-js/combobox@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/collection': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-visible': 1.40.0 + '@zag-js/live-region': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/core@1.40.0': + dependencies: + '@zag-js/dom-query': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/date-input@1.40.0(@internationalized/date@3.12.0)': + dependencies: + '@internationalized/date': 3.12.0 + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/date-utils': 1.40.0(@internationalized/date@3.12.0) + '@zag-js/dom-query': 1.40.0 + '@zag-js/live-region': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/date-picker@1.40.0(@internationalized/date@3.12.0)': + dependencies: + '@internationalized/date': 3.12.0 + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/date-utils': 1.40.0(@internationalized/date@3.12.0) + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/live-region': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/date-utils@1.40.0(@internationalized/date@3.12.0)': + dependencies: + '@internationalized/date': 3.12.0 + + '@zag-js/dialog@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/aria-hidden': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-trap': 1.40.0 + '@zag-js/remove-scroll': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/dismissable@1.40.0': + dependencies: + '@zag-js/dom-query': 1.40.0 + '@zag-js/interact-outside': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/dom-query@1.40.0': + dependencies: + '@zag-js/types': 1.40.0 + + '@zag-js/drawer@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/aria-hidden': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-trap': 1.40.0 + '@zag-js/remove-scroll': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/editable@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/interact-outside': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/file-upload@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/file-utils': 1.40.0 + '@zag-js/i18n-utils': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/file-utils@1.40.0': + dependencies: + '@zag-js/i18n-utils': 1.40.0 + + '@zag-js/floating-panel@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/rect-utils': 1.40.0 + '@zag-js/store': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/focus-trap@1.40.0': + dependencies: + '@zag-js/dom-query': 1.40.0 + + '@zag-js/focus-visible@1.40.0': + dependencies: + '@zag-js/dom-query': 1.40.0 + + '@zag-js/highlight-word@1.40.0': {} + + '@zag-js/hover-card@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/i18n-utils@1.40.0': + dependencies: + '@zag-js/dom-query': 1.40.0 + + '@zag-js/image-cropper@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/interact-outside@1.40.0': + dependencies: + '@zag-js/dom-query': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/json-tree-utils@1.40.0': {} + + '@zag-js/listbox@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/collection': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-visible': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/live-region@1.40.0': {} + + '@zag-js/marquee@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/menu@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-visible': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/rect-utils': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/navigation-menu@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/number-input@1.40.0': + dependencies: + '@internationalized/number': 3.6.5 + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/pagination@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/password-input@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/pin-input@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/popover@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/aria-hidden': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-trap': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/remove-scroll': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/popper@1.40.0': + dependencies: + '@floating-ui/dom': 1.7.6 + '@zag-js/dom-query': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/presence@1.40.0': + dependencies: + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + + '@zag-js/progress@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/qr-code@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + proxy-memoize: 3.0.1 + uqr: 0.1.2 + + '@zag-js/radio-group@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-visible': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/rating-group@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/react@1.40.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@zag-js/core': 1.40.0 + '@zag-js/store': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@zag-js/rect-utils@1.40.0': {} + + '@zag-js/remove-scroll@1.40.0': + dependencies: + '@zag-js/dom-query': 1.40.0 + + '@zag-js/scroll-area@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/scroll-snap@1.40.0': + dependencies: + '@zag-js/dom-query': 1.40.0 + + '@zag-js/select@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/collection': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-visible': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/signature-pad@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + perfect-freehand: 1.2.3 + + '@zag-js/slider@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/splitter@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/steps@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/store@1.40.0': + dependencies: + proxy-compare: 3.0.1 + + '@zag-js/switch@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-visible': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/tabs@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/tags-input@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/auto-resize': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/interact-outside': 1.40.0 + '@zag-js/live-region': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/timer@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/toast@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/toggle-group@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/toggle@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/tooltip@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-visible': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/tour@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dismissable': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/focus-trap': 1.40.0 + '@zag-js/interact-outside': 1.40.0 + '@zag-js/popper': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/tree-view@1.40.0': + dependencies: + '@zag-js/anatomy': 1.40.0 + '@zag-js/collection': 1.40.0 + '@zag-js/core': 1.40.0 + '@zag-js/dom-query': 1.40.0 + '@zag-js/types': 1.40.0 + '@zag-js/utils': 1.40.0 + + '@zag-js/types@1.40.0': + dependencies: + csstype: 3.2.3 + + '@zag-js/utils@1.40.0': {} + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.7 + cosmiconfig: 7.1.0 + resolve: 1.22.12 + + callsites@3.1.0: {} + + convert-source-map@1.9.0: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-errors@1.3.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 + + escape-string-regexp@4.0.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + find-root@1.1.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + is-arrayish@0.2.1: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + lines-and-columns@1.2.4: {} + + lucide-react@1.17.0(react@19.2.7): + dependencies: + react: 19.2.7 + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + oxfmt@0.54.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.54.0 + '@oxfmt/binding-android-arm64': 0.54.0 + '@oxfmt/binding-darwin-arm64': 0.54.0 + '@oxfmt/binding-darwin-x64': 0.54.0 + '@oxfmt/binding-freebsd-x64': 0.54.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.54.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.54.0 + '@oxfmt/binding-linux-arm64-gnu': 0.54.0 + '@oxfmt/binding-linux-arm64-musl': 0.54.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.54.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.54.0 + '@oxfmt/binding-linux-riscv64-musl': 0.54.0 + '@oxfmt/binding-linux-s390x-gnu': 0.54.0 + '@oxfmt/binding-linux-x64-gnu': 0.54.0 + '@oxfmt/binding-linux-x64-musl': 0.54.0 + '@oxfmt/binding-openharmony-arm64': 0.54.0 + '@oxfmt/binding-win32-arm64-msvc': 0.54.0 + '@oxfmt/binding-win32-ia32-msvc': 0.54.0 + '@oxfmt/binding-win32-x64-msvc': 0.54.0 + + oxlint@1.69.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.69.0 + '@oxlint/binding-android-arm64': 1.69.0 + '@oxlint/binding-darwin-arm64': 1.69.0 + '@oxlint/binding-darwin-x64': 1.69.0 + '@oxlint/binding-freebsd-x64': 1.69.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.69.0 + '@oxlint/binding-linux-arm-musleabihf': 1.69.0 + '@oxlint/binding-linux-arm64-gnu': 1.69.0 + '@oxlint/binding-linux-arm64-musl': 1.69.0 + '@oxlint/binding-linux-ppc64-gnu': 1.69.0 + '@oxlint/binding-linux-riscv64-gnu': 1.69.0 + '@oxlint/binding-linux-riscv64-musl': 1.69.0 + '@oxlint/binding-linux-s390x-gnu': 1.69.0 + '@oxlint/binding-linux-x64-gnu': 1.69.0 + '@oxlint/binding-linux-x64-musl': 1.69.0 + '@oxlint/binding-openharmony-arm64': 1.69.0 + '@oxlint/binding-win32-arm64-msvc': 1.69.0 + '@oxlint/binding-win32-ia32-msvc': 1.69.0 + '@oxlint/binding-win32-x64-msvc': 1.69.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: {} + + perfect-freehand@1.2.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + 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-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 + + rollup@4.61.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.1 + '@rollup/rollup-android-arm64': 4.61.1 + '@rollup/rollup-darwin-arm64': 4.61.1 + '@rollup/rollup-darwin-x64': 4.61.1 + '@rollup/rollup-freebsd-arm64': 4.61.1 + '@rollup/rollup-freebsd-x64': 4.61.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.1 + '@rollup/rollup-linux-arm-musleabihf': 4.61.1 + '@rollup/rollup-linux-arm64-gnu': 4.61.1 + '@rollup/rollup-linux-arm64-musl': 4.61.1 + '@rollup/rollup-linux-loong64-gnu': 4.61.1 + '@rollup/rollup-linux-loong64-musl': 4.61.1 + '@rollup/rollup-linux-ppc64-gnu': 4.61.1 + '@rollup/rollup-linux-ppc64-musl': 4.61.1 + '@rollup/rollup-linux-riscv64-gnu': 4.61.1 + '@rollup/rollup-linux-riscv64-musl': 4.61.1 + '@rollup/rollup-linux-s390x-gnu': 4.61.1 + '@rollup/rollup-linux-x64-gnu': 4.61.1 + '@rollup/rollup-linux-x64-musl': 4.61.1 + '@rollup/rollup-openbsd-x64': 4.61.1 + '@rollup/rollup-openharmony-arm64': 4.61.1 + '@rollup/rollup-win32-arm64-msvc': 4.61.1 + '@rollup/rollup-win32-ia32-msvc': 4.61.1 + '@rollup/rollup-win32-x64-gnu': 4.61.1 + '@rollup/rollup-win32-x64-msvc': 4.61.1 + fsevents: 2.3.3 + + scheduler@0.27.0: {} + + source-map-js@1.2.1: {} + + source-map@0.5.7: {} + + stylis@4.2.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@2.1.0: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + uqr@0.1.2: {} + + vite@7.3.5(@types/node@22.19.20): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.61.1 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.19.20 + fsevents: 2.3.3 + + yaml@1.10.3: {} diff --git a/invokeai/frontend/webv2/src/App.tsx b/invokeai/frontend/webv2/src/App.tsx new file mode 100644 index 00000000000..2344acb33be --- /dev/null +++ b/invokeai/frontend/webv2/src/App.tsx @@ -0,0 +1,13 @@ +import { ChakraProvider } from '@chakra-ui/react'; + +import { system } from './theme/system'; +import { WorkbenchProvider } from './workbench/WorkbenchContext'; +import { WorkbenchShell } from './workbench/WorkbenchShell'; + +export const App = () => ( + + + + + +); 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..009c0555968 --- /dev/null +++ b/invokeai/frontend/webv2/src/main.tsx @@ -0,0 +1,16 @@ +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/theme/system.ts b/invokeai/frontend/webv2/src/theme/system.ts new file mode 100644 index 00000000000..8b9c3050a24 --- /dev/null +++ b/invokeai/frontend/webv2/src/theme/system.ts @@ -0,0 +1,92 @@ +import { createSystem, defaultConfig, defineConfig } from '@chakra-ui/react'; + +/** + * Workbench design tokens. + * + * The v7 workbench is a dark-only creative surface (Photoshop / Blender / DaVinci + * feel), so the palette is expressed as concrete tokens and surfaced through + * semantic tokens with single values. Components reference the semantic tokens + * (`bg.shell`, `fg.muted`, `accent.invoke`, …) rather than raw hex, which keeps + * the shell theme-able and removes the brittle hard-coded colors the prototype + * relied on. + */ +const config = defineConfig({ + globalCss: { + 'html, body, #root': { + height: '100%', + }, + body: { + margin: 0, + minWidth: '960px', + minHeight: '720px', + overflow: 'hidden', + bg: 'bg.shell', + color: 'fg.default', + fontFamily: 'body', + }, + }, + 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", + }, + }, + colors: { + workbench: { + shell: { value: '#141514' }, + surface: { value: '#161716' }, + surfaceRaised: { value: '#181918' }, + center: { value: '#1a1b1a' }, + canvas: { value: '#1c1d1c' }, + panel: { value: '#282928' }, + panelStroke: { value: '#303330' }, + line: { value: '#2e312d' }, + lineStrong: { value: '#343832' }, + dot: { value: '#343734' }, + fg: { value: '#d7dec7' }, + fgMuted: { value: '#a9afa1' }, + fgSubtle: { value: '#6f756a' }, + }, + lime: { + accent: { value: '#cbff63' }, + contrast: { value: '#10160b' }, + mutedBg: { value: '#33372a' }, + mutedFg: { value: '#dfff8c' }, + }, + sky: { + accent: { value: '#59cfff' }, + contrast: { value: '#081218' }, + }, + }, + }, + semanticTokens: { + colors: { + 'bg.shell': { value: '{colors.workbench.shell}' }, + 'bg.surface': { value: '{colors.workbench.surface}' }, + 'bg.surfaceRaised': { value: '{colors.workbench.surfaceRaised}' }, + 'bg.center': { value: '{colors.workbench.center}' }, + 'bg.canvas': { value: '{colors.workbench.canvas}' }, + 'bg.panel': { value: '{colors.workbench.panel}' }, + 'border.subtle': { value: '{colors.workbench.line}' }, + 'border.emphasis': { value: '{colors.workbench.lineStrong}' }, + 'border.panel': { value: '{colors.workbench.panelStroke}' }, + 'fg.default': { value: '{colors.workbench.fg}' }, + 'fg.muted': { value: '{colors.workbench.fgMuted}' }, + 'fg.subtle': { value: '{colors.workbench.fgSubtle}' }, + 'accent.invoke': { value: '{colors.lime.accent}' }, + 'accent.invokeFg': { value: '{colors.lime.contrast}' }, + 'accent.widget': { value: '{colors.lime.mutedBg}' }, + 'accent.widgetFg': { value: '{colors.lime.mutedFg}' }, + 'accent.active': { value: '{colors.sky.accent}' }, + 'accent.activeFg': { value: '{colors.sky.contrast}' }, + 'canvas.dot': { value: '{colors.workbench.dot}' }, + }, + }, + }, +}); + +export const system = createSystem(defaultConfig, config); diff --git a/invokeai/frontend/webv2/src/workbench/WorkbenchContext.tsx b/invokeai/frontend/webv2/src/workbench/WorkbenchContext.tsx new file mode 100644 index 00000000000..600336bf00c --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/WorkbenchContext.tsx @@ -0,0 +1,126 @@ +import { createContext, use, useEffect, useMemo, useReducer, useRef, type Dispatch, type ReactNode } from 'react'; + +import { localStorageWorkbenchPersistence } from './persistence'; +import type { Project, WorkbenchState } from './types'; +import { createInitialWorkbenchState, workbenchReducer, type WorkbenchAction } from './workbenchState'; + +interface WorkbenchContextValue { + state: WorkbenchState; + activeProject: Project; + dispatch: Dispatch; +} + +const WorkbenchContext = createContext(null); + +const AUTOSAVE_DELAY_MS = 500; + +const getPersistedStateKey = (state: WorkbenchState): string => + JSON.stringify({ + account: state.account, + activeProjectId: state.activeProjectId, + errorLog: state.errorLog, + projects: state.projects, + widgetFailures: state.widgetFailures, + }); + +export const WorkbenchProvider = ({ children }: { children: ReactNode }) => { + const [state, dispatch] = useReducer(workbenchReducer, undefined, createInitialWorkbenchState); + const hasLoadedPersistenceRef = useRef(false); + const latestStateRef = useRef(state); + const lastSavedStateKeyRef = useRef(getPersistedStateKey(state)); + const persistedStateKey = getPersistedStateKey(state); + + latestStateRef.current = state; + + useEffect(() => { + let isCancelled = false; + + const loadPersistedState = async () => { + try { + const snapshot = await localStorageWorkbenchPersistence.loadWorkbench(); + + if (isCancelled) { + return; + } + + if (snapshot) { + lastSavedStateKeyRef.current = getPersistedStateKey(snapshot.state); + dispatch({ state: snapshot.state, type: 'hydrateWorkbench' }); + } + } catch (error) { + dispatch({ + message: error instanceof Error ? error.message : 'Failed to load persisted workbench.', + type: 'recordError', + }); + } finally { + hasLoadedPersistenceRef.current = true; + } + }; + + void loadPersistedState(); + + return () => { + isCancelled = true; + }; + }, []); + + useEffect(() => { + if (!hasLoadedPersistenceRef.current) { + return undefined; + } + + if (persistedStateKey === lastSavedStateKeyRef.current) { + return undefined; + } + + let isStale = false; + + dispatch({ type: 'autosaveStarted' }); + + const timeoutId = window.setTimeout(() => { + localStorageWorkbenchPersistence + .saveWorkbench(latestStateRef.current) + .then((snapshot) => { + if (isStale) { + return; + } + + lastSavedStateKeyRef.current = persistedStateKey; + dispatch({ savedAt: snapshot.savedAt, type: 'autosaveSucceeded' }); + }) + .catch((error: unknown) => { + if (isStale) { + return; + } + + dispatch({ + error: error instanceof Error ? error.message : 'Failed to autosave workbench.', + type: 'autosaveFailed', + }); + }); + }, AUTOSAVE_DELAY_MS); + + return () => { + isStale = true; + window.clearTimeout(timeoutId); + }; + }, [persistedStateKey]); + + const value = useMemo(() => { + const activeProject = state.projects.find((project) => project.id === state.activeProjectId) ?? state.projects[0]; + + return { state, activeProject, dispatch }; + }, [state]); + + return {children}; +}; + +export const useWorkbench = (): WorkbenchContextValue => { + const context = use(WorkbenchContext); + + if (!context) { + throw new Error('useWorkbench must be used within a WorkbenchProvider.'); + } + + return context; +}; diff --git a/invokeai/frontend/webv2/src/workbench/WorkbenchShell.tsx b/invokeai/frontend/webv2/src/workbench/WorkbenchShell.tsx new file mode 100644 index 00000000000..fd0d575522b --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/WorkbenchShell.tsx @@ -0,0 +1,89 @@ +import { Flex } from '@chakra-ui/react'; +import { useEffect } from 'react'; + +import { BottomPanel } from './components/BottomPanel'; +import { CenterArea } from './components/CenterArea'; +import { LeftPanel, RightPanel } from './components/Panels'; +import { ShellErrorSurface } from './components/ShellErrorSurface'; +import { StatusBar } from './components/StatusBar'; +import { TopBar } from './components/TopBar'; +import { WidgetBar, type WidgetBarItem } from './components/WidgetBar'; +import { getWidgetsForRegion, widgetRegistrationFailures } from './widgetRegistry'; +import { useWorkbench } from './WorkbenchContext'; +import type { WidgetId, WidgetRegion } from './types'; + +const getWidgetBarItems = (region: WidgetRegion, enabledWidgetIds: WidgetId[]): WidgetBarItem[] => + getWidgetsForRegion(region).map((widget) => ({ + failureMessage: widget.failure?.message, + id: widget.manifest.id, + iconId: widget.manifest.icon, + isEnabled: enabledWidgetIds.includes(widget.manifest.id), + label: widget.manifest.labelText, + status: widget.status, + })); + +/** + * Top-level v7 workbench shell (Phase 1 skeleton). + * + * Composes the fixed Invoke control + project tabs, the left/right widget bars, + * project-owned side panels, the center work area, the status bar, and the + * copyable error surface. Layout regions are flex children that mount/unmount + * from project layout state, which is more robust than the prototype's CSS grid + * whose column template had to stay in lockstep with conditional children. + */ +export const WorkbenchShell = () => { + const { activeProject, dispatch } = useWorkbench(); + const { panels } = activeProject.layout; + const leftRegion = activeProject.widgetRegions.left; + const rightRegion = activeProject.widgetRegions.right; + const leftMenuItems = getWidgetBarItems('left', leftRegion.enabledWidgetIds); + const rightMenuItems = getWidgetBarItems('right', rightRegion.enabledWidgetIds); + const leftRailItems = leftMenuItems.filter((item) => item.isEnabled && item.status !== 'disabled'); + const rightRailItems = rightMenuItems.filter((item) => item.isEnabled && item.status !== 'disabled'); + const canShowLeftPanel = leftRailItems.some((item) => item.id === leftRegion.activeWidgetId); + const canShowRightPanel = rightRailItems.some((item) => item.id === rightRegion.activeWidgetId); + + useEffect(() => { + for (const failure of widgetRegistrationFailures) { + dispatch({ failure, type: 'recordWidgetFailure' }); + } + }, [dispatch]); + + return ( + + + + + dispatch({ region: 'left', type: 'selectRegionWidget', widgetId })} + onToggle={(widgetId) => dispatch({ region: 'left', type: 'toggleRegionWidget', widgetId })} + /> + {panels.isLeftOpen && !leftRegion.isCollapsed && canShowLeftPanel ? ( + + ) : null} + + {panels.isRightOpen && !rightRegion.isCollapsed && canShowRightPanel ? ( + + ) : null} + dispatch({ region: 'right', type: 'selectRegionWidget', widgetId })} + onToggle={(widgetId) => dispatch({ region: 'right', type: 'toggleRegionWidget', widgetId })} + /> + + + + + + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/components/BottomPanel.tsx b/invokeai/frontend/webv2/src/workbench/components/BottomPanel.tsx new file mode 100644 index 00000000000..8f8aa994dee --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/BottomPanel.tsx @@ -0,0 +1,40 @@ +import { Text } from '@chakra-ui/react'; + +import { useWorkbench } from '../WorkbenchContext'; +import { getWidgetById } from '../widgetRegistry'; +import { WidgetPanelFrame } from './WidgetFrames'; +import { WidgetRenderer } from './WidgetRenderer'; + +export const BottomPanel = () => { + const { activeProject } = useWorkbench(); + const { panels } = activeProject.layout; + const bottomRegion = activeProject.widgetRegions.bottom; + const widget = getWidgetById(bottomRegion.activeWidgetId); + const View = widget?.manifest.view; + const canShowBottomPanel = + panels.isBottomOpen && + !bottomRegion.isCollapsed && + bottomRegion.enabledWidgetIds.includes(bottomRegion.activeWidgetId) && + widget?.status === 'enabled' && + widget.manifest.bottomPanel !== 'tooltip'; + + if (!canShowBottomPanel) { + return null; + } + + if (!widget || !View) { + return ( + + + Widget view unavailable. + + + ); + } + + return ( + + + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/components/CenterArea.tsx b/invokeai/frontend/webv2/src/workbench/components/CenterArea.tsx new file mode 100644 index 00000000000..1b65f1c1c2b --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/CenterArea.tsx @@ -0,0 +1,170 @@ +import { Box, Flex, HStack, Icon, Menu, Portal, Tabs, Text } from '@chakra-ui/react'; +import { PiCheckBold, PiDotsThreeBold } from 'react-icons/pi'; + +import { WidgetIcon } from '../iconResolver'; +import { useWorkbench } from '../WorkbenchContext'; +import type { CenterViewId, RegisteredWidget } from '../types'; +import { getWidgetsForRegion } from '../widgetRegistry'; +import { WidgetRenderer } from './WidgetRenderer'; + +interface CenterWidgetItem { + id: Exclude; + isEnabled: boolean; + label: string; + status: RegisteredWidget['status']; + widget: RegisteredWidget; +} + +const getCenterWidgetItems = (enabledWidgetIds: string[]): CenterWidgetItem[] => + getWidgetsForRegion('center').map((widget) => ({ + id: widget.manifest.id as Exclude, + isEnabled: enabledWidgetIds.includes(widget.manifest.id), + label: widget.manifest.labelText, + status: widget.status, + widget, + })); + +/** Center work area: the view tab strip plus the active registered center view. */ +export const CenterArea = () => { + const { activeProject, dispatch } = useWorkbench(); + const centerRegion = activeProject.widgetRegions.center; + const centerWidgetItems = getCenterWidgetItems(centerRegion.enabledWidgetIds); + const enabledCenterWidgetItems = centerWidgetItems.filter((item) => item.isEnabled && item.status === 'enabled'); + + return ( + + + + dispatch({ region: 'center', type: 'selectRegionWidget', widgetId: event.value as CenterWidgetItem['id'] }) + } + > + + {enabledCenterWidgetItems.map((item) => ( + + + {item.label} + + ))} + + + + + dispatch({ region: 'center', type: 'toggleRegionWidget', widgetId: centerWidgetId }) + } + /> + + + + + + + ); +}; + +const CenterViewSlot = ({ activeWidgetId, items }: { activeWidgetId: string; items: CenterWidgetItem[] }) => { + const widget = items.find((item) => item.id === activeWidgetId)?.widget; + const View = widget?.manifest.view; + + if (!widget || widget.status !== 'enabled' || !View) { + return ; + } + + return ; +}; + +const CenterWidgetMenu = ({ + enabledCount, + items, + onToggle, +}: { + enabledCount: number; + items: CenterWidgetItem[]; + onToggle: (centerWidgetId: CenterWidgetItem['id']) => void; +}) => ( + + + + + + + + + + + + Center Widgets + + {items.map((item) => ( + onToggle(item.id)} + > + + + {item.label} + {item.isEnabled && enabledCount === 1 ? ( + + Required + + ) : null} + + ))} + + + + + +); + +const FallbackCenterView = ({ label }: { label: string }) => ( + + + {label} view + + +); diff --git a/invokeai/frontend/webv2/src/workbench/components/GraphPreviewDialog.tsx b/invokeai/frontend/webv2/src/workbench/components/GraphPreviewDialog.tsx new file mode 100644 index 00000000000..2a78ef5cc5c --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/GraphPreviewDialog.tsx @@ -0,0 +1,82 @@ +import { Button, Code, Dialog, Portal, Stack, Text } from '@chakra-ui/react'; + +import { formatRoute, isInvocationRouteValid, resolveInvocationRoute } from '../invocation'; +import type { GraphContract, GraphId, InvocationSourceId } from '../types'; +import { useWorkbench } from '../WorkbenchContext'; + +interface GraphPreviewDialogProps { + graph: GraphContract | null; + graphId: GraphId; + isOpen: boolean; + sourceId?: InvocationSourceId; + title: string; + onOpenChange: (isOpen: boolean) => void; +} + +export const GraphPreviewDialog = ({ + graph, + graphId, + isOpen, + sourceId, + title, + onOpenChange, +}: GraphPreviewDialogProps) => { + const { activeProject, dispatch } = useWorkbench(); + const dialogRoute = sourceId + ? resolveInvocationRoute(activeProject, 'dialog', { ...activeProject.invocation, sourceId, sourceLocked: true }) + : null; + const canInvoke = dialogRoute ? isInvocationRouteValid(dialogRoute) : false; + + return ( + onOpenChange(event.open)}> + + + + + + {title} Graph Preview + + + + + Read-only shell for graph inspection. Full graph preview rendering lands in later phases. + + + {JSON.stringify(graph ?? { id: graphId, status: 'not-created-yet' }, null, 2)} + + + + + {dialogRoute ? ( + + ) : null} + + + + + + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/components/GraphSurfaceActions.tsx b/invokeai/frontend/webv2/src/workbench/components/GraphSurfaceActions.tsx new file mode 100644 index 00000000000..f709315886b --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/GraphSurfaceActions.tsx @@ -0,0 +1,84 @@ +import { Icon, IconButton, Menu, Portal, Text } from '@chakra-ui/react'; +import { useState } from 'react'; +import { PiDotsThreeBold, PiGraphBold, PiTargetBold } from 'react-icons/pi'; + +import { useWorkbench } from '../WorkbenchContext'; +import type { GraphBearingSurfaceContract, GraphContract, WidgetId } from '../types'; +import { GraphPreviewDialog } from './GraphPreviewDialog'; + +interface GraphSurfaceActionsProps { + surface: GraphBearingSurfaceContract; +} + +const getPreviewGraph = ( + widgetGraphs: Partial>, + surface: GraphBearingSurfaceContract +) => widgetGraphs[surface.widgetId] ?? null; + +export const GraphSurfaceActions = ({ surface }: GraphSurfaceActionsProps) => { + const { activeProject, dispatch } = useWorkbench(); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); + const isActiveSource = activeProject.invocation.sourceId === surface.sourceId; + const previewGraph = getPreviewGraph(activeProject.widgetGraphs, surface); + + return ( + <> + + + + + + + + + + + + Graph + + dispatch({ sourceId: surface.sourceId, type: 'setInvocationSource' })} + > + + Set Source + {isActiveSource ? ( + + Active + + ) : null} + + setIsPreviewOpen(true)} + > + + View Graph + + + + + + + + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/components/InvokeControl.tsx b/invokeai/frontend/webv2/src/workbench/components/InvokeControl.tsx new file mode 100644 index 00000000000..39f67a6c1e1 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/InvokeControl.tsx @@ -0,0 +1,216 @@ +import { Box, HStack, Icon, Menu, Portal, Text, VStack } from '@chakra-ui/react'; +import { useEffect, useRef } from 'react'; +import { PiCaretDownBold, PiCheckBold, PiLockSimpleFill, PiLightningFill } from 'react-icons/pi'; + +import { useWorkbench } from '../WorkbenchContext'; +import { + formatRoute, + invocationSources, + isInvocationRouteValid, + resolveInvocationRoute, + resultDestinations, +} from '../invocation'; +import type { InvocationSourceId, ResultDestination } from '../types'; + +/** + * Fixed-width global Invoke control. + * + * The defining Phase 1 requirement: the primary label stays `Invoke`, the + * secondary line shows the resolved `Source → Destination` route with a compact + * lock indicator, and the control reserves stable horizontal space so project + * tabs never shift when the route text changes. The caret opens the Invocation + * Controller menu (source / destination / lock), which is wired to project-owned + * state even though full invocation lands in Phase 4. + */ +const CONTROL_WIDTH = '13.5rem'; + +export const InvokeControl = () => { + const { activeProject, dispatch } = useWorkbench(); + const { invocation } = activeProject; + const resolvedRoute = resolveInvocationRoute(activeProject); + const isLocked = invocation.sourceLocked || invocation.destinationLocked; + const isValid = isInvocationRouteValid(resolvedRoute); + const routeLabel = formatRoute(resolvedRoute); + const invokeLabel = isValid ? `Invoke — ${routeLabel}` : `Invoke unavailable — ${resolvedRoute.validationMessage}`; + const resolvedRouteRef = useRef(resolvedRoute); + const isValidRef = useRef(isValid); + + resolvedRouteRef.current = resolvedRoute; + isValidRef.current = isValid; + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if (!(event.metaKey || event.ctrlKey) || event.key !== 'Enter') { + return; + } + + event.preventDefault(); + + if (!isValidRef.current) { + return; + } + + dispatch({ + backendSupportsCancellation: true, + route: resolvedRouteRef.current, + type: 'submitResolvedInvocationSnapshot', + }); + }; + + window.addEventListener('keydown', onKeyDown); + + return () => { + window.removeEventListener('keydown', onKeyDown); + }; + }, [dispatch]); + + return ( + + { + if (!isValid) { + return; + } + + dispatch({ + backendSupportsCancellation: true, + route: resolvedRoute, + type: 'submitResolvedInvocationSnapshot', + }); + }} + _hover={isValid ? { filter: 'brightness(1.05)' } : undefined} + _active={{ filter: 'brightness(0.96)' }} + > + + + + Invoke + + + + {routeLabel} + + {isLocked ? : null} + + + + + + + + + + + + + + + dispatch({ sourceId: event.value as InvocationSourceId, type: 'setInvocationSource' }) + } + > + + Source + + {invocationSources.map((source) => ( + + {source.label} + {source.available ? null : ( + + Soon + + )} + + + + + ))} + + + + + + dispatch({ destination: event.value as ResultDestination, type: 'setInvocationDestination' }) + } + > + + Destination + + {resultDestinations.map((destination) => ( + + {destination.label} + + + + + ))} + + + + + dispatch({ type: 'toggleSourceLock' })} + > + + {invocation.sourceLocked ? 'Unlock source' : 'Lock source'} + + dispatch({ type: 'toggleDestinationLock' })} + > + + + {invocation.destinationLocked ? 'Unlock destination' : 'Lock destination'} + + + + + + + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/components/LayoutPresetMenu.tsx b/invokeai/frontend/webv2/src/workbench/components/LayoutPresetMenu.tsx new file mode 100644 index 00000000000..f00cf26468a --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/LayoutPresetMenu.tsx @@ -0,0 +1,78 @@ +import { Button, Icon, Menu, Portal, Stack, Text } from '@chakra-ui/react'; +import { PiCaretDownBold, PiCheckBold } from 'react-icons/pi'; + +import { useWorkbench } from '../WorkbenchContext'; +import { getLayoutPreset, layoutPresets } from '../layoutPresets'; +import type { LayoutPresetId } from '../types'; + +/** + * Global layout preset registry surfaced as a menu. + * + * Replaces the prototype's hand-rolled absolutely-positioned popover (and its + * open/close `useState`) with a Chakra `Menu`, which handles focus trapping, + * outside-click dismissal, and keyboard navigation. Presets are global; applying + * one mutates only the active project's layout state, per the spec. + */ +export const LayoutPresetMenu = () => { + const { activeProject, dispatch } = useWorkbench(); + const activePreset = getLayoutPreset(activeProject.layout.presetId); + + return ( + + + + + + + + + + Layout presets + + {layoutPresets.map((preset) => ( + dispatch({ presetId: preset.id as LayoutPresetId, type: 'applyPreset' })} + > + + + {preset.label} + + + {preset.description} + + + {preset.id === activePreset.id ? : null} + + ))} + + + + + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/components/Panels.tsx b/invokeai/frontend/webv2/src/workbench/components/Panels.tsx new file mode 100644 index 00000000000..cc7e6f5a0d5 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/Panels.tsx @@ -0,0 +1,51 @@ +import { Stack, Text } from '@chakra-ui/react'; + +import type { WidgetId, WorkbenchRegion } from '../types'; +import { getWidgetById } from '../widgetRegistry'; +import { WidgetRenderer } from './WidgetRenderer'; + +/** Left panel — hosts the active registered widget panel view. */ +export const LeftPanel = ({ widgetId }: { widgetId: WidgetId }) => ( + +); + +/** Right panel — hosts the active registered widget panel view. */ +export const RightPanel = ({ widgetId }: { widgetId: WidgetId }) => ( + +); + +const panelRegions = { + leftPanel: 'left', + rightPanel: 'right', +} as const satisfies Record; + +const WidgetPanelSlot = ({ widgetId, panel }: { widgetId: WidgetId; panel: keyof typeof panelRegions }) => { + const widget = getWidgetById(widgetId); + const View = widget?.manifest.view; + const region = panelRegions[panel]; + + if (!widget || widget.status !== 'enabled' || !View) { + return ; + } + + return ; +}; + +const MissingWidgetPanel = ({ label }: { label: string }) => ( + + + {label} + + Widget view unavailable. + +); diff --git a/invokeai/frontend/webv2/src/workbench/components/ProjectTabs.tsx b/invokeai/frontend/webv2/src/workbench/components/ProjectTabs.tsx new file mode 100644 index 00000000000..42a68583586 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/ProjectTabs.tsx @@ -0,0 +1,95 @@ +import { Box, Flex, HStack, Icon, IconButton } from '@chakra-ui/react'; +import { PiPlusBold, PiXBold } from 'react-icons/pi'; + +import { useWorkbench } from '../WorkbenchContext'; +import type { Project } from '../types'; + +/** + * Document-style project tabs, immediately right of the Invoke control. + * + * Each tab is a container with two real buttons (select + close) rather than the + * prototype's invalid button-nested-in-button, so keyboard focus and click + * targets behave correctly. + */ +export const ProjectTabs = () => { + const { state, activeProject, dispatch } = useWorkbench(); + + return ( + + {state.projects.map((project) => ( + 1} + onSelect={() => dispatch({ projectId: project.id, type: 'switchProject' })} + onClose={() => dispatch({ projectId: project.id, type: 'closeProject' })} + /> + ))} + dispatch({ type: 'createProject' })} + > + + + + ); +}; + +interface ProjectTabProps { + project: Project; + isActive: boolean; + canClose: boolean; + onSelect: () => void; + onClose: () => void; +} + +const ProjectTab = ({ project, isActive, canClose, onSelect, onClose }: ProjectTabProps) => ( + + + {project.name} + + {canClose ? ( + + + + ) : null} + +); diff --git a/invokeai/frontend/webv2/src/workbench/components/ShellErrorSurface.tsx b/invokeai/frontend/webv2/src/workbench/components/ShellErrorSurface.tsx new file mode 100644 index 00000000000..909ff856b54 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/ShellErrorSurface.tsx @@ -0,0 +1,60 @@ +import { Button, HStack, Icon, Stack, Text } from '@chakra-ui/react'; +import { PiWarningBold } from 'react-icons/pi'; + +import { useWorkbench } from '../WorkbenchContext'; + +/** + * Copyable shell error surface. + * + * Phase 1 requires visible, copyable render/registration error details for + * shell-level failures so they can be filed as issues. Widget-level isolation + * arrives with the registry in Phase 3. + */ +export const ShellErrorSurface = () => { + const { state } = useWorkbench(); + + if (state.errorLog.length === 0) { + return null; + } + + return ( + + + + + Shell error log + + + {state.errorLog.map((message, index) => ( + + + {message} + + + + ))} + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/components/StatusBar.tsx b/invokeai/frontend/webv2/src/workbench/components/StatusBar.tsx new file mode 100644 index 00000000000..8694d8855f9 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/StatusBar.tsx @@ -0,0 +1,206 @@ +import { Box, Flex, Icon, Menu, Portal, Text, Tooltip } from '@chakra-ui/react'; +import { PiCheckBold, PiDotsThreeBold } from 'react-icons/pi'; + +import { WidgetIcon } from '../iconResolver'; +import type { RegisteredWidget, WidgetId } from '../types'; +import { getWidgetsForRegion } from '../widgetRegistry'; +import { useWorkbench } from '../WorkbenchContext'; +import { WidgetRenderer } from './WidgetRenderer'; + +interface BottomWidgetItem { + failureMessage?: string; + id: WidgetId; + isEnabled: boolean; + isExpandable: boolean; + label: string; + status: RegisteredWidget['status']; + widget: RegisteredWidget; +} + +export const StatusBar = () => { + const { activeProject, dispatch } = useWorkbench(); + const bottomRegion = activeProject.widgetRegions.bottom; + const items: BottomWidgetItem[] = getWidgetsForRegion('bottom').map((widget) => ({ + failureMessage: widget.failure?.message, + id: widget.manifest.id, + isEnabled: bottomRegion.enabledWidgetIds.includes(widget.manifest.id), + isExpandable: widget.manifest.bottomPanel !== 'tooltip', + label: widget.manifest.labelText, + status: widget.status, + widget, + })); + const compactItems = items.filter((item) => item.isEnabled && item.status === 'enabled'); + + return ( + + {compactItems.map((item) => ( + dispatch({ region: 'bottom', type: 'selectRegionWidget', widgetId })} + /> + ))} + + dispatch({ region: 'bottom', type: 'toggleRegionWidget', widgetId })} + /> + + ); +}; + +const CompactBottomWidget = ({ + isActive, + item, + onSelect, +}: { + isActive: boolean; + item: BottomWidgetItem; + onSelect: (widgetId: WidgetId) => void; +}) => { + const View = item.widget.manifest.view; + + if (!View) { + return null; + } + + const content = ( + { + if (item.isExpandable) { + onSelect(item.id); + } + }} + onKeyDown={(event) => { + if (item.isExpandable && (event.key === 'Enter' || event.key === ' ')) { + event.preventDefault(); + onSelect(item.id); + } + }} + > + + + ); + + if (item.isExpandable) { + return ( + + {content} + + + + {item.failureMessage ? `${item.label}: ${item.failureMessage}` : item.label} + + + + + ); + } + + return ( + + {content} + + + + + + + + + ); +}; + +const BottomWidgetMenu = ({ + items, + onToggle, +}: { + items: BottomWidgetItem[]; + onToggle: (widgetId: WidgetId) => void; +}) => ( + + + + + + + + + + + + Bottom Widgets + + {items.map((item) => { + return ( + onToggle(item.id)} + > + + + {item.label} + {item.failureMessage ? ( + + Failed + + ) : null} + + ); + })} + + + + + +); diff --git a/invokeai/frontend/webv2/src/workbench/components/TopBar.tsx b/invokeai/frontend/webv2/src/workbench/components/TopBar.tsx new file mode 100644 index 00000000000..110e50252c5 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/TopBar.tsx @@ -0,0 +1,140 @@ +import { Badge, Box, Flex, HStack, Icon, IconButton, Text, VStack } from '@chakra-ui/react'; +import { + PiCaretDownBold, + PiCaretUpBold, + PiCubeBold, + PiGearSixBold, + PiListNumbersBold, + PiUserCircleBold, + PiXBold, +} from 'react-icons/pi'; + +import { InvokeControl } from './InvokeControl'; +import { LayoutPresetMenu } from './LayoutPresetMenu'; +import { ProjectTabs } from './ProjectTabs'; + +/** Workbench top bar: brand, global Invoke command cluster, project tabs, layout + account controls. */ +export const TopBar = () => ( + + + + + + + + + + + + + + + + + + + + Josh + + + +); + +/** Brand mark placeholder for the future workbench logo / app menu. */ +const BrandMark = () => ( + + + +); + +/** + * Batch-count stepper placeholder. + * + * The iteration/batch field is wired in a later phase (Generate vertical slice); + * here it reserves its slot and reads as a real stepper. + */ +const BatchCountField = () => ( + + + 3 + + + + + + +); + +/** + * Queue status + cancel cluster placeholder. + * + * Mirrors the spec's queue progress / cancel affordance. Real queue wiring, + * snapshotting, and cancellation arrive with the Invocation Controller phases. + */ +const QueueCluster = () => ( + + + + 2/3 + + + + + + + + +); diff --git a/invokeai/frontend/webv2/src/workbench/components/WidgetBar.tsx b/invokeai/frontend/webv2/src/workbench/components/WidgetBar.tsx new file mode 100644 index 00000000000..8bef0cd72c7 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/WidgetBar.tsx @@ -0,0 +1,174 @@ +import { Flex, Icon, Menu, Portal, Text, Tooltip } from '@chakra-ui/react'; +import { PiCheckBold, PiDotsThreeBold } from 'react-icons/pi'; + +import { WidgetIcon } from '../iconResolver'; +import type { RegisteredWidget, WidgetIconId, WidgetId, WidgetRegion } from '../types'; + +export interface WidgetBarItem { + id: WidgetId; + label: string; + iconId: WidgetIconId; + status?: RegisteredWidget['status']; + failureMessage?: string; + isEnabled: boolean; +} + +interface WidgetBarProps { + side: 'left' | 'right'; + region: Exclude; + activeId: WidgetId | null; + railItems: WidgetBarItem[]; + menuItems: WidgetBarItem[]; + onSelect: (widgetId: WidgetId) => void; + onToggle: (widgetId: WidgetId) => void; +} + +/** + * A widget bar (left / right rail). + * + * Phase 1 renders the rail and its slots; the widget registry, manifests, and + * real views land in Phase 3. Each slot is a labelled button so the placeholders + * are accessible and ready to bind to registered widgets later. + */ +export const WidgetBar = ({ activeId, menuItems, onSelect, onToggle, railItems, region, side }: WidgetBarProps) => ( + + {railItems.map((item) => ( + + ))} + + +); + +const WidgetSlot = ({ + item, + isActive, + onSelect, + tooltipPlacement, +}: { + item: WidgetBarItem; + isActive: boolean; + onSelect: (widgetId: WidgetId) => void; + tooltipPlacement: 'left' | 'right'; +}) => { + const tooltipLabel = item.failureMessage ? `${item.label}: ${item.failureMessage}` : item.label; + + return ( + + + onSelect(item.id)} + > + + + + + + + {tooltipLabel} + + + + + ); +}; + +const WidgetMenuButton = ({ + items, + onToggle, + region, +}: { + items: WidgetBarItem[]; + onToggle: (widgetId: WidgetId) => void; + region: Exclude; +}) => ( + + + + + + + + + + + + Widgets + + {items.map((item) => ( + onToggle(item.id)} + > + + {item.label} + {item.failureMessage ? ( + + Failed + + ) : null} + + ))} + + + + + +); diff --git a/invokeai/frontend/webv2/src/workbench/components/WidgetFailureBoundary.tsx b/invokeai/frontend/webv2/src/workbench/components/WidgetFailureBoundary.tsx new file mode 100644 index 00000000000..1d3a49c0be5 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/WidgetFailureBoundary.tsx @@ -0,0 +1,56 @@ +import { Button, Code, Stack, Text } from '@chakra-ui/react'; +import { Component, type ErrorInfo, type ReactNode } from 'react'; + +import type { WidgetId } from '../types'; + +interface WidgetFailureBoundaryProps { + widgetId: WidgetId; + children: ReactNode; +} + +interface WidgetFailureBoundaryState { + error?: Error; + details?: string; +} + +export class WidgetFailureBoundary extends Component { + state: WidgetFailureBoundaryState = {}; + + static getDerivedStateFromError(error: Error): WidgetFailureBoundaryState { + return { error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + this.setState({ details: errorInfo.componentStack ?? error.stack ?? error.message }); + } + + render() { + const { children, widgetId } = this.props; + const { details, error } = this.state; + + if (!error) { + return children; + } + + const copyableDetails = details ?? error.message; + + return ( + + + Widget failed: {widgetId} + + + {copyableDetails} + + + + ); + } +} diff --git a/invokeai/frontend/webv2/src/workbench/components/WidgetFrames.tsx b/invokeai/frontend/webv2/src/workbench/components/WidgetFrames.tsx new file mode 100644 index 00000000000..c5bbdc9c080 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/WidgetFrames.tsx @@ -0,0 +1,184 @@ +import { Box, HStack, Icon, Stack, Text } from '@chakra-ui/react'; +import { + useState, + type KeyboardEvent as ReactKeyboardEvent, + type PointerEvent as ReactPointerEvent, + type ReactNode, +} from 'react'; +import type { IconType } from 'react-icons'; + +import { createGraphBearingSurface } from '../graphSurfaces'; +import type { WidgetManifest, WidgetRegion, WorkbenchRegion } from '../types'; +import { useWorkbench } from '../WorkbenchContext'; +import { GraphSurfaceActions } from './GraphSurfaceActions'; + +const PANEL_SIZE_STEP_PX = 16; +const MIN_PANEL_SIZE_PX = 180; +const MAX_PANEL_SIZE_PX = 520; +const MIN_BOTTOM_PANEL_SIZE_PX = 96; +const MAX_BOTTOM_PANEL_SIZE_PX = 420; + +const getPanelSizeBounds = (region: WidgetRegion): { max: number; min: number } => { + if (region === 'bottom') { + return { max: MAX_BOTTOM_PANEL_SIZE_PX, min: MIN_BOTTOM_PANEL_SIZE_PX }; + } + + return { max: MAX_PANEL_SIZE_PX, min: MIN_PANEL_SIZE_PX }; +}; + +const clampSize = (region: WidgetRegion, sizePx: number): number => { + const { max, min } = getPanelSizeBounds(region); + + return Math.min(max, Math.max(min, sizePx)); +}; + +export const WidgetPanelFrame = ({ + children, + region, +}: { + children: ReactNode; + region: Exclude; +}) => { + const { activeProject, dispatch } = useWorkbench(); + const regionState = activeProject.widgetRegions[region]; + const [dragSizePx, setDragSizePx] = useState(null); + const isLeft = region === 'left'; + const isBottom = region === 'bottom'; + const displaySizePx = dragSizePx ?? regionState.sizePx; + const sizeBounds = getPanelSizeBounds(region); + + const commitSize = (sizePx: number) => { + const nextSizePx = clampSize(region, sizePx); + + if (nextSizePx !== regionState.sizePx) { + dispatch({ region, sizePx: nextSizePx, type: 'setRegionWidgetSize' }); + } + }; + + const handlePointerDown = (event: ReactPointerEvent) => { + event.preventDefault(); + + const startX = event.clientX; + const startY = event.clientY; + const startSizePx = regionState.sizePx; + let nextSizePx = startSizePx; + const direction = isLeft ? 1 : -1; + + const handlePointerMove = (moveEvent: PointerEvent) => { + const deltaPx = isBottom ? startY - moveEvent.clientY : (moveEvent.clientX - startX) * direction; + + nextSizePx = clampSize(region, startSizePx + deltaPx); + setDragSizePx(nextSizePx); + }; + + const handlePointerUp = () => { + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); + window.removeEventListener('pointercancel', handlePointerUp); + setDragSizePx(null); + commitSize(nextSizePx); + }; + + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + window.addEventListener('pointercancel', handlePointerUp); + }; + + const handleKeyDown = (event: ReactKeyboardEvent) => { + const step = event.shiftKey ? PANEL_SIZE_STEP_PX * 2 : PANEL_SIZE_STEP_PX; + const sizeChanges: Partial> = isBottom + ? { ArrowDown: -step, ArrowUp: step, End: sizeBounds.max - displaySizePx, Home: sizeBounds.min - displaySizePx } + : { + ArrowLeft: isLeft ? -step : step, + ArrowRight: isLeft ? step : -step, + End: sizeBounds.max - displaySizePx, + Home: sizeBounds.min - displaySizePx, + }; + const sizeChange = sizeChanges[event.key]; + + if (sizeChange === undefined) { + return; + } + + event.preventDefault(); + commitSize(displaySizePx + sizeChange); + }; + + return ( + + {children} + + + ); +}; + +export const GraphBearingWidgetHeader = ({ + manifest, + region, +}: { + manifest: WidgetManifest; + region: WorkbenchRegion; +}) => { + const surface = createGraphBearingSurface(manifest, region); + + return ( + + + {manifest.labelText} + + {surface ? : null} + + ); +}; + +export const FieldPlaceholder = ({ label, h }: { label: string; h: string }) => ( + + + {label} + + + +); + +export const StatusWidgetChip = ({ icon, children }: { icon: IconType; children: ReactNode }) => ( + + + + {children} + + +); diff --git a/invokeai/frontend/webv2/src/workbench/components/WidgetRenderer.tsx b/invokeai/frontend/webv2/src/workbench/components/WidgetRenderer.tsx new file mode 100644 index 00000000000..f44cdaeef11 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/components/WidgetRenderer.tsx @@ -0,0 +1,22 @@ +import type { RegisteredWidget, WidgetViewProps } from '../types'; +import { WidgetFailureBoundary } from './WidgetFailureBoundary'; + +interface WidgetRendererProps extends Omit { + widget: RegisteredWidget; +} + +export const WidgetRenderer = ({ presentation, region, widget }: WidgetRendererProps) => { + const View = widget.manifest.view; + + if (!View) { + return null; + } + + const content = ; + + if (!widget.manifest.failurePolicy.isolateRenderFailure) { + return content; + } + + return {content}; +}; diff --git a/invokeai/frontend/webv2/src/workbench/graphSurfaces.ts b/invokeai/frontend/webv2/src/workbench/graphSurfaces.ts new file mode 100644 index 00000000000..c774662de22 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/graphSurfaces.ts @@ -0,0 +1,24 @@ +import type { GraphBearingSurfaceContract, WidgetManifest, WorkbenchRegion } from './types'; +import { isInvocationSourceAvailable } from './invocation'; + +export const createGraphBearingSurface = ( + manifest: WidgetManifest, + region: WorkbenchRegion +): GraphBearingSurfaceContract | null => { + const graphBearing = manifest.graphBearing; + + if (!graphBearing || !graphBearing.surfaces.includes(region)) { + return null; + } + + return { + canPreviewGraph: true, + canSetSource: isInvocationSourceAvailable(graphBearing.sourceId), + graphId: graphBearing.defaultGraphId, + label: manifest.labelText, + region, + sourceId: graphBearing.sourceId, + surfaceId: `${manifest.id}:${region}`, + widgetId: manifest.id, + }; +}; diff --git a/invokeai/frontend/webv2/src/workbench/iconResolver.tsx b/invokeai/frontend/webv2/src/workbench/iconResolver.tsx new file mode 100644 index 00000000000..c6c57038301 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/iconResolver.tsx @@ -0,0 +1,54 @@ +import { Icon, type IconProps } from '@chakra-ui/react'; +import { Square } from 'lucide-react'; +import { lazy, Suspense, type ComponentType } from 'react'; + +import type { WidgetIconId } from './types'; + +type IconImporter = () => Promise<{ default: ComponentType }>; + +const iconImporters = { + 'lucide-react:cloud-check': () => import('lucide-react/dist/esm/icons/cloud-check.mjs'), + 'lucide-react:image': () => import('lucide-react/dist/esm/icons/image.mjs'), + 'lucide-react:info': () => import('lucide-react/dist/esm/icons/info.mjs'), + 'lucide-react:layers': () => import('lucide-react/dist/esm/icons/layers.mjs'), + 'lucide-react:list-ordered': () => import('lucide-react/dist/esm/icons/list-ordered.mjs'), + 'lucide-react:panel-bottom': () => import('lucide-react/dist/esm/icons/panel-bottom.mjs'), + 'lucide-react:plug-zap': () => import('lucide-react/dist/esm/icons/plug-zap.mjs'), + 'lucide-react:sliders-horizontal': () => import('lucide-react/dist/esm/icons/sliders-horizontal.mjs'), + 'lucide-react:undo-2': () => import('lucide-react/dist/esm/icons/undo-2.mjs'), + 'lucide-react:wand-sparkles': () => import('lucide-react/dist/esm/icons/wand-sparkles.mjs'), + 'lucide-react:workflow': () => import('lucide-react/dist/esm/icons/workflow.mjs'), +} satisfies Partial>; + +const iconCache = new Map(); + +const getIconImporter = (iconId: WidgetIconId): IconImporter | undefined => + iconImporters[iconId as keyof typeof iconImporters]; + +export const isSupportedIconId = (iconId: string): iconId is WidgetIconId => { + return getIconImporter(iconId as WidgetIconId) !== undefined; +}; + +const resolveLazyIcon = (iconId: WidgetIconId): ComponentType => { + const cachedIcon = iconCache.get(iconId); + + if (cachedIcon) { + return cachedIcon; + } + + const LazyIcon = lazy(getIconImporter(iconId) ?? (() => Promise.resolve({ default: Square }))); + + iconCache.set(iconId, LazyIcon); + + return LazyIcon; +}; + +export const WidgetIcon = ({ iconId, ...props }: IconProps & { iconId: WidgetIconId }) => { + const ResolvedIcon = resolveLazyIcon(iconId); + + return ( + }> + + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/invocation.ts b/invokeai/frontend/webv2/src/workbench/invocation.ts new file mode 100644 index 00000000000..ce317bcdccc --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/invocation.ts @@ -0,0 +1,106 @@ +import type { + InvocationMode, + InvocationRoute, + InvocationSourceId, + Project, + ResolvedInvocationRoute, + ResultDestination, + WidgetId, +} from './types'; + +/** + * Static metadata for the Invocation Controller surfaces. + * + * MVP destinations are limited to Canvas and Gallery per the spec. Sources are + * the first-party graph-bearing surfaces; only `generate` is wired in Phase 1, + * the rest are declared so the source menu reads as a real (if partly inert) + * placeholder for later phases. + */ +export interface InvocationSourceMeta { + id: InvocationSourceId; + label: string; + /** Whether the source is selectable yet, or a forward-looking placeholder. */ + available: boolean; +} + +export interface ResultDestinationMeta { + id: ResultDestination; + label: string; +} + +export const invocationSources: InvocationSourceMeta[] = [ + { id: 'generate', label: 'Generate', available: true }, + { id: 'project-graph', label: 'Workflow', available: true }, + { id: 'upscale', label: 'Upscale', available: false }, + { id: 'canvas-fill', label: 'Canvas Fill', available: false }, +]; + +export const resultDestinations: ResultDestinationMeta[] = [ + { id: 'canvas', label: 'Canvas' }, + { id: 'gallery', label: 'Gallery' }, +]; + +const sourceLabels = new Map(invocationSources.map((source) => [source.id, source.label])); +const destinationLabels = new Map(resultDestinations.map((destination) => [destination.id, destination.label])); + +export const getSourceLabel = (id: InvocationSourceId): string => sourceLabels.get(id) ?? 'Generate'; + +export const isInvocationSourceAvailable = (id: InvocationSourceId): boolean => + invocationSources.some((source) => source.id === id && source.available); + +export const getDestinationLabel = (id: ResultDestination): string => destinationLabels.get(id) ?? 'Canvas'; + +export const formatRoute = (route: InvocationRoute): string => + `${getSourceLabel(route.sourceId)} → ${getDestinationLabel(route.destination)}`; + +export const defaultInvocationRoute: InvocationRoute = { + sourceId: 'generate', + destination: 'canvas', + sourceLocked: false, + destinationLocked: false, +}; + +const validDestinationIds = new Set(resultDestinations.map((destination) => destination.id)); + +const sourceWidgetIds: Partial> = { + 'canvas-fill': 'canvas', + generate: 'generate', + 'project-graph': 'workflow', +}; + +const isWidgetMounted = (project: Project, widgetId: WidgetId): boolean => + Object.values(project.widgetRegions).some((region) => region.enabledWidgetIds.includes(widgetId)); + +export const isResultDestinationAvailable = (destination: ResultDestination): boolean => + validDestinationIds.has(destination); + +export const resolveInvocationRoute = ( + project: Project, + mode: InvocationMode = 'global', + route: InvocationRoute = project.invocation +): ResolvedInvocationRoute => { + const sourceId = route.sourceId; + const destination = route.destination; + const sourceWidgetId = sourceWidgetIds[sourceId]; + const sourceValid = + isInvocationSourceAvailable(sourceId) && (!sourceWidgetId || isWidgetMounted(project, sourceWidgetId)); + const destinationValid = isResultDestinationAvailable(destination); + const validationMessage = !sourceValid + ? `${getSourceLabel(sourceId)} is not an available invocation source.` + : !destinationValid + ? `${getDestinationLabel(destination)} is not an available result destination.` + : undefined; + + return { + ...route, + destination, + destinationValid, + mode, + sourceId, + sourceValid, + validationMessage, + }; +}; + +export const isInvocationRouteValid = (route: ResolvedInvocationRoute): boolean => + route.sourceValid && route.destinationValid; diff --git a/invokeai/frontend/webv2/src/workbench/layoutPresets.ts b/invokeai/frontend/webv2/src/workbench/layoutPresets.ts new file mode 100644 index 00000000000..d40456deab9 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/layoutPresets.ts @@ -0,0 +1,59 @@ +import type { LayoutPreset } from './types'; + +export const layoutPresets: LayoutPreset[] = [ + { + description: 'Generate controls, canvas, preview, and workflow tabs in one project-owned workbench.', + id: 'canvas-default', + initialLayout: { + centerViewId: 'canvas', + panels: { isBottomOpen: true, isLeftOpen: true, isRightOpen: true }, + presetId: 'canvas-default', + }, + label: 'Default Layout', + }, + { + description: 'Future gallery-first page parity preset.', + id: 'gallery', + initialLayout: { + centerViewId: 'gallery', + panels: { isBottomOpen: true, isLeftOpen: false, isRightOpen: true }, + presetId: 'gallery', + }, + label: 'Gallery', + }, + { + description: 'Future graph authoring preset.', + id: 'workflow', + initialLayout: { + centerViewId: 'workflow', + panels: { isBottomOpen: false, isLeftOpen: true, isRightOpen: false }, + presetId: 'workflow', + }, + label: 'Workflow', + }, + { + description: 'Future linear generation preset.', + id: 'linear', + initialLayout: { + centerViewId: 'canvas', + panels: { isBottomOpen: true, isLeftOpen: true, isRightOpen: false }, + presetId: 'linear', + }, + label: 'Linear UI', + }, + { + description: 'Future model management preset.', + id: 'model-manager', + initialLayout: { + centerViewId: 'gallery', + panels: { isBottomOpen: false, isLeftOpen: false, isRightOpen: true }, + presetId: 'model-manager', + }, + label: 'Model Manager', + }, +]; + +export const defaultLayoutPreset = layoutPresets[0]; + +export const getLayoutPreset = (presetId: string) => + layoutPresets.find((preset) => preset.id === presetId) ?? defaultLayoutPreset; diff --git a/invokeai/frontend/webv2/src/workbench/persistence.ts b/invokeai/frontend/webv2/src/workbench/persistence.ts new file mode 100644 index 00000000000..1b22ac49a05 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/persistence.ts @@ -0,0 +1,53 @@ +import type { WorkbenchPersistenceSnapshot, WorkbenchState } from './types'; + +const STORAGE_KEY = 'invokeai:v7:webv2:workbench'; + +export interface WorkbenchPersistenceService { + loadWorkbench(): Promise; + saveWorkbench(state: WorkbenchState): Promise; + clearWorkbench(): Promise; +} + +const isBrowser = (): boolean => typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'; + +const createSnapshot = (state: WorkbenchState): WorkbenchPersistenceSnapshot => ({ + savedAt: new Date().toISOString(), + state, + version: 1, +}); + +export const localStorageWorkbenchPersistence: WorkbenchPersistenceService = { + clearWorkbench() { + if (!isBrowser()) { + return Promise.resolve(); + } + + window.localStorage.removeItem(STORAGE_KEY); + + return Promise.resolve(); + }, + loadWorkbench() { + if (!isBrowser()) { + return Promise.resolve(null); + } + + const value = window.localStorage.getItem(STORAGE_KEY); + + if (!value) { + return Promise.resolve(null); + } + + return Promise.resolve(JSON.parse(value) as WorkbenchPersistenceSnapshot); + }, + saveWorkbench(state) { + const snapshot = createSnapshot(state); + + if (!isBrowser()) { + return Promise.resolve(snapshot); + } + + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot)); + + return Promise.resolve(snapshot); + }, +}; diff --git a/invokeai/frontend/webv2/src/workbench/types.ts b/invokeai/frontend/webv2/src/workbench/types.ts new file mode 100644 index 00000000000..c9667b2b5bc --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/types.ts @@ -0,0 +1,298 @@ +import type { ComponentType } from 'react'; + +export type LayoutPresetId = 'canvas-default' | 'gallery' | 'workflow' | 'linear' | 'model-manager'; + +export type CenterViewId = 'canvas' | 'gallery' | 'preview' | 'workflow'; + +export type GraphId = string; + +export type WidgetId = + | 'autosave-status' + | 'canvas' + | 'gallery' + | 'generate' + | 'history-controls' + | 'layout-actions' + | 'layers' + | 'queue' + | 'server-status' + | 'version-status' + | 'workflow'; + +export type WorkbenchRegion = 'left' | 'right' | 'center' | 'bottom' | 'dialog' | 'popover'; + +export type QueueItemStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; + +export type ProjectEventType = 'project-created' | 'layout-updated' | 'invocation-updated' | 'queue-submitted'; + +export interface GraphNodeContract { + id: string; + type: string; + inputs: Record; +} + +export interface GraphEdgeContract { + id: string; + sourceNodeId: string; + sourceField: string; + targetNodeId: string; + targetField: string; +} + +export interface GraphContract { + id: GraphId; + version: 1; + label: string; + nodes: GraphNodeContract[]; + edges: GraphEdgeContract[]; + updatedAt: string; +} + +export interface WidgetStateContract { + id: WidgetId; + label: string; + version: 1; + values: Record; + graphId?: GraphId; +} + +export interface GraphBearingSurfaceContract { + surfaceId: string; + widgetId: WidgetId; + label: string; + sourceId: InvocationSourceId; + graphId: GraphId; + region: WorkbenchRegion; + canSetSource: boolean; + canPreviewGraph: boolean; +} + +export interface WidgetViewProps { + region: WorkbenchRegion; + manifest: WidgetManifest; + presentation?: 'compact' | 'expanded' | 'tooltip'; +} + +export type WidgetView = ComponentType; + +export interface WidgetLabelProps { + region: WorkbenchRegion; + presentation?: 'compact' | 'expanded' | 'tooltip'; +} + +export type WidgetLabel = string | ComponentType; + +export type WidgetIconId = `lucide-react:${string}`; + +export interface WidgetManifest { + id: WidgetId; + label: WidgetLabel; + labelText: string; + version: 1; + regions: WorkbenchRegion[]; + icon: WidgetIconId; + bottomPanel?: 'expandable' | 'tooltip'; + view?: WidgetView; + graphBearing?: { + sourceId: InvocationSourceId; + defaultGraphId: GraphId; + surfaces: WorkbenchRegion[]; + }; + failurePolicy: { + onRegistrationFailure: 'disable' | 'hide'; + isolateRenderFailure: boolean; + }; +} + +export interface RegisteredWidget { + manifest: WidgetManifest; + status: 'enabled' | 'disabled' | 'hidden'; + failure?: WidgetFailure; +} + +export interface WidgetFailure { + widgetId: WidgetId; + message: string; + details: string; + occurredAt: string; +} + +export interface CanvasStateContract { + version: 1; + layers: string[]; + stagingArea: { + selectedLayerId?: string; + pendingImageIds: string[]; + }; +} + +/** + * Invocation sources and destinations. + * + * Phase 1 only needs enough of the Invocation Controller to render a stable, + * fixed-width `Source → Destination` route on the global Invoke control. The + * full resolver (auto vs. locked, validation, dialog focus) arrives in Phase 4, + * but the project-owned shape is modelled now so the control is not a dead + * placeholder. + */ +export type InvocationSourceId = 'generate' | 'project-graph' | 'upscale' | 'canvas-fill'; + +export type InvocationMode = 'global' | 'dialog'; + +export type ResultDestination = 'canvas' | 'gallery'; + +export interface InvocationRoute { + sourceId: InvocationSourceId; + destination: ResultDestination; + sourceLocked: boolean; + destinationLocked: boolean; +} + +export interface ResolvedInvocationRoute extends InvocationRoute { + mode: InvocationMode; + sourceValid: boolean; + destinationValid: boolean; + validationMessage?: string; +} + +export interface InvocationControllerState extends InvocationRoute { + lastSubmittedRunId?: string; +} + +export interface PanelState { + isLeftOpen: boolean; + isRightOpen: boolean; + isBottomOpen: boolean; +} + +export type WidgetRegion = 'left' | 'right' | 'bottom' | 'center'; + +export interface WidgetRegionState { + activeWidgetId: WidgetId; + enabledWidgetIds: WidgetId[]; + isCollapsed: boolean; + sizePx: number; +} + +export interface ProjectLayoutState { + presetId: LayoutPresetId; + centerViewId: CenterViewId; + panels: PanelState; +} + +export interface Project { + id: string; + name: string; + layout: ProjectLayoutState; + invocation: InvocationControllerState; + projectGraph: GraphContract; + widgetStates: Record; + widgetRegions: Record; + widgetGraphs: Partial>; + canvas: CanvasStateContract; + graphHistory: GraphHistorySnapshot[]; + undoRedo: UndoRedoHistory; + queue: QueueState; + events: ProjectEvent[]; +} + +export interface LayoutPreset { + id: LayoutPresetId; + label: string; + description: string; + initialLayout: ProjectLayoutState; +} + +export interface WorkbenchState { + projects: Project[]; + activeProjectId: string; + errorLog: string[]; + autosave: AutosaveState; + account: AccountState; + widgetFailures: WidgetFailure[]; +} + +export interface GraphHistorySnapshot { + id: string; + createdAt: string; + label: string; + graph: GraphContract; +} + +export interface UndoRedoEntry { + id: string; + createdAt: string; + label: string; + project: ProjectUndoSnapshot; +} + +export interface ProjectUndoSnapshot { + layout: ProjectLayoutState; + invocation: InvocationControllerState; + projectGraph: GraphContract; + widgetStates: Record; + widgetRegions: Record; + widgetGraphs: Partial>; + canvas: CanvasStateContract; +} + +export interface UndoRedoHistory { + past: UndoRedoEntry[]; + future: UndoRedoEntry[]; +} + +export interface QueueSubmissionSnapshot { + sourceId: InvocationSourceId; + destination: ResultDestination; + graph: GraphContract; + widgetStates: Record; + canvas: CanvasStateContract; + submittedAt: string; +} + +export interface QueueItem { + id: string; + status: QueueItemStatus; + cancellable: boolean; + snapshot: QueueSubmissionSnapshot; +} + +export interface QueueState { + items: QueueItem[]; +} + +export interface ProjectEvent { + id: string; + type: ProjectEventType; + createdAt: string; + summary: string; + runId?: string; +} + +export interface RunRecord { + id: string; + projectId: string; + queueItemId: string; + sourceId: InvocationSourceId; + destination: ResultDestination; + graphSnapshotId: string; + status: QueueItemStatus; + submittedAt: string; + completedAt?: string; +} + +export interface AutosaveState { + status: 'idle' | 'saving' | 'saved' | 'error'; + lastSavedAt?: string; + error?: string; +} + +export interface AccountState { + activeLayoutPresetId: LayoutPresetId; +} + +export interface WorkbenchPersistenceSnapshot { + version: 1; + savedAt: string; + state: WorkbenchState; +} diff --git a/invokeai/frontend/webv2/src/workbench/widgetRegistry.ts b/invokeai/frontend/webv2/src/workbench/widgetRegistry.ts new file mode 100644 index 00000000000..fb49274837f --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgetRegistry.ts @@ -0,0 +1,76 @@ +import { isSupportedIconId } from './iconResolver'; +import type { RegisteredWidget, WidgetFailure, WidgetId, WidgetManifest, WorkbenchRegion } from './types'; +import { autosaveStatusWidgetManifest } from './widgets/autosave-status'; +import { canvasWidgetManifest } from './widgets/canvas'; +import { galleryWidgetManifest } from './widgets/gallery'; +import { generateWidgetManifest } from './widgets/generate'; +import { historyControlsWidgetManifest } from './widgets/history-controls'; +import { layoutActionsWidgetManifest } from './widgets/layout-actions'; +import { layersWidgetManifest } from './widgets/layers'; +import { queueWidgetManifest } from './widgets/queue'; +import { serverStatusWidgetManifest } from './widgets/server-status'; +import { versionStatusWidgetManifest } from './widgets/version-status'; +import { workflowWidgetManifest } from './widgets/workflow'; + +const firstPartyWidgetManifests: WidgetManifest[] = [ + generateWidgetManifest, + workflowWidgetManifest, + canvasWidgetManifest, + galleryWidgetManifest, + layersWidgetManifest, + queueWidgetManifest, + serverStatusWidgetManifest, + autosaveStatusWidgetManifest, + historyControlsWidgetManifest, + layoutActionsWidgetManifest, + versionStatusWidgetManifest, +]; + +const createFailure = (widgetId: WidgetId, error: unknown): WidgetFailure => ({ + details: error instanceof Error ? (error.stack ?? error.message) : String(error), + message: error instanceof Error ? error.message : `Failed to register ${widgetId}.`, + occurredAt: new Date().toISOString(), + widgetId, +}); + +const renderableRegions = new Set(['bottom', 'center', 'dialog', 'left', 'popover', 'right']); + +const validateManifest = (manifest: WidgetManifest): void => { + if (manifest.regions.length === 0) { + throw new Error(`Widget ${manifest.id} must declare at least one allowed region.`); + } + + if (!isSupportedIconId(manifest.icon)) { + throw new Error(`Widget ${manifest.id} references unsupported icon id ${manifest.icon}.`); + } + + if (manifest.regions.some((region) => renderableRegions.has(region)) && !manifest.view) { + throw new Error(`Widget ${manifest.id} declares a renderable region but does not include manifest.view.`); + } +}; + +export const registerFirstPartyWidgets = (): RegisteredWidget[] => + firstPartyWidgetManifests.map((manifest) => { + try { + validateManifest(manifest); + + return { manifest, status: 'enabled' as const }; + } catch (error) { + const failure = createFailure(manifest.id, error); + const status = manifest.failurePolicy.onRegistrationFailure === 'hide' ? 'hidden' : 'disabled'; + + return { failure, manifest, status }; + } + }); + +export const registeredWidgets = registerFirstPartyWidgets(); + +export const getWidgetsForRegion = (region: WorkbenchRegion): RegisteredWidget[] => + registeredWidgets.filter((widget) => widget.status !== 'hidden' && widget.manifest.regions.includes(region)); + +export const getWidgetById = (widgetId: WidgetId): RegisteredWidget | undefined => + registeredWidgets.find((widget) => widget.manifest.id === widgetId); + +export const widgetRegistrationFailures = registeredWidgets.flatMap((widget) => + widget.failure ? [widget.failure] : [] +); diff --git a/invokeai/frontend/webv2/src/workbench/widgets/autosave-status/AutosaveStatusWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/autosave-status/AutosaveStatusWidgetView.tsx new file mode 100644 index 00000000000..46105af39f5 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/autosave-status/AutosaveStatusWidgetView.tsx @@ -0,0 +1,37 @@ +import { Stack, Text } from '@chakra-ui/react'; +import { PiCloudCheckBold, PiCloudWarningBold } from 'react-icons/pi'; + +import { StatusWidgetChip } from '../../components/WidgetFrames'; +import type { WidgetViewProps } from '../../types'; +import { useWorkbench } from '../../WorkbenchContext'; + +export const AutosaveStatusWidgetView = ({ presentation }: WidgetViewProps) => { + const { state } = useWorkbench(); + const icon = state.autosave.status === 'error' ? PiCloudWarningBold : PiCloudCheckBold; + const label = state.autosave.status === 'saved' ? 'Saved' : state.autosave.status; + + if (presentation === 'tooltip') { + return ( + + + Autosave + + + Status: {label} + + {state.autosave.lastSavedAt ? ( + + Last saved: {state.autosave.lastSavedAt} + + ) : null} + {state.autosave.error ? ( + + {state.autosave.error} + + ) : null} + + ); + } + + return Autosave: {label}; +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/autosave-status/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/autosave-status/index.ts new file mode 100644 index 00000000000..ee7e69b3549 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/autosave-status/index.ts @@ -0,0 +1,14 @@ +import type { WidgetManifest } from '../../types'; +import { AutosaveStatusWidgetView } from './AutosaveStatusWidgetView'; + +export const autosaveStatusWidgetManifest: WidgetManifest = { + bottomPanel: 'tooltip', + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + icon: 'lucide-react:cloud-check', + id: 'autosave-status', + label: 'Autosave', + labelText: 'Autosave', + regions: ['bottom'], + version: 1, + view: AutosaveStatusWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/canvas/CanvasWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/canvas/CanvasWidgetView.tsx new file mode 100644 index 00000000000..2f555555481 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/canvas/CanvasWidgetView.tsx @@ -0,0 +1,63 @@ +import { Box } from '@chakra-ui/react'; + +import { GraphBearingWidgetHeader } from '../../components/WidgetFrames'; +import { WidgetFailureBoundary } from '../../components/WidgetFailureBoundary'; +import type { WidgetViewProps } from '../../types'; + +export const CanvasWidgetView = ({ manifest }: WidgetViewProps) => ( + + + + + + + + + + + +); + +interface StagingFrameProps { + isPrimary?: boolean; + top?: string; + left?: string; + right?: string; + bottom?: string; + w: string; + h: string; +} + +const StagingFrame = ({ isPrimary, ...position }: StagingFrameProps) => ( + +); + +const ToolScrubber = () => ( + +); diff --git a/invokeai/frontend/webv2/src/workbench/widgets/canvas/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/canvas/index.ts new file mode 100644 index 00000000000..62bb1085c43 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/canvas/index.ts @@ -0,0 +1,14 @@ +import type { WidgetManifest } from '../../types'; +import { CanvasWidgetView } from './CanvasWidgetView'; + +export const canvasWidgetManifest: WidgetManifest = { + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + graphBearing: { defaultGraphId: 'canvas-graph', sourceId: 'canvas-fill', surfaces: ['center'] }, + icon: 'lucide-react:wand-sparkles', + id: 'canvas', + label: 'Canvas', + labelText: 'Canvas', + regions: ['center'], + version: 1, + view: CanvasWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/gallery/GalleryWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/gallery/GalleryWidgetView.tsx new file mode 100644 index 00000000000..10d4e4cb1c7 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/gallery/GalleryWidgetView.tsx @@ -0,0 +1,50 @@ +import { Flex, Stack, Text } from '@chakra-ui/react'; +import { PiImageBold } from 'react-icons/pi'; + +import { StatusWidgetChip, WidgetPanelFrame } from '../../components/WidgetFrames'; +import { WidgetFailureBoundary } from '../../components/WidgetFailureBoundary'; +import type { WidgetViewProps } from '../../types'; + +export const GalleryWidgetView = ({ presentation, region }: WidgetViewProps) => { + if (region === 'bottom') { + if (presentation === 'expanded') { + return ( + + + Gallery + + + Gallery status and recent outputs will render here once gallery data is connected. + + + ); + } + + return Gallery; + } + + if (region === 'right') { + return ( + + + + Gallery + + + Gallery controls will render here when this widget is mounted into the right panel. + + + + ); + } + + return ( + + + + Gallery view + + + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/gallery/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/gallery/index.ts new file mode 100644 index 00000000000..ca394c27cca --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/gallery/index.ts @@ -0,0 +1,13 @@ +import type { WidgetManifest } from '../../types'; +import { GalleryWidgetView } from './GalleryWidgetView'; + +export const galleryWidgetManifest: WidgetManifest = { + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + icon: 'lucide-react:image', + id: 'gallery', + label: 'Gallery', + labelText: 'Gallery', + regions: ['right', 'center', 'bottom'], + version: 1, + view: GalleryWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/generate/GenerateWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/generate/GenerateWidgetView.tsx new file mode 100644 index 00000000000..90421c28fcd --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/generate/GenerateWidgetView.tsx @@ -0,0 +1,15 @@ +import { FieldPlaceholder, GraphBearingWidgetHeader, WidgetPanelFrame } from '../../components/WidgetFrames'; +import { WidgetFailureBoundary } from '../../components/WidgetFailureBoundary'; +import type { WidgetViewProps } from '../../types'; + +export const GenerateWidgetView = ({ manifest }: WidgetViewProps) => ( + + + + + + + + + +); diff --git a/invokeai/frontend/webv2/src/workbench/widgets/generate/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/generate/index.ts new file mode 100644 index 00000000000..f82f2d922f8 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/generate/index.ts @@ -0,0 +1,14 @@ +import type { WidgetManifest } from '../../types'; +import { GenerateWidgetView } from './GenerateWidgetView'; + +export const generateWidgetManifest: WidgetManifest = { + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + graphBearing: { defaultGraphId: 'generate-graph', sourceId: 'generate', surfaces: ['left'] }, + icon: 'lucide-react:sliders-horizontal', + id: 'generate', + label: 'Generate', + labelText: 'Generate', + regions: ['left'], + version: 1, + view: GenerateWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/history-controls/HistoryControlsWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/history-controls/HistoryControlsWidgetView.tsx new file mode 100644 index 00000000000..577f9372172 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/history-controls/HistoryControlsWidgetView.tsx @@ -0,0 +1,82 @@ +import { Button, HStack, Stack, Text } from '@chakra-ui/react'; +import { PiArrowClockwiseBold, PiArrowCounterClockwiseBold } from 'react-icons/pi'; + +import type { WidgetViewProps } from '../../types'; +import { useWorkbench } from '../../WorkbenchContext'; + +export const HistoryControlsWidgetView = ({ presentation }: WidgetViewProps) => { + const { activeProject, dispatch } = useWorkbench(); + const canUndo = activeProject.undoRedo.past.length > 0; + const canRedo = activeProject.undoRedo.future.length > 0; + + if (presentation === 'expanded') { + return ( + + + History Controls + + dispatch({ type: 'redoProjectChange' })} + onUndo={() => dispatch({ type: 'undoProjectChange' })} + /> + + Undo entries: {activeProject.undoRedo.past.length}. Redo entries: {activeProject.undoRedo.future.length}. + + + ); + } + + return ( + dispatch({ type: 'redoProjectChange' })} + onUndo={() => dispatch({ type: 'undoProjectChange' })} + /> + ); +}; + +const HistoryButtons = ({ + canRedo, + canUndo, + onRedo, + onUndo, +}: { + canRedo: boolean; + canUndo: boolean; + onRedo: () => void; + onUndo: () => void; +}) => ( + + + + +); diff --git a/invokeai/frontend/webv2/src/workbench/widgets/history-controls/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/history-controls/index.ts new file mode 100644 index 00000000000..dc2bd532ad7 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/history-controls/index.ts @@ -0,0 +1,13 @@ +import type { WidgetManifest } from '../../types'; +import { HistoryControlsWidgetView } from './HistoryControlsWidgetView'; + +export const historyControlsWidgetManifest: WidgetManifest = { + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + icon: 'lucide-react:undo-2', + id: 'history-controls', + label: 'History Controls', + labelText: 'History Controls', + regions: ['bottom'], + version: 1, + view: HistoryControlsWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/layers/LayersWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/layers/LayersWidgetView.tsx new file mode 100644 index 00000000000..8721eaff107 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/layers/LayersWidgetView.tsx @@ -0,0 +1,58 @@ +import { Flex, HStack, Icon, IconButton, Stack, Text } from '@chakra-ui/react'; +import { PiDotsThreeBold, PiStackBold } from 'react-icons/pi'; + +import { WidgetPanelFrame } from '../../components/WidgetFrames'; +import { WidgetFailureBoundary } from '../../components/WidgetFailureBoundary'; +import { useWorkbench } from '../../WorkbenchContext'; + +export const LayersWidgetView = () => { + const { activeProject } = useWorkbench(); + + return ( + + + + + Layers + + + + + + + + + {activeProject.canvas.layers.length} layers, {activeProject.canvas.stagingArea.pendingImageIds.length}{' '} + staged images. + + + + + Project Contracts + + + Graph history: {activeProject.graphHistory.length} + + + Events: {activeProject.events.length} + + + Undo: {activeProject.undoRedo.past.length} / Redo: {activeProject.undoRedo.future.length} + + + + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/layers/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/layers/index.ts new file mode 100644 index 00000000000..c9ee9c139e2 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/layers/index.ts @@ -0,0 +1,13 @@ +import type { WidgetManifest } from '../../types'; +import { LayersWidgetView } from './LayersWidgetView'; + +export const layersWidgetManifest: WidgetManifest = { + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + icon: 'lucide-react:layers', + id: 'layers', + label: 'Layers', + labelText: 'Layers', + regions: ['right'], + version: 1, + view: LayersWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/layout-actions/LayoutActionsWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/layout-actions/LayoutActionsWidgetView.tsx new file mode 100644 index 00000000000..6890352042a --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/layout-actions/LayoutActionsWidgetView.tsx @@ -0,0 +1,69 @@ +import { Button, HStack, Stack, Text } from '@chakra-ui/react'; + +import type { WidgetViewProps } from '../../types'; +import { useWorkbench } from '../../WorkbenchContext'; + +export const LayoutActionsWidgetView = ({ presentation }: WidgetViewProps) => { + const { dispatch } = useWorkbench(); + + if (presentation === 'expanded') { + return ( + + + Layout Actions + + dispatch({ type: 'recoverShellLayout' })} + onResetLayout={() => dispatch({ type: 'resetActiveLayout' })} + /> + + Reset restores the active preset. Recover reopens the shell regions. + + + ); + } + + return ( + dispatch({ type: 'recoverShellLayout' })} + onResetLayout={() => dispatch({ type: 'resetActiveLayout' })} + /> + ); +}; + +const LayoutButtons = ({ + onRecoverLayout, + onResetLayout, +}: { + onRecoverLayout: () => void; + onResetLayout: () => void; +}) => ( + + + + +); diff --git a/invokeai/frontend/webv2/src/workbench/widgets/layout-actions/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/layout-actions/index.ts new file mode 100644 index 00000000000..7352e40c6f3 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/layout-actions/index.ts @@ -0,0 +1,13 @@ +import type { WidgetManifest } from '../../types'; +import { LayoutActionsWidgetView } from './LayoutActionsWidgetView'; + +export const layoutActionsWidgetManifest: WidgetManifest = { + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + icon: 'lucide-react:panel-bottom', + id: 'layout-actions', + label: 'Layout Actions', + labelText: 'Layout Actions', + regions: ['bottom'], + version: 1, + view: LayoutActionsWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/queue/QueueWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/queue/QueueWidgetView.tsx new file mode 100644 index 00000000000..66894086a50 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/queue/QueueWidgetView.tsx @@ -0,0 +1,51 @@ +import { Stack, Text } from '@chakra-ui/react'; +import { PiListNumbersBold } from 'react-icons/pi'; + +import { StatusWidgetChip, WidgetPanelFrame } from '../../components/WidgetFrames'; +import { WidgetFailureBoundary } from '../../components/WidgetFailureBoundary'; +import type { WidgetViewProps } from '../../types'; +import { useWorkbench } from '../../WorkbenchContext'; + +export const QueueWidgetView = ({ presentation, region }: WidgetViewProps) => { + const { activeProject } = useWorkbench(); + const pendingQueueCount = activeProject.queue.items.filter((item) => item.status === 'pending').length; + + if (region === 'bottom' && presentation !== 'expanded') { + return {pendingQueueCount} queued; + } + + if (region === 'bottom') { + return ; + } + + return ( + + + + Queue + + + + + ); +}; + +const QueueContents = () => { + const { activeProject } = useWorkbench(); + + return ( + + {activeProject.queue.items.length === 0 ? ( + + Queue submissions will appear here. + + ) : ( + activeProject.queue.items.map((item) => ( + + {item.id}: {item.status} + + )) + )} + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/queue/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/queue/index.ts new file mode 100644 index 00000000000..1541b8465a6 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/queue/index.ts @@ -0,0 +1,13 @@ +import type { WidgetManifest } from '../../types'; +import { QueueWidgetView } from './QueueWidgetView'; + +export const queueWidgetManifest: WidgetManifest = { + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + icon: 'lucide-react:list-ordered', + id: 'queue', + label: 'Queue', + labelText: 'Queue', + regions: ['right', 'bottom'], + version: 1, + view: QueueWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/server-status/ServerStatusWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/server-status/ServerStatusWidgetView.tsx new file mode 100644 index 00000000000..1a703862c3e --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/server-status/ServerStatusWidgetView.tsx @@ -0,0 +1,22 @@ +import { Stack, Text } from '@chakra-ui/react'; +import { PiPlugsConnectedBold } from 'react-icons/pi'; + +import { StatusWidgetChip } from '../../components/WidgetFrames'; +import type { WidgetViewProps } from '../../types'; + +export const ServerStatusWidgetView = ({ presentation }: WidgetViewProps) => { + if (presentation === 'tooltip') { + return ( + + + Server Status + + + Connected to Server. Detailed socket and API health will render here once backend status data is connected. + + + ); + } + + return Connected to Server; +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/server-status/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/server-status/index.ts new file mode 100644 index 00000000000..fbe30bdcd7c --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/server-status/index.ts @@ -0,0 +1,14 @@ +import type { WidgetManifest } from '../../types'; +import { ServerStatusWidgetView } from './ServerStatusWidgetView'; + +export const serverStatusWidgetManifest: WidgetManifest = { + bottomPanel: 'tooltip', + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + icon: 'lucide-react:plug-zap', + id: 'server-status', + label: 'Server Status', + labelText: 'Server Status', + regions: ['bottom'], + version: 1, + view: ServerStatusWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/version-status/VersionStatusWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/version-status/VersionStatusWidgetView.tsx new file mode 100644 index 00000000000..933276fb595 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/version-status/VersionStatusWidgetView.tsx @@ -0,0 +1,22 @@ +import { Stack, Text } from '@chakra-ui/react'; +import { PiInfoBold } from 'react-icons/pi'; + +import { StatusWidgetChip } from '../../components/WidgetFrames'; +import type { WidgetViewProps } from '../../types'; + +export const VersionStatusWidgetView = ({ presentation }: WidgetViewProps) => { + if (presentation === 'tooltip') { + return ( + + + Version + + + Invoke V7 shell version 7.0. + + + ); + } + + return Version 7.0; +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/version-status/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/version-status/index.ts new file mode 100644 index 00000000000..fd50ef672a6 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/version-status/index.ts @@ -0,0 +1,14 @@ +import type { WidgetManifest } from '../../types'; +import { VersionStatusWidgetView } from './VersionStatusWidgetView'; + +export const versionStatusWidgetManifest: WidgetManifest = { + bottomPanel: 'tooltip', + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + icon: 'lucide-react:info', + id: 'version-status', + label: 'Version', + labelText: 'Version', + regions: ['bottom'], + version: 1, + view: VersionStatusWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/workflow/WorkflowWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/workflow/WorkflowWidgetView.tsx new file mode 100644 index 00000000000..7628f9604f3 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/workflow/WorkflowWidgetView.tsx @@ -0,0 +1,33 @@ +import { Flex, Text } from '@chakra-ui/react'; + +import { GraphBearingWidgetHeader, WidgetPanelFrame } from '../../components/WidgetFrames'; +import { WidgetFailureBoundary } from '../../components/WidgetFailureBoundary'; +import type { WidgetViewProps } from '../../types'; + +export const WorkflowWidgetView = ({ manifest, region }: WidgetViewProps) => { + if (region === 'left') { + return ( + + + + + Workflow controls will render here when this widget is mounted into the left panel. + + + + ); + } + + return ( + + + + + + + Workflow view + + + + ); +}; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/workflow/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/workflow/index.ts new file mode 100644 index 00000000000..57072698473 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/workflow/index.ts @@ -0,0 +1,18 @@ +import type { WidgetManifest } from '../../types'; +import { WorkflowWidgetView } from './WorkflowWidgetView'; + +export const workflowWidgetManifest: WidgetManifest = { + failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, + graphBearing: { + defaultGraphId: 'workflow-graph', + sourceId: 'project-graph', + surfaces: ['center', 'left'], + }, + icon: 'lucide-react:workflow', + id: 'workflow', + label: 'Workflow', + labelText: 'Workflow', + regions: ['center', 'left'], + version: 1, + view: WorkflowWidgetView, +}; diff --git a/invokeai/frontend/webv2/src/workbench/workbenchState.ts b/invokeai/frontend/webv2/src/workbench/workbenchState.ts new file mode 100644 index 00000000000..22c03029f6a --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/workbenchState.ts @@ -0,0 +1,750 @@ +import { + defaultInvocationRoute, + isInvocationRouteValid, + isInvocationSourceAvailable, + resolveInvocationRoute, +} from './invocation'; +import { defaultLayoutPreset, getLayoutPreset } from './layoutPresets'; +import type { + CanvasStateContract, + CenterViewId, + GraphContract, + GraphHistorySnapshot, + InvocationRoute, + InvocationSourceId, + LayoutPresetId, + Project, + ProjectLayoutState, + ProjectUndoSnapshot, + QueueItem, + ResultDestination, + WidgetFailure, + WidgetId, + WidgetRegion, + WidgetRegionState, + WidgetStateContract, + WorkbenchState, +} from './types'; + +type WorkbenchAction = + | { type: 'createProject' } + | { type: 'closeProject'; projectId: string } + | { type: 'switchProject'; projectId: string } + | { type: 'setCenterView'; centerViewId: CenterViewId } + | { type: 'applyPreset'; presetId: LayoutPresetId } + | { type: 'resetActiveLayout' } + | { type: 'recoverShellLayout' } + | { type: 'setInvocationSource'; sourceId: InvocationSourceId } + | { type: 'setInvocationDestination'; destination: ResultDestination } + | { type: 'toggleSourceLock' } + | { type: 'toggleDestinationLock' } + | { type: 'selectRegionWidget'; region: WidgetRegion; widgetId: WidgetId } + | { type: 'toggleRegionWidget'; region: WidgetRegion; widgetId: WidgetId } + | { type: 'setRegionWidgetCollapsed'; region: WidgetRegion; isCollapsed: boolean } + | { type: 'setRegionWidgetSize'; region: WidgetRegion; sizePx: number } + | { type: 'submitInvocationSnapshot'; backendSupportsCancellation: boolean } + | { type: 'submitResolvedInvocationSnapshot'; backendSupportsCancellation: boolean; route: InvocationRoute } + | { type: 'cancelQueueItem'; queueItemId: string } + | { type: 'undoProjectChange' } + | { type: 'redoProjectChange' } + | { type: 'hydrateWorkbench'; state: WorkbenchState } + | { type: 'autosaveStarted' } + | { type: 'autosaveSucceeded'; savedAt: string } + | { type: 'autosaveFailed'; error: string } + | { type: 'recordWidgetFailure'; failure: WidgetFailure } + | { type: 'recordError'; message: string }; + +const HISTORY_LIMIT = 40; +const INITIAL_PROJECT_COUNT = 3; +const ERROR_LOG_LIMIT = 5; +const MIN_PANEL_SIZE_PX = 180; +const MAX_PANEL_SIZE_PX = 520; +const MIN_STATUS_PANEL_SIZE_PX = 96; +const MAX_STATUS_PANEL_SIZE_PX = 420; + +const now = (): string => new Date().toISOString(); + +const createId = (prefix: string): string => + `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + +const cloneGraph = (graph: GraphContract): GraphContract => ({ + ...graph, + edges: graph.edges.map((edge) => ({ ...edge })), + nodes: graph.nodes.map((node) => ({ ...node, inputs: { ...node.inputs } })), +}); + +const cloneCanvas = (canvas: CanvasStateContract): CanvasStateContract => ({ + ...canvas, + layers: [...canvas.layers], + stagingArea: { + ...canvas.stagingArea, + pendingImageIds: [...canvas.stagingArea.pendingImageIds], + }, +}); + +const cloneWidgetState = (widgetState: WidgetStateContract): WidgetStateContract => ({ + ...widgetState, + values: { ...widgetState.values }, +}); + +const cloneWidgetStates = ( + widgetStates: Record +): Record => ({ + 'autosave-status': cloneWidgetState(widgetStates['autosave-status']), + canvas: cloneWidgetState(widgetStates.canvas), + gallery: cloneWidgetState(widgetStates.gallery), + generate: cloneWidgetState(widgetStates.generate), + 'history-controls': cloneWidgetState(widgetStates['history-controls']), + 'layout-actions': cloneWidgetState(widgetStates['layout-actions']), + layers: cloneWidgetState(widgetStates.layers), + queue: cloneWidgetState(widgetStates.queue), + 'server-status': cloneWidgetState(widgetStates['server-status']), + 'version-status': cloneWidgetState(widgetStates['version-status']), + workflow: cloneWidgetState(widgetStates.workflow), +}); + +const cloneWidgetRegions = ( + widgetRegions: Record +): Record => ({ + center: { + ...widgetRegions.center, + enabledWidgetIds: [...widgetRegions.center.enabledWidgetIds], + }, + left: { + ...widgetRegions.left, + enabledWidgetIds: [...widgetRegions.left.enabledWidgetIds], + }, + right: { + ...widgetRegions.right, + enabledWidgetIds: [...widgetRegions.right.enabledWidgetIds], + }, + bottom: { + ...widgetRegions.bottom, + enabledWidgetIds: [...widgetRegions.bottom.enabledWidgetIds], + }, +}); + +const cloneWidgetGraphs = (widgetGraphs: Project['widgetGraphs']): Project['widgetGraphs'] => + Object.fromEntries(Object.entries(widgetGraphs).map(([key, graph]) => [key, graph ? cloneGraph(graph) : graph])); + +const createUndoSnapshot = (project: Project): ProjectUndoSnapshot => ({ + canvas: cloneCanvas(project.canvas), + invocation: { ...project.invocation }, + layout: { ...project.layout, panels: { ...project.layout.panels } }, + projectGraph: cloneGraph(project.projectGraph), + widgetGraphs: cloneWidgetGraphs(project.widgetGraphs), + widgetRegions: cloneWidgetRegions(project.widgetRegions), + widgetStates: cloneWidgetStates(project.widgetStates), +}); + +const restoreUndoSnapshot = (project: Project, snapshot: ProjectUndoSnapshot): Project => ({ + ...project, + canvas: cloneCanvas(snapshot.canvas), + invocation: { ...snapshot.invocation }, + layout: { ...snapshot.layout, panels: { ...snapshot.layout.panels } }, + projectGraph: cloneGraph(snapshot.projectGraph), + widgetGraphs: cloneWidgetGraphs(snapshot.widgetGraphs), + widgetRegions: cloneWidgetRegions(snapshot.widgetRegions), + widgetStates: cloneWidgetStates(snapshot.widgetStates), +}); + +const createGraphHistorySnapshot = (label: string, graph: GraphContract): GraphHistorySnapshot => ({ + createdAt: now(), + graph: cloneGraph(graph), + id: createId('graph-history'), + label, +}); + +const pushUndo = (project: Project, label: string): Project => ({ + ...project, + undoRedo: { + future: [], + past: [ + ...project.undoRedo.past, + { + createdAt: now(), + id: createId('undo'), + label, + project: createUndoSnapshot(project), + }, + ].slice(-HISTORY_LIMIT), + }, +}); + +const createProjectGraph = (index: number, projectId: string): GraphContract => ({ + edges: [], + id: `${projectId}-graph`, + label: `Project ${index} Graph`, + nodes: [], + updatedAt: now(), + version: 1, +}); + +const createWidgetStates = (): Record => ({ + 'autosave-status': { id: 'autosave-status', label: 'Autosave', values: {}, version: 1 }, + canvas: { id: 'canvas', label: 'Canvas', values: {}, version: 1 }, + gallery: { id: 'gallery', label: 'Gallery', values: {}, version: 1 }, + generate: { graphId: 'generate-graph', id: 'generate', label: 'Generate', values: {}, version: 1 }, + 'history-controls': { id: 'history-controls', label: 'History Controls', values: {}, version: 1 }, + 'layout-actions': { id: 'layout-actions', label: 'Layout Actions', values: {}, version: 1 }, + layers: { id: 'layers', label: 'Layers', values: {}, version: 1 }, + queue: { id: 'queue', label: 'Queue', values: {}, version: 1 }, + 'server-status': { id: 'server-status', label: 'Server Status', values: {}, version: 1 }, + 'version-status': { id: 'version-status', label: 'Version', values: {}, version: 1 }, + workflow: { graphId: 'workflow-graph', id: 'workflow', label: 'Workflow', values: {}, version: 1 }, +}); + +const createWidgetRegions = (): Record => ({ + left: { + activeWidgetId: 'generate', + enabledWidgetIds: ['generate', 'workflow'], + isCollapsed: false, + sizePx: 288, + }, + right: { + activeWidgetId: 'layers', + enabledWidgetIds: ['queue', 'gallery', 'layers'], + isCollapsed: false, + sizePx: 240, + }, + bottom: { + activeWidgetId: 'queue', + enabledWidgetIds: [ + 'server-status', + 'queue', + 'gallery', + 'autosave-status', + 'history-controls', + 'layout-actions', + 'version-status', + ], + isCollapsed: true, + sizePx: 180, + }, + center: { + activeWidgetId: 'canvas', + enabledWidgetIds: ['canvas', 'gallery', 'workflow'], + isCollapsed: false, + sizePx: 0, + }, +}); + +const getCenterWidgetIdFromViewId = (centerViewId: CenterViewId): WidgetId => + centerViewId === 'preview' ? 'gallery' : centerViewId; + +const ensureCenterRegion = ( + centerRegion: WidgetRegionState | undefined, + fallbackCenterViewId: CenterViewId +): WidgetRegionState => { + const defaultCenterRegion = createWidgetRegions().center; + const activeWidgetId = centerRegion?.activeWidgetId ?? getCenterWidgetIdFromViewId(fallbackCenterViewId); + const enabledWidgetIds = centerRegion?.enabledWidgetIds.length + ? centerRegion.enabledWidgetIds + : defaultCenterRegion.enabledWidgetIds; + const normalizedActiveWidgetId = enabledWidgetIds.includes(activeWidgetId) ? activeWidgetId : enabledWidgetIds[0]; + + return { + ...defaultCenterRegion, + ...centerRegion, + activeWidgetId: normalizedActiveWidgetId, + enabledWidgetIds, + isCollapsed: false, + }; +}; + +const ensureProjectWidgetContracts = (project: Project): Project => { + const defaultWidgetRegions = createWidgetRegions(); + const defaultWidgetStates = createWidgetStates(); + const legacyWidgetRegions = project.widgetRegions as + | Partial> + | undefined; + + return { + ...project, + widgetRegions: { + left: legacyWidgetRegions?.left ?? legacyWidgetRegions?.['left-panel'] ?? defaultWidgetRegions.left, + right: legacyWidgetRegions?.right ?? legacyWidgetRegions?.['right-panel'] ?? defaultWidgetRegions.right, + bottom: legacyWidgetRegions?.bottom ?? legacyWidgetRegions?.['status-bar'] ?? defaultWidgetRegions.bottom, + center: ensureCenterRegion(legacyWidgetRegions?.center, project.layout.centerViewId), + }, + widgetStates: { + ...defaultWidgetStates, + ...project.widgetStates, + }, + }; +}; + +const clampPanelSize = (region: WidgetRegion, sizePx: number): number => { + if (region === 'bottom') { + return Math.min(MAX_STATUS_PANEL_SIZE_PX, Math.max(MIN_STATUS_PANEL_SIZE_PX, sizePx)); + } + + return Math.min(MAX_PANEL_SIZE_PX, Math.max(MIN_PANEL_SIZE_PX, sizePx)); +}; + +const createCanvasState = (): CanvasStateContract => ({ + layers: [], + stagingArea: { pendingImageIds: [] }, + version: 1, +}); + +const createProject = (index: number, id = `project-${index}`): Project => ({ + canvas: createCanvasState(), + events: [ + { + createdAt: now(), + id: createId('event'), + summary: `Created Project Name #${index}`, + type: 'project-created', + }, + ], + graphHistory: [], + id, + invocation: { ...defaultInvocationRoute }, + layout: { ...defaultLayoutPreset.initialLayout, panels: { ...defaultLayoutPreset.initialLayout.panels } }, + name: `Project Name #${index}`, + projectGraph: createProjectGraph(index, id), + queue: { items: [] }, + undoRedo: { future: [], past: [] }, + widgetGraphs: {}, + widgetRegions: createWidgetRegions(), + widgetStates: createWidgetStates(), +}); + +const getNextProjectIndex = (projects: Project[]): number => { + const usedIndices = projects.map((project) => Number(project.name.match(/#(\d+)$/)?.[1] ?? 0)); + + return Math.max(0, ...usedIndices) + 1; +}; + +const updateActiveProject = (state: WorkbenchState, getProject: (project: Project) => Project): WorkbenchState => ({ + ...state, + projects: state.projects.map((project) => + project.id === state.activeProjectId ? getProject(ensureProjectWidgetContracts(project)) : project + ), +}); + +const getNextEnabledWidgetId = (region: WidgetRegionState, widgetId: WidgetId): WidgetId | null => { + if (region.activeWidgetId !== widgetId) { + return region.activeWidgetId; + } + + return region.enabledWidgetIds.find((enabledWidgetId) => enabledWidgetId !== widgetId) ?? null; +}; + +const updateActiveWidgetRegion = ( + state: WorkbenchState, + region: WidgetRegion, + getRegion: (regionState: WidgetRegionState) => WidgetRegionState +): WorkbenchState => + updateActiveProject(state, (project) => ({ + ...project, + widgetRegions: { + ...project.widgetRegions, + [region]: getRegion(project.widgetRegions[region]), + }, + })); + +const openPanelForRegion = (layout: ProjectLayoutState, region: WidgetRegion): ProjectLayoutState => ({ + ...layout, + panels: { + ...layout.panels, + isBottomOpen: region === 'bottom' ? true : layout.panels.isBottomOpen, + isLeftOpen: region === 'left' ? true : layout.panels.isLeftOpen, + isRightOpen: region === 'right' ? true : layout.panels.isRightOpen, + }, +}); + +const normalizeWorkbenchState = (state: WorkbenchState): WorkbenchState => ({ + ...state, + projects: state.projects.map(ensureProjectWidgetContracts), +}); + +const updateActiveLayout = ( + state: WorkbenchState, + getLayout: (layout: ProjectLayoutState) => ProjectLayoutState +): WorkbenchState => + updateActiveProject(state, (project) => { + const nextProject = pushUndo(project, 'Update layout'); + + return { + ...nextProject, + events: [ + { + createdAt: now(), + id: createId('event'), + summary: 'Updated active layout', + type: 'layout-updated', + }, + ...nextProject.events, + ], + layout: getLayout(project.layout), + }; + }); + +const updateActiveInvocation = ( + state: WorkbenchState, + getInvocation: (invocation: InvocationRoute) => InvocationRoute +): WorkbenchState => + updateActiveProject(state, (project) => { + const nextProject = pushUndo(project, 'Update invocation route'); + + return { + ...nextProject, + events: [ + { + createdAt: now(), + id: createId('event'), + summary: 'Updated invocation source or destination', + type: 'invocation-updated', + }, + ...nextProject.events, + ], + invocation: getInvocation(project.invocation), + }; + }); + +const selectGraphForInvocation = (project: Project, invocation: InvocationRoute): GraphContract => { + const widgetGraph = project.widgetGraphs[invocation.sourceId as WidgetId]; + + return widgetGraph ? cloneGraph(widgetGraph) : cloneGraph(project.projectGraph); +}; + +const submitInvocationSnapshot = ( + project: Project, + backendSupportsCancellation: boolean, + route = resolveInvocationRoute(project) +): Project => { + if (!isInvocationRouteValid(route)) { + return project; + } + + const submittedAt = now(); + const queueItemId = createId('queue-item'); + const graph = selectGraphForInvocation(project, route); + const graphHistorySnapshot = createGraphHistorySnapshot(`Queue snapshot ${queueItemId}`, graph); + const queueItem: QueueItem = { + cancellable: backendSupportsCancellation, + id: queueItemId, + snapshot: { + canvas: cloneCanvas(project.canvas), + destination: route.destination, + graph, + sourceId: route.sourceId, + submittedAt, + widgetStates: cloneWidgetStates(project.widgetStates), + }, + status: 'pending', + }; + + return { + ...project, + events: [ + { + createdAt: submittedAt, + id: createId('event'), + runId: queueItemId, + summary: `Submitted immutable ${route.sourceId} graph snapshot to ${route.destination}`, + type: 'queue-submitted', + }, + ...project.events, + ], + graphHistory: [graphHistorySnapshot, ...project.graphHistory].slice(0, HISTORY_LIMIT), + invocation: { + ...project.invocation, + destination: route.destination, + lastSubmittedRunId: queueItemId, + sourceId: route.sourceId, + }, + queue: { items: [queueItem, ...project.queue.items] }, + }; +}; + +export const createInitialWorkbenchState = (): WorkbenchState => ({ + account: { activeLayoutPresetId: 'canvas-default' }, + activeProjectId: 'project-1', + autosave: { status: 'idle' }, + errorLog: [], + projects: Array.from({ length: INITIAL_PROJECT_COUNT }, (_value, index) => createProject(index + 1)), + widgetFailures: [], +}); + +export const workbenchReducer = (state: WorkbenchState, action: WorkbenchAction): WorkbenchState => { + switch (action.type) { + case 'createProject': { + const project = createProject(getNextProjectIndex(state.projects), createId('project')); + + return { ...state, activeProjectId: project.id, projects: [...state.projects, project] }; + } + case 'closeProject': { + if (state.projects.length === 1) { + return { + ...state, + errorLog: ['At least one project must remain open.', ...state.errorLog], + }; + } + + const projectIndex = state.projects.findIndex((project) => project.id === action.projectId); + const projects = state.projects.filter((project) => project.id !== action.projectId); + + if (action.projectId !== state.activeProjectId) { + return { ...state, projects }; + } + + const fallbackProject = projects[Math.max(0, projectIndex - 1)]; + + return { ...state, activeProjectId: fallbackProject.id, projects }; + } + case 'switchProject': { + return { ...state, activeProjectId: action.projectId }; + } + case 'setCenterView': { + const widgetId = getCenterWidgetIdFromViewId(action.centerViewId); + + return updateActiveWidgetRegion(state, 'center', (region) => ({ + ...region, + activeWidgetId: region.enabledWidgetIds.includes(widgetId) ? widgetId : region.activeWidgetId, + isCollapsed: false, + })); + } + case 'applyPreset': { + const preset = getLayoutPreset(action.presetId); + const widgetId = getCenterWidgetIdFromViewId(preset.initialLayout.centerViewId); + const nextState = updateActiveLayout(state, () => ({ + ...preset.initialLayout, + panels: { ...preset.initialLayout.panels }, + })); + + return { + ...updateActiveWidgetRegion(nextState, 'center', (region) => ({ + ...region, + activeWidgetId: region.enabledWidgetIds.includes(widgetId) ? widgetId : region.activeWidgetId, + isCollapsed: false, + })), + account: { activeLayoutPresetId: action.presetId }, + }; + } + case 'resetActiveLayout': { + const preset = getLayoutPreset( + state.projects.find((project) => project.id === state.activeProjectId)?.layout.presetId ?? + state.account.activeLayoutPresetId + ); + const widgetId = getCenterWidgetIdFromViewId(preset.initialLayout.centerViewId); + const nextState = updateActiveLayout(state, () => { + return { ...preset.initialLayout, panels: { ...preset.initialLayout.panels } }; + }); + + return updateActiveWidgetRegion(nextState, 'center', (region) => ({ + ...region, + activeWidgetId: region.enabledWidgetIds.includes(widgetId) ? widgetId : region.activeWidgetId, + isCollapsed: false, + })); + } + case 'recoverShellLayout': { + return updateActiveLayout(state, (layout) => ({ + ...layout, + panels: { isLeftOpen: true, isRightOpen: true, isBottomOpen: true }, + })); + } + case 'setInvocationSource': { + if (!isInvocationSourceAvailable(action.sourceId)) { + return state; + } + + return updateActiveInvocation(state, (invocation) => ({ ...invocation, sourceId: action.sourceId })); + } + case 'setInvocationDestination': { + return updateActiveInvocation(state, (invocation) => ({ ...invocation, destination: action.destination })); + } + case 'toggleSourceLock': { + return updateActiveInvocation(state, (invocation) => ({ ...invocation, sourceLocked: !invocation.sourceLocked })); + } + case 'toggleDestinationLock': { + return updateActiveInvocation(state, (invocation) => ({ + ...invocation, + destinationLocked: !invocation.destinationLocked, + })); + } + case 'selectRegionWidget': { + return updateActiveProject(state, (project) => { + const region = project.widgetRegions[action.region]; + + if (action.region === 'center') { + return { + ...project, + widgetRegions: { + ...project.widgetRegions, + center: { ...region, activeWidgetId: action.widgetId, isCollapsed: false }, + }, + }; + } + + const widgetRegion = + region.activeWidgetId === action.widgetId + ? { ...region, isCollapsed: !region.isCollapsed } + : { ...region, activeWidgetId: action.widgetId, isCollapsed: false }; + + return { + ...project, + layout: openPanelForRegion(project.layout, action.region), + widgetRegions: { ...project.widgetRegions, [action.region]: widgetRegion }, + }; + }); + } + case 'toggleRegionWidget': { + return updateActiveWidgetRegion(state, action.region, (region) => { + const isEnabled = region.enabledWidgetIds.includes(action.widgetId); + + if (action.region === 'center' && isEnabled && region.enabledWidgetIds.length === 1) { + return region; + } + + const enabledWidgetIds = isEnabled + ? region.enabledWidgetIds.filter((widgetId) => widgetId !== action.widgetId) + : [...region.enabledWidgetIds, action.widgetId]; + const fallbackWidgetId = getNextEnabledWidgetId(region, action.widgetId); + + return { + ...region, + activeWidgetId: isEnabled && fallbackWidgetId ? fallbackWidgetId : action.widgetId, + enabledWidgetIds, + isCollapsed: action.region === 'center' ? false : enabledWidgetIds.length === 0 ? true : region.isCollapsed, + }; + }); + } + case 'setRegionWidgetCollapsed': { + if (action.region === 'center') { + return state; + } + + return updateActiveWidgetRegion(state, action.region, (region) => ({ + ...region, + isCollapsed: action.isCollapsed, + })); + } + case 'setRegionWidgetSize': { + return updateActiveWidgetRegion(state, action.region, (region) => ({ + ...region, + sizePx: clampPanelSize(action.region, action.sizePx), + })); + } + case 'submitInvocationSnapshot': { + return updateActiveProject(state, (project) => + submitInvocationSnapshot(project, action.backendSupportsCancellation) + ); + } + case 'submitResolvedInvocationSnapshot': { + return updateActiveProject(state, (project) => + submitInvocationSnapshot( + project, + action.backendSupportsCancellation, + resolveInvocationRoute(project, 'global', action.route) + ) + ); + } + case 'cancelQueueItem': { + return updateActiveProject(state, (project) => ({ + ...project, + queue: { + items: project.queue.items.map((item) => { + if (item.id !== action.queueItemId || !item.cancellable || item.status !== 'pending') { + return item; + } + + return { ...item, status: 'cancelled' }; + }), + }, + })); + } + case 'undoProjectChange': { + return updateActiveProject(state, (project) => { + const undoEntry = project.undoRedo.past.at(-1); + + if (!undoEntry) { + return project; + } + + const restoredProject = restoreUndoSnapshot(project, undoEntry.project); + + return { + ...restoredProject, + events: project.events, + graphHistory: project.graphHistory, + queue: project.queue, + undoRedo: { + future: [ + { + createdAt: now(), + id: createId('redo'), + label: undoEntry.label, + project: createUndoSnapshot(project), + }, + ...project.undoRedo.future, + ].slice(0, HISTORY_LIMIT), + past: project.undoRedo.past.slice(0, -1), + }, + }; + }); + } + case 'redoProjectChange': { + return updateActiveProject(state, (project) => { + const redoEntry = project.undoRedo.future[0]; + + if (!redoEntry) { + return project; + } + + const restoredProject = restoreUndoSnapshot(project, redoEntry.project); + + return { + ...restoredProject, + events: project.events, + graphHistory: project.graphHistory, + queue: project.queue, + undoRedo: { + future: project.undoRedo.future.slice(1), + past: [ + ...project.undoRedo.past, + { + createdAt: now(), + id: createId('undo'), + label: redoEntry.label, + project: createUndoSnapshot(project), + }, + ].slice(-HISTORY_LIMIT), + }, + }; + }); + } + case 'hydrateWorkbench': { + return normalizeWorkbenchState(action.state); + } + case 'autosaveStarted': { + return { ...state, autosave: { status: 'saving' } }; + } + case 'autosaveSucceeded': { + return { ...state, autosave: { lastSavedAt: action.savedAt, status: 'saved' } }; + } + case 'autosaveFailed': { + return { ...state, autosave: { error: action.error, status: 'error' } }; + } + case 'recordWidgetFailure': { + const hasFailure = state.widgetFailures.some((failure) => failure.widgetId === action.failure.widgetId); + + if (hasFailure) { + return state; + } + + return { + ...state, + errorLog: [action.failure.details, ...state.errorLog].slice(0, ERROR_LOG_LIMIT), + widgetFailures: [action.failure, ...state.widgetFailures], + }; + } + case 'recordError': { + return { ...state, errorLog: [action.message, ...state.errorLog].slice(0, ERROR_LOG_LIMIT) }; + } + } +}; + +export type { WorkbenchAction }; diff --git a/invokeai/frontend/webv2/tsconfig.json b/invokeai/frontend/webv2/tsconfig.json new file mode 100644 index 00000000000..782609aeafc --- /dev/null +++ b/invokeai/frontend/webv2/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/invokeai/frontend/webv2/tsconfig.node.json b/invokeai/frontend/webv2/tsconfig.node.json new file mode 100644 index 00000000000..9a861f6a437 --- /dev/null +++ b/invokeai/frontend/webv2/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.mts"] +} diff --git a/invokeai/frontend/webv2/vite.config.mts b/invokeai/frontend/webv2/vite.config.mts new file mode 100644 index 00000000000..42762231ea4 --- /dev/null +++ b/invokeai/frontend/webv2/vite.config.mts @@ -0,0 +1,32 @@ +import react from '@vitejs/plugin-react-swc'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', + build: { + rollupOptions: { + output: { + manualChunks(id) { + if (!id.includes('/node_modules/')) { + return undefined; + } + + if (id.includes('/node_modules/react-icons/')) { + return 'react-icons'; + } + + if (id.includes('/node_modules/@chakra-ui/') || id.includes('/node_modules/@emotion/')) { + return 'chakra'; + } + + return 'vendor'; + }, + }, + }, + }, + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5174, + }, +}); From 388d733df160bcd807c1c7085cc693183028659c Mon Sep 17 00:00:00 2001 From: joshistoast Date: Tue, 9 Jun 2026 23:36:19 -0600 Subject: [PATCH 002/153] docs(webv2): document workbench phase contracts and oxc rules --- .../frontend/webv2/OXC_RULE_COMPATIBILITY.md | 23 ++++++++++ .../src/workbench/PHASE_2_STATE_CONTRACTS.md | 38 ++++++++++++++++ .../src/workbench/PHASE_3_WIDGET_REGISTRY.md | 43 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 invokeai/frontend/webv2/OXC_RULE_COMPATIBILITY.md create mode 100644 invokeai/frontend/webv2/src/workbench/PHASE_2_STATE_CONTRACTS.md create mode 100644 invokeai/frontend/webv2/src/workbench/PHASE_3_WIDGET_REGISTRY.md 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/src/workbench/PHASE_2_STATE_CONTRACTS.md b/invokeai/frontend/webv2/src/workbench/PHASE_2_STATE_CONTRACTS.md new file mode 100644 index 00000000000..4a5e9ef675e --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/PHASE_2_STATE_CONTRACTS.md @@ -0,0 +1,38 @@ +# Phase 2 State Contracts + +Phase 2 locks down ownership boundaries before feature migration. + +## Ownership + +- `WorkbenchState.account` owns global user/workbench preferences such as the active layout preset registry selection. +- `WorkbenchState.autosave` owns persistence status only; it is excluded from the autosave comparison key to avoid save loops. +- `Project.layout` owns the active project layout. Layout presets are copied into the project instead of referenced as live global state. +- `Project.invocation` owns source, destination, and lock state for the global Invoke control. +- `Project.projectGraph` owns the primary project graph. +- `Project.widgetStates` owns widget-local state values. +- `Project.widgetGraphs` owns graph-bearing widget graphs that can be selected as invocation sources. +- `Project.canvas` owns canvas layers and staging-area state. +- `Project.graphHistory` owns immutable graph snapshots. +- `Project.undoRedo` owns undo/redo snapshots for project and widget state updates. +- `Project.queue` owns immutable queue submission records for the active project. +- `Project.events` owns internal timeline primitives. Conversation and Run Journal UI remain deferred. + +## Persistence + +`WorkbenchPersistenceService` defines the persistence boundary. The initial implementation uses `localStorage` for autosave, but feature code should depend on the service interface rather than direct storage calls. + +## Undo/Redo Policy + +Undoable project changes push a `ProjectUndoSnapshot` before mutation and clear redo history. Queue submissions are not undoable because they represent immutable external work requests. + +## Queue Snapshot Policy + +Clicking Invoke creates a `QueueItem` with a frozen-by-copy `QueueSubmissionSnapshot`: + +- resolved source and destination +- selected graph copy +- widget state copy +- canvas state copy +- submission timestamp + +Backend cancellation is represented per item through `QueueItem.cancellable`, allowing later API integration to expose cancellation only when supported. diff --git a/invokeai/frontend/webv2/src/workbench/PHASE_3_WIDGET_REGISTRY.md b/invokeai/frontend/webv2/src/workbench/PHASE_3_WIDGET_REGISTRY.md new file mode 100644 index 00000000000..ef5d549ed16 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/PHASE_3_WIDGET_REGISTRY.md @@ -0,0 +1,43 @@ +# Phase 3 Widget Registry + +Phase 3 establishes the registration and graph-bearing surface contracts. + +## Widget Manifest + +`WidgetManifest` declares: + +- stable widget id and label +- manifest version +- allowed workbench regions +- icon key +- availability policy: enabled, disabled, or hidden +- optional graph-bearing source metadata +- registration/render failure behavior +- third-party readiness metadata, with third-party enablement fixed to `false` for MVP + +## Registration Flow + +Each widget owns a directory under `src/workbench/widgets//` with an `index.ts` that exports its manifest. If the widget renders UI, the manifest owns a single `view` component. `widgetRegistry.ts` imports those manifests into the first-party list, validates them, and returns `RegisteredWidget` records. Invalid manifests follow their failure policy and become disabled or hidden without taking down the workbench. + +Widget directories own their renderable surfaces. A widget that can render in a panel, center view, dialog, or popover provides one manifest view and receives the target region as a prop. Multi-surface widgets branch inside that one view instead of exporting one view per surface. + +## Regions + +Allowed regions are `left-panel`, `right-panel`, `center-view`, `dialog`, `popover`, and `status-bar`. + +The widget rails are derived from `getWidgetsForRegion()` instead of hardcoded local lists. + +## Graph-Bearing Surfaces + +`GraphBearingSurfaceContract` is resolved through `getGraphBearingSurface(widgetId, region)`. + +All graph-bearing surfaces expose the same actions through `GraphSurfaceActions`: + +- `Set Source`, disabled when already active +- `View Graph`, opening a read-only `GraphPreviewDialog` shell + +The current first-party graph-bearing surfaces are Generate, Canvas, and Workflow. + +## Failure Isolation + +`WidgetFailureBoundary` isolates render failures at widget region boundaries and provides copyable error details. Registration failures are recorded in `WorkbenchState.widgetFailures` and also shown through the shell error surface. From 7e78034a152e86ff857b81c21bfcf222529451fd Mon Sep 17 00:00:00 2001 From: joshistoast Date: Tue, 9 Jun 2026 23:37:15 -0600 Subject: [PATCH 003/153] chore(webv2): remove phase planning notes --- .../src/workbench/PHASE_2_STATE_CONTRACTS.md | 38 ---------------- .../src/workbench/PHASE_3_WIDGET_REGISTRY.md | 43 ------------------- 2 files changed, 81 deletions(-) delete mode 100644 invokeai/frontend/webv2/src/workbench/PHASE_2_STATE_CONTRACTS.md delete mode 100644 invokeai/frontend/webv2/src/workbench/PHASE_3_WIDGET_REGISTRY.md diff --git a/invokeai/frontend/webv2/src/workbench/PHASE_2_STATE_CONTRACTS.md b/invokeai/frontend/webv2/src/workbench/PHASE_2_STATE_CONTRACTS.md deleted file mode 100644 index 4a5e9ef675e..00000000000 --- a/invokeai/frontend/webv2/src/workbench/PHASE_2_STATE_CONTRACTS.md +++ /dev/null @@ -1,38 +0,0 @@ -# Phase 2 State Contracts - -Phase 2 locks down ownership boundaries before feature migration. - -## Ownership - -- `WorkbenchState.account` owns global user/workbench preferences such as the active layout preset registry selection. -- `WorkbenchState.autosave` owns persistence status only; it is excluded from the autosave comparison key to avoid save loops. -- `Project.layout` owns the active project layout. Layout presets are copied into the project instead of referenced as live global state. -- `Project.invocation` owns source, destination, and lock state for the global Invoke control. -- `Project.projectGraph` owns the primary project graph. -- `Project.widgetStates` owns widget-local state values. -- `Project.widgetGraphs` owns graph-bearing widget graphs that can be selected as invocation sources. -- `Project.canvas` owns canvas layers and staging-area state. -- `Project.graphHistory` owns immutable graph snapshots. -- `Project.undoRedo` owns undo/redo snapshots for project and widget state updates. -- `Project.queue` owns immutable queue submission records for the active project. -- `Project.events` owns internal timeline primitives. Conversation and Run Journal UI remain deferred. - -## Persistence - -`WorkbenchPersistenceService` defines the persistence boundary. The initial implementation uses `localStorage` for autosave, but feature code should depend on the service interface rather than direct storage calls. - -## Undo/Redo Policy - -Undoable project changes push a `ProjectUndoSnapshot` before mutation and clear redo history. Queue submissions are not undoable because they represent immutable external work requests. - -## Queue Snapshot Policy - -Clicking Invoke creates a `QueueItem` with a frozen-by-copy `QueueSubmissionSnapshot`: - -- resolved source and destination -- selected graph copy -- widget state copy -- canvas state copy -- submission timestamp - -Backend cancellation is represented per item through `QueueItem.cancellable`, allowing later API integration to expose cancellation only when supported. diff --git a/invokeai/frontend/webv2/src/workbench/PHASE_3_WIDGET_REGISTRY.md b/invokeai/frontend/webv2/src/workbench/PHASE_3_WIDGET_REGISTRY.md deleted file mode 100644 index ef5d549ed16..00000000000 --- a/invokeai/frontend/webv2/src/workbench/PHASE_3_WIDGET_REGISTRY.md +++ /dev/null @@ -1,43 +0,0 @@ -# Phase 3 Widget Registry - -Phase 3 establishes the registration and graph-bearing surface contracts. - -## Widget Manifest - -`WidgetManifest` declares: - -- stable widget id and label -- manifest version -- allowed workbench regions -- icon key -- availability policy: enabled, disabled, or hidden -- optional graph-bearing source metadata -- registration/render failure behavior -- third-party readiness metadata, with third-party enablement fixed to `false` for MVP - -## Registration Flow - -Each widget owns a directory under `src/workbench/widgets//` with an `index.ts` that exports its manifest. If the widget renders UI, the manifest owns a single `view` component. `widgetRegistry.ts` imports those manifests into the first-party list, validates them, and returns `RegisteredWidget` records. Invalid manifests follow their failure policy and become disabled or hidden without taking down the workbench. - -Widget directories own their renderable surfaces. A widget that can render in a panel, center view, dialog, or popover provides one manifest view and receives the target region as a prop. Multi-surface widgets branch inside that one view instead of exporting one view per surface. - -## Regions - -Allowed regions are `left-panel`, `right-panel`, `center-view`, `dialog`, `popover`, and `status-bar`. - -The widget rails are derived from `getWidgetsForRegion()` instead of hardcoded local lists. - -## Graph-Bearing Surfaces - -`GraphBearingSurfaceContract` is resolved through `getGraphBearingSurface(widgetId, region)`. - -All graph-bearing surfaces expose the same actions through `GraphSurfaceActions`: - -- `Set Source`, disabled when already active -- `View Graph`, opening a read-only `GraphPreviewDialog` shell - -The current first-party graph-bearing surfaces are Generate, Canvas, and Workflow. - -## Failure Isolation - -`WidgetFailureBoundary` isolates render failures at widget region boundaries and provides copyable error details. Registration failures are recorded in `WorkbenchState.widgetFailures` and also shown through the shell error surface. From 12b2af687747f4a7bbdf7339dda79cde03909689 Mon Sep 17 00:00:00 2001 From: joshistoast Date: Tue, 9 Jun 2026 23:37:32 -0600 Subject: [PATCH 004/153] test(webv2): add vitest coverage for workbench state --- invokeai/frontend/webv2/package.json | 6 +- invokeai/frontend/webv2/pnpm-lock.yaml | 254 ++++++++ invokeai/frontend/webv2/src/vite-env.d.ts | 1 + .../webv2/src/workbench/persistence.test.ts | 30 + .../widgets/gallery/galleryStateView.test.ts | 48 ++ .../src/workbench/workbenchState.test.ts | 612 ++++++++++++++++++ 6 files changed, 949 insertions(+), 2 deletions(-) create mode 100644 invokeai/frontend/webv2/src/vite-env.d.ts create mode 100644 invokeai/frontend/webv2/src/workbench/persistence.test.ts create mode 100644 invokeai/frontend/webv2/src/workbench/widgets/gallery/galleryStateView.test.ts create mode 100644 invokeai/frontend/webv2/src/workbench/workbenchState.test.ts diff --git a/invokeai/frontend/webv2/package.json b/invokeai/frontend/webv2/package.json index 346764388ea..5000e48d612 100644 --- a/invokeai/frontend/webv2/package.json +++ b/invokeai/frontend/webv2/package.json @@ -14,7 +14,8 @@ "lint:oxc": "oxlint --react-plugin --import-plugin --deny-warnings .", "lint:oxc:fix": "oxlint --react-plugin --import-plugin --fix .", "preview": "vite preview", - "lint:tsc": "tsc --noEmit" + "lint:tsc": "tsc --noEmit", + "test": "vitest run" }, "dependencies": { "@chakra-ui/react": "^3.28.0", @@ -32,7 +33,8 @@ "oxfmt": "^0.54.0", "oxlint": "^1.69.0", "typescript": "^5.8.3", - "vite": "^7.0.5" + "vite": "^7.0.5", + "vitest": "^4.1.8" }, "engines": { "pnpm": "10" diff --git a/invokeai/frontend/webv2/pnpm-lock.yaml b/invokeai/frontend/webv2/pnpm-lock.yaml index 0bb24af1f3f..77cd316882b 100644 --- a/invokeai/frontend/webv2/pnpm-lock.yaml +++ b/invokeai/frontend/webv2/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: vite: specifier: ^7.0.5 version: 7.3.5(@types/node@22.19.20) + vitest: + specifier: ^4.1.8 + version: 4.1.8(@types/node@22.19.20)(vite@7.3.5(@types/node@22.19.20)) packages: @@ -700,6 +703,9 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/core-darwin-arm64@1.15.40': resolution: {integrity: sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==} engines: {node: '>=10'} @@ -790,6 +796,12 @@ packages: '@swc/types@0.1.26': resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -812,6 +824,35 @@ packages: peerDependencies: vite: ^4 || ^5 || ^6 || ^7 + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} + + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} + 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.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} + + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} + + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} + + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + '@zag-js/accordion@1.40.0': resolution: {integrity: sha512-YDdyvZJ6fr92RZazyXQq+juT3ZA0ubjDISptb5YPgMoTPdnjKNiICPpMeCeVj1ncYRDkHXrOdChS/5CtuX/K6g==} @@ -1046,6 +1087,10 @@ packages: '@zag-js/utils@1.40.0': resolution: {integrity: sha512-XUpqDtXfHe7CySjOhLPLj9H8rxbiFUJAGgmBzNdpsGPP4wx12cpOXrpSjRXZ2kMwooMPz/P7RPDBteto8sqhAQ==} + 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'} @@ -1054,9 +1099,16 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + 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==} + cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} @@ -1080,6 +1132,9 @@ packages: 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'} @@ -1089,6 +1144,13 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1146,6 +1208,9 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1154,6 +1219,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + obug@2.1.2: + resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} + engines: {node: '>=12.20.0'} + oxfmt@0.54.0: resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1195,6 +1264,9 @@ packages: 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==} @@ -1249,6 +1321,9 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1257,6 +1332,12 @@ packages: 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==} @@ -1264,6 +1345,13 @@ packages: 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'} @@ -1272,6 +1360,10 @@ packages: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1326,6 +1418,52 @@ packages: yaml: optional: true + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} + 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.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + yaml@1.10.3: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} @@ -1845,6 +1983,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.61.1': optional: true + '@standard-schema/spec@1.1.0': {} + '@swc/core-darwin-arm64@1.15.40': optional: true @@ -1910,6 +2050,13 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.9': {} '@types/node@22.19.20': @@ -1934,6 +2081,47 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' + '@vitest/expect@4.1.8': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.8(vite@7.3.5(@types/node@22.19.20))': + dependencies: + '@vitest/spy': 4.1.8 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.5(@types/node@22.19.20) + + '@vitest/pretty-format@4.1.8': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.8': + dependencies: + '@vitest/utils': 4.1.8 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.8': {} + + '@vitest/utils@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@zag-js/accordion@1.40.0': dependencies: '@zag-js/anatomy': 1.40.0 @@ -2501,6 +2689,8 @@ snapshots: '@zag-js/utils@1.40.0': {} + assertion-error@2.0.1: {} + babel-plugin-macros@3.1.0: dependencies: '@babel/runtime': 7.29.7 @@ -2509,8 +2699,12 @@ snapshots: callsites@3.1.0: {} + chai@6.2.2: {} + convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} + cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 @@ -2531,6 +2725,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@2.1.0: {} + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -2562,6 +2758,12 @@ snapshots: escape-string-regexp@4.0.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.3.0: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -2604,10 +2806,16 @@ snapshots: dependencies: react: 19.2.7 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + ms@2.1.3: {} nanoid@3.3.12: {} + obug@2.1.2: {} + oxfmt@0.54.0: dependencies: tinypool: 2.1.0 @@ -2669,6 +2877,8 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + perfect-freehand@1.2.3: {} picocolors@1.1.1: {} @@ -2742,14 +2952,24 @@ snapshots: scheduler@0.27.0: {} + siginfo@2.0.0: {} + 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) @@ -2757,6 +2977,8 @@ snapshots: tinypool@2.1.0: {} + tinyrainbow@3.1.0: {} + tslib@2.8.1: {} typescript@5.9.3: {} @@ -2777,4 +2999,36 @@ snapshots: '@types/node': 22.19.20 fsevents: 2.3.3 + vitest@4.1.8(@types/node@22.19.20)(vite@7.3.5(@types/node@22.19.20)): + dependencies: + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@7.3.5(@types/node@22.19.20)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.2 + 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: 7.3.5(@types/node@22.19.20) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.20 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + yaml@1.10.3: {} diff --git a/invokeai/frontend/webv2/src/vite-env.d.ts b/invokeai/frontend/webv2/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/invokeai/frontend/webv2/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/invokeai/frontend/webv2/src/workbench/persistence.test.ts b/invokeai/frontend/webv2/src/workbench/persistence.test.ts new file mode 100644 index 00000000000..466c2dfb237 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/persistence.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; + +import { migrateWorkbenchPersistenceSnapshot } from './persistence'; +import { createInitialWorkbenchState } from './workbenchState'; + +describe('workbench persistence migration', () => { + it('accepts current versioned workbench snapshots', () => { + const state = createInitialWorkbenchState(); + const snapshot = migrateWorkbenchPersistenceSnapshot({ savedAt: '2026-06-09T00:00:00.000Z', state, version: 1 }); + + expect(snapshot).toEqual({ savedAt: '2026-06-09T00:00:00.000Z', state, version: 1 }); + }); + + it('migrates legacy schemaVersion snapshots to the authoritative version field', () => { + const state = createInitialWorkbenchState(); + const snapshot = migrateWorkbenchPersistenceSnapshot({ + savedAt: '2026-06-09T00:00:00.000Z', + schemaVersion: 1, + state, + }); + + expect(snapshot?.version).toBe(1); + expect(snapshot?.state.projects).toHaveLength(3); + }); + + it('rejects unsupported persistence snapshots', () => { + expect(migrateWorkbenchPersistenceSnapshot({ state: createInitialWorkbenchState(), version: 999 })).toBeNull(); + expect(migrateWorkbenchPersistenceSnapshot({ state: { projects: [] }, version: 1 })).toBeNull(); + }); +}); diff --git a/invokeai/frontend/webv2/src/workbench/widgets/gallery/galleryStateView.test.ts b/invokeai/frontend/webv2/src/workbench/widgets/gallery/galleryStateView.test.ts new file mode 100644 index 00000000000..edfd06482e1 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/gallery/galleryStateView.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; + +import type { GalleryBoard } from '../../gallery/api'; +import type { GeneratedImageContract } from '../../types'; +import { getBoardCounts, getGallerySelectedBoardId, getGalleryStateView } from './galleryStateView'; + +const boards: GalleryBoard[] = [ + { assetCount: 0, id: 'none', imageCount: 1, isVirtual: true, name: 'Uncategorized' }, + { assetCount: 0, id: 'board-1', imageCount: 2, name: 'Board 1' }, +]; + +const createImage = (imageName: string): GeneratedImageContract => ({ + height: 768, + imageName, + imageUrl: `/api/v1/images/i/${imageName}/full`, + queuedAt: '2026-06-09T00:00:00.000Z', + sourceQueueItemId: 'queue-item-1', + thumbnailUrl: `/api/v1/images/i/${imageName}/thumbnail`, + width: 512, +}); + +describe('gallery state view', () => { + it('preserves a selected backend board id while boards are still loading', () => { + const values = { selectedBoardId: 'board-1' }; + + expect(getGallerySelectedBoardId(values, [])).toBe('board-1'); + expect(getGalleryStateView(values, [], null, false).selectedBoardId).toBe('board-1'); + }); + + it('falls back to uncategorized after loaded boards do not contain the selected board', () => { + const values = { selectedBoardId: 'missing-board' }; + + expect(getGallerySelectedBoardId(values, boards)).toBe('none'); + expect(getGalleryStateView(values, boards, [], false).selectedBoardId).toBe('none'); + }); + + it('does not render local fallback images while backend images are loading', () => { + const values = { recentImages: [createImage('local-fallback.png')], selectedBoardId: 'none' }; + const gallery = getGalleryStateView(values, boards, null, true); + + expect(gallery.images).toEqual([]); + expect(gallery.isLoading).toBe(true); + }); + + it('exposes both image and asset counts for board labels', () => { + expect(getBoardCounts(boards[1])).toEqual({ assetCount: 0, imageCount: 2 }); + }); +}); diff --git a/invokeai/frontend/webv2/src/workbench/workbenchState.test.ts b/invokeai/frontend/webv2/src/workbench/workbenchState.test.ts new file mode 100644 index 00000000000..8874e667302 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/workbenchState.test.ts @@ -0,0 +1,612 @@ +import { describe, expect, it } from 'vitest'; + +import type { GenerateWidgetValues, MainModelConfig } from './generation/types'; +import type { GeneratedImageContract, Project, WorkbenchState } from './types'; +import { createInitialWorkbenchState, workbenchReducer } from './workbenchState'; + +const model: MainModelConfig = { + base: 'sdxl', + key: 'test-model', + name: 'Test Model', + type: 'main', +}; + +const createGenerateValues = (overrides: Partial = {}): GenerateWidgetValues => ({ + batchCount: 1, + cfgRescaleMultiplier: 0, + cfgScale: 7, + height: 1024, + model, + modelKey: model.key, + negativePrompt: '', + positivePrompt: 'first prompt', + scheduler: 'euler_a', + seed: 123, + shouldRandomizeSeed: false, + steps: 30, + width: 1024, + ...overrides, +}); + +const createImage = (imageName: string, sourceQueueItemId: string): GeneratedImageContract => ({ + height: 768, + imageName, + imageUrl: `/api/v1/images/i/${imageName}/full`, + queuedAt: '2026-06-09T00:00:00.000Z', + sourceQueueItemId, + thumbnailUrl: `/api/v1/images/i/${imageName}/thumbnail`, + width: 512, +}); + +const getProject = (state: WorkbenchState, projectId: string): Project => { + const project = state.projects.find((candidate) => candidate.id === projectId); + + expect(project).toBeDefined(); + + return project as Project; +}; + +const getActiveProject = (state: WorkbenchState): Project => getProject(state, state.activeProjectId); + +const primeGenerate = (state = createInitialWorkbenchState(), overrides: Partial = {}) => + workbenchReducer(state, { type: 'setGenerateSettings', values: createGenerateValues(overrides) }); + +const submitGenerate = (state: WorkbenchState) => + workbenchReducer(state, { backendSupportsCancellation: true, type: 'submitInvocationSnapshot' }); + +describe('workbench widget region defaults', () => { + it('enables Diagnostics in the right side panel rail', () => { + const state = createInitialWorkbenchState(); + + expect(getActiveProject(state).widgetRegions.right.enabledWidgetIds).toContain('diagnostics'); + }); + + it('hydrates the old default right rail with Diagnostics while preserving customized rails', () => { + const initial = createInitialWorkbenchState(); + const legacyDefault = { + ...initial, + projects: initial.projects.map((project) => ({ + ...project, + widgetRegions: { + ...project.widgetRegions, + right: { ...project.widgetRegions.right, enabledWidgetIds: ['queue', 'gallery', 'layers'] }, + }, + })), + } satisfies WorkbenchState; + const customized = { + ...initial, + projects: initial.projects.map((project) => ({ + ...project, + widgetRegions: { + ...project.widgetRegions, + right: { ...project.widgetRegions.right, enabledWidgetIds: ['gallery', 'layers'] }, + }, + })), + } satisfies WorkbenchState; + + const hydratedLegacyDefault = workbenchReducer(initial, { state: legacyDefault, type: 'hydrateWorkbench' }); + const hydratedCustomized = workbenchReducer(initial, { state: customized, type: 'hydrateWorkbench' }); + + expect(getActiveProject(hydratedLegacyDefault).widgetRegions.right.enabledWidgetIds).toEqual([ + 'queue', + 'gallery', + 'layers', + 'diagnostics', + ]); + expect(getActiveProject(hydratedCustomized).widgetRegions.right.enabledWidgetIds).toEqual(['gallery', 'layers']); + }); +}); + +describe('workbenchReducer Phase 5 generation flow', () => { + it('routes queue results back to the originating project after the user switches projects', () => { + let state = submitGenerate(primeGenerate()); + const originProject = getActiveProject(state); + const queueItem = originProject.queue.items[0]; + + expect(queueItem).toBeDefined(); + + const otherProjectId = state.projects.find((project) => project.id !== originProject.id)?.id; + + expect(otherProjectId).toBeDefined(); + + state = workbenchReducer(state, { projectId: otherProjectId as string, type: 'switchProject' }); + state = workbenchReducer(state, { + backendItemIds: [42], + projectId: originProject.id, + queueItemId: queueItem.id, + type: 'markQueueItemBackendSubmitted', + }); + state = workbenchReducer(state, { + images: [createImage('origin-image.png', queueItem.id)], + projectId: originProject.id, + queueItemId: queueItem.id, + type: 'routeQueueItemResults', + }); + + const updatedOriginProject = getProject(state, originProject.id); + const activeProject = getActiveProject(state); + + expect(activeProject.id).toBe(otherProjectId); + expect(updatedOriginProject.canvas.stagingArea.pendingImageIds).toEqual(['origin-image.png']); + expect(updatedOriginProject.queue.items[0]?.status).toBe('completed'); + expect(activeProject.canvas.stagingArea.pendingImageIds).toEqual([]); + expect(activeProject.queue.items).toEqual([]); + }); + + it('keeps submitted Generate snapshots immutable after later settings changes', () => { + let state = submitGenerate(primeGenerate(undefined, { positivePrompt: 'first prompt', shouldRandomizeSeed: true })); + const firstQueueItem = getActiveProject(state).queue.items[0]; + + expect(firstQueueItem).toBeDefined(); + + state = primeGenerate(state, { positivePrompt: 'second prompt', seed: 999 }); + state = submitGenerate(state); + + const [secondQueueItem, unchangedFirstQueueItem] = getActiveProject(state).queue.items; + const firstValues = unchangedFirstQueueItem?.snapshot.widgetStates.generate + .values as unknown as GenerateWidgetValues; + const secondValues = secondQueueItem?.snapshot.widgetStates.generate.values as unknown as GenerateWidgetValues; + + expect(firstValues.positivePrompt).toBe('first prompt'); + expect(firstValues.shouldRandomizeSeed).toBe(false); + expect(typeof firstValues.seed).toBe('number'); + expect(secondValues.positivePrompt).toBe('second prompt'); + expect(secondValues.seed).toBe(999); + }); + + it('accepts a staged candidate into an undoable raster layer and prevents duplicate accepts', () => { + let state = submitGenerate(primeGenerate()); + const queueItem = getActiveProject(state).queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('candidate.png', queueItem.id)], + projectId: getActiveProject(state).id, + queueItemId: queueItem.id, + type: 'routeQueueItemResults', + }); + state = workbenchReducer(state, { type: 'acceptStagedImage' }); + + let project = getActiveProject(state); + + expect(project.canvas.document.layers).toHaveLength(1); + expect(project.canvas.document.layers[0]?.imageName).toBe('candidate.png'); + expect(project.canvas.stagingArea.pendingImages).toEqual([]); + expect(project.undoRedo.past).toHaveLength(1); + + state = workbenchReducer(state, { type: 'acceptStagedImage' }); + project = getActiveProject(state); + + expect(project.canvas.document.layers).toHaveLength(1); + + state = workbenchReducer(state, { type: 'undoProjectChange' }); + project = getActiveProject(state); + + expect(project.canvas.document.layers).toEqual([]); + expect(project.canvas.stagingArea.pendingImages).toEqual([]); + }); + + it('discards selected and all staged canvas candidates without touching accepted document layers', () => { + let state = submitGenerate(primeGenerate()); + const queueItem = getActiveProject(state).queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('candidate-1.png', queueItem.id), createImage('candidate-2.png', queueItem.id)], + projectId: getActiveProject(state).id, + queueItemId: queueItem.id, + type: 'routeQueueItemResults', + }); + state = workbenchReducer(state, { imageIndex: 1, type: 'setStagedImageIndex' }); + state = workbenchReducer(state, { type: 'discardSelectedStagedImage' }); + + let project = getActiveProject(state); + + expect(project.canvas.document.layers).toEqual([]); + expect(project.canvas.stagingArea.pendingImageIds).toEqual(['candidate-1.png']); + expect(project.canvas.stagingArea.selectedImageIndex).toBe(0); + expect(project.canvas.stagingArea.isVisible).toBe(true); + + state = workbenchReducer(state, { type: 'discardAllStagedImages' }); + project = getActiveProject(state); + + expect(project.canvas.stagingArea.pendingImages).toEqual([]); + expect(project.canvas.stagingArea.pendingImageIds).toEqual([]); + expect(project.canvas.stagingArea.isVisible).toBe(false); + }); + + it('cycles staged canvas candidates and accepts the selected candidate placement into a raster layer', () => { + let state = submitGenerate(primeGenerate()); + const queueItem = getActiveProject(state).queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('candidate-1.png', queueItem.id), createImage('candidate-2.png', queueItem.id)], + projectId: getActiveProject(state).id, + queueItemId: queueItem.id, + type: 'routeQueueItemResults', + }); + state = workbenchReducer(state, { direction: -1, type: 'cycleStagedImage' }); + + expect(getActiveProject(state).canvas.stagingArea.selectedImageIndex).toBe(1); + + const selectedPlacement = getActiveProject(state).canvas.stagingArea.pendingImages[1]?.placement; + + state = workbenchReducer(state, { type: 'acceptStagedImage' }); + + let project = getActiveProject(state); + + expect(project.canvas.document.layers[0]?.imageName).toBe('candidate-2.png'); + expect(project.canvas.document.layers[0]?.placement).toEqual(selectedPlacement); + + state = workbenchReducer(state, { type: 'undoProjectChange' }); + project = getActiveProject(state); + + expect(project.canvas.document.layers).toEqual([]); + + state = workbenchReducer(state, { type: 'redoProjectChange' }); + project = getActiveProject(state); + + expect(project.canvas.document.layers[0]?.imageName).toBe('candidate-2.png'); + }); + + it('keeps thumbnail strip visibility separate from staged result preview visibility', () => { + let state = submitGenerate(primeGenerate()); + const queueItem = getActiveProject(state).queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('candidate-1.png', queueItem.id), createImage('candidate-2.png', queueItem.id)], + projectId: getActiveProject(state).id, + queueItemId: queueItem.id, + type: 'routeQueueItemResults', + }); + + expect(getActiveProject(state).canvas.stagingArea.pendingImageIds).toEqual(['candidate-1.png', 'candidate-2.png']); + expect(getActiveProject(state).canvas.stagingArea.areThumbnailsVisible).toBe(true); + expect(getActiveProject(state).canvas.stagingArea.isVisible).toBe(true); + + state = workbenchReducer(state, { type: 'toggleCanvasStagingThumbnailsVisibility' }); + expect(getActiveProject(state).canvas.stagingArea.areThumbnailsVisible).toBe(false); + expect(getActiveProject(state).canvas.stagingArea.isVisible).toBe(true); + + state = workbenchReducer(state, { type: 'toggleCanvasStagingVisibility' }); + expect(getActiveProject(state).canvas.stagingArea.areThumbnailsVisible).toBe(false); + expect(getActiveProject(state).canvas.stagingArea.isVisible).toBe(false); + }); + + it('appends later canvas results to the active staging session instead of replacing candidates', () => { + let state = submitGenerate(primeGenerate()); + const firstQueueItem = getActiveProject(state).queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('candidate-1.png', firstQueueItem.id)], + projectId: getActiveProject(state).id, + queueItemId: firstQueueItem.id, + type: 'routeQueueItemResults', + }); + state = submitGenerate(primeGenerate(state, { positivePrompt: 'second prompt' })); + + const secondQueueItem = getActiveProject(state).queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('candidate-2.png', secondQueueItem.id)], + projectId: getActiveProject(state).id, + queueItemId: secondQueueItem.id, + type: 'routeQueueItemResults', + }); + + expect(getActiveProject(state).canvas.stagingArea.pendingImageIds).toEqual(['candidate-1.png', 'candidate-2.png']); + expect(getActiveProject(state).canvas.stagingArea.selectedImageIndex).toBe(1); + }); + + it('keeps staged canvas candidates isolated per project when switching projects', () => { + let state = submitGenerate(primeGenerate()); + const originProject = getActiveProject(state); + const queueItem = originProject.queue.items[0]; + const otherProjectId = state.projects.find((project) => project.id !== originProject.id)?.id; + + expect(otherProjectId).toBeDefined(); + + state = workbenchReducer(state, { + images: [createImage('origin-candidate.png', queueItem.id)], + projectId: originProject.id, + queueItemId: queueItem.id, + type: 'routeQueueItemResults', + }); + state = workbenchReducer(state, { projectId: otherProjectId as string, type: 'switchProject' }); + + expect(getActiveProject(state).canvas.stagingArea.pendingImages).toEqual([]); + + state = workbenchReducer(state, { projectId: originProject.id, type: 'switchProject' }); + + expect(getActiveProject(state).canvas.stagingArea.pendingImageIds).toEqual(['origin-candidate.png']); + }); + + it('marks cancellable running queue items cancelled for backend cancellation', () => { + let state = submitGenerate(primeGenerate()); + const project = getActiveProject(state); + const queueItem = project.queue.items[0]; + + state = workbenchReducer(state, { + backendItemIds: [42], + projectId: project.id, + queueItemId: queueItem.id, + type: 'markQueueItemBackendSubmitted', + }); + state = workbenchReducer(state, { queueItemId: queueItem.id, type: 'cancelQueueItem' }); + + expect(getActiveProject(state).queue.items[0]?.status).toBe('cancelled'); + }); + + it('keeps cancellation terminal when backend item ids arrive late', () => { + let state = submitGenerate(primeGenerate()); + const project = getActiveProject(state); + const queueItem = project.queue.items[0]; + + state = workbenchReducer(state, { queueItemId: queueItem.id, type: 'cancelQueueItem' }); + state = workbenchReducer(state, { + backendItemIds: [42], + projectId: project.id, + queueItemId: queueItem.id, + type: 'markQueueItemBackendSubmitted', + }); + state = workbenchReducer(state, { + images: [createImage('late-result.png', queueItem.id)], + projectId: project.id, + queueItemId: queueItem.id, + type: 'routeQueueItemResults', + }); + state = workbenchReducer(state, { + error: 'backend cancellation completed', + projectId: project.id, + queueItemId: queueItem.id, + status: 'failed', + type: 'setQueueItemStatus', + }); + + const cancelledItem = getActiveProject(state).queue.items[0]; + + expect(cancelledItem?.status).toBe('cancelled'); + expect(cancelledItem?.backendItemIds).toEqual([42]); + expect(getActiveProject(state).canvas.stagingArea.pendingImages).toEqual([]); + expect(state.notifications.map((notification) => notification.title)).toEqual([ + 'Invocation cancellation requested', + 'Invocation queued', + ]); + }); + + it('does not report cancellation for completed queue items', () => { + let state = submitGenerate(primeGenerate()); + const project = getActiveProject(state); + const queueItem = project.queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('completed-result.png', queueItem.id)], + projectId: project.id, + queueItemId: queueItem.id, + type: 'routeQueueItemResults', + }); + state = workbenchReducer(state, { queueItemId: queueItem.id, type: 'cancelQueueItem' }); + + expect(getActiveProject(state).queue.items[0]?.status).toBe('completed'); + expect(state.notifications.map((notification) => notification.title)).toEqual([ + 'Invocation completed', + 'Invocation queued', + ]); + }); + + it('records notifications for errors and successful operations', () => { + let state = submitGenerate(primeGenerate()); + + expect(state.notifications[0]?.title).toBe('Invocation queued'); + + const queueItem = getActiveProject(state).queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('candidate.png', queueItem.id)], + projectId: getActiveProject(state).id, + queueItemId: queueItem.id, + type: 'routeQueueItemResults', + }); + state = workbenchReducer(state, { type: 'acceptStagedImage' }); + state = workbenchReducer(state, { message: 'boom', type: 'recordError' }); + + expect(state.notifications.map((notification) => notification.title)).toEqual([ + 'Error', + 'Canvas layer accepted', + 'Invocation completed', + 'Invocation queued', + ]); + expect(state.notifications.every((notification) => !notification.isRead)).toBe(true); + + state = workbenchReducer(state, { type: 'markAllNotificationsRead' }); + + expect(state.notifications.every((notification) => notification.isRead)).toBe(true); + + state = workbenchReducer(state, { type: 'clearNotifications' }); + + expect(state.notifications).toEqual([]); + + state = workbenchReducer(state, { type: 'clearErrorLog' }); + + expect(state.errorLog).toEqual([]); + }); + + it('does not queue unavailable workflow/project graph sources in Phase 5', () => { + let state = createInitialWorkbenchState(); + + state = workbenchReducer(state, { sourceId: 'project-graph', type: 'setInvocationSource' }); + + expect(getActiveProject(state).invocation.sourceId).toBe('generate'); + + state = workbenchReducer(state, { + backendSupportsCancellation: true, + route: { destination: 'canvas', destinationLocked: false, sourceId: 'project-graph', sourceLocked: true }, + type: 'submitResolvedInvocationSnapshot', + }); + + expect(getActiveProject(state).queue.items).toEqual([]); + }); + + it('does not queue Generate snapshots with non-finite numeric settings', () => { + let state = primeGenerate(undefined, { seed: Number.NaN }); + + state = submitGenerate(state); + + expect(getActiveProject(state).queue.items).toEqual([]); + }); + + it('routes Gallery destination results to Gallery without staging them on Canvas', () => { + let state = primeGenerate(); + + state = workbenchReducer(state, { destination: 'gallery', type: 'setInvocationDestination' }); + state = submitGenerate(state); + + const project = getActiveProject(state); + const queueItem = project.queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('gallery-image.png', queueItem.id)], + projectId: project.id, + queueItemId: queueItem.id, + type: 'routeQueueItemResults', + }); + + const updatedProject = getActiveProject(state); + + expect(updatedProject.canvas.stagingArea.pendingImages).toEqual([]); + expect(updatedProject.widgetStates.gallery.values.recentImages).toEqual([ + createImage('gallery-image.png', queueItem.id), + ]); + expect(updatedProject.widgetStates.gallery.values.selectedImage).toEqual( + createImage('gallery-image.png', queueItem.id) + ); + expect(updatedProject.widgetStates.gallery.values.selectedImageName).toBe('gallery-image.png'); + }); + + it('appends Gallery destination results for local fallback while backend owns boards', () => { + let state = primeGenerate(); + + state = workbenchReducer(state, { destination: 'gallery', type: 'setInvocationDestination' }); + state = submitGenerate(state); + + const firstQueueItem = getActiveProject(state).queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('gallery-image-1.png', firstQueueItem.id)], + projectId: getActiveProject(state).id, + queueItemId: firstQueueItem.id, + type: 'routeQueueItemResults', + }); + state = submitGenerate(state); + + const secondQueueItem = getActiveProject(state).queue.items[0]; + + state = workbenchReducer(state, { + images: [createImage('gallery-image-2.png', secondQueueItem.id)], + projectId: getActiveProject(state).id, + queueItemId: secondQueueItem.id, + type: 'routeQueueItemResults', + }); + + const values = getActiveProject(state).widgetStates.gallery.values; + + expect((values.recentImages as GeneratedImageContract[]).map((image) => image.imageName)).toEqual([ + 'gallery-image-2.png', + 'gallery-image-1.png', + ]); + expect(values.imageBoards).toBeUndefined(); + }); + + it('stores selected backend board id for gallery submissions', () => { + let state = createInitialWorkbenchState(); + + state = workbenchReducer(state, { boardId: 'backend-board-id', type: 'selectGalleryBoard' }); + state = workbenchReducer(state, { destination: 'gallery', type: 'setInvocationDestination' }); + state = primeGenerate(state); + state = submitGenerate(state); + + const queueItem = getActiveProject(state).queue.items[0]; + + expect(queueItem.snapshot.widgetStates.gallery.values.selectedBoardId).toBe('backend-board-id'); + }); + + it('stores full selected gallery image data for Preview widget', () => { + let state = createInitialWorkbenchState(); + const image = createImage('backend-selected.png', 'backend-gallery'); + + state = workbenchReducer(state, { image, type: 'selectGalleryImage' }); + + expect(getActiveProject(state).widgetStates.gallery.values.selectedImageName).toBe('backend-selected.png'); + expect(getActiveProject(state).widgetStates.gallery.values.selectedImage).toEqual(image); + }); +}); + +describe('workbench preferences', () => { + it('defaults to the dark theme with motion enabled', () => { + const state = createInitialWorkbenchState(); + + expect(state.account.preferences).toEqual({ reduceMotion: false, showFocusRegionHighlight: true, themeId: 'dark' }); + }); + + it('updates the theme without dropping other preferences', () => { + let state = createInitialWorkbenchState(); + + state = workbenchReducer(state, { preferences: { reduceMotion: true }, type: 'setPreferences' }); + state = workbenchReducer(state, { preferences: { showFocusRegionHighlight: false }, type: 'setPreferences' }); + state = workbenchReducer(state, { preferences: { themeId: 'forest' }, type: 'setPreferences' }); + + expect(state.account.preferences).toEqual({ + reduceMotion: true, + showFocusRegionHighlight: false, + themeId: 'forest', + }); + }); + + it('preserves preferences when applying a layout preset', () => { + let state = createInitialWorkbenchState(); + + state = workbenchReducer(state, { preferences: { themeId: 'mono' }, type: 'setPreferences' }); + state = workbenchReducer(state, { presetId: 'gallery', type: 'applyPreset' }); + + expect(state.account.activeLayoutPresetId).toBe('gallery'); + expect(state.account.preferences.themeId).toBe('mono'); + }); + + it('heals hydrated state that predates preferences', () => { + const initial = createInitialWorkbenchState(); + const legacy = { + ...initial, + account: { activeLayoutPresetId: initial.account.activeLayoutPresetId }, + } as unknown as WorkbenchState; + + const state = workbenchReducer(initial, { state: legacy, type: 'hydrateWorkbench' }); + + expect(state.account.preferences).toEqual({ reduceMotion: false, showFocusRegionHighlight: true, themeId: 'dark' }); + }); + + it('heals hydrated state with an unsupported theme id', () => { + const initial = createInitialWorkbenchState(); + const persisted = { + ...initial, + account: { + ...initial.account, + preferences: { reduceMotion: true, showFocusRegionHighlight: false, themeId: 'sunset' }, + }, + } as unknown as WorkbenchState; + + const state = workbenchReducer(initial, { state: persisted, type: 'hydrateWorkbench' }); + + expect(state.account.preferences).toEqual({ + reduceMotion: true, + showFocusRegionHighlight: false, + themeId: 'dark', + }); + }); + + it('rejects unsupported theme ids when updating preferences', () => { + const state = workbenchReducer(createInitialWorkbenchState(), { + preferences: { themeId: 'sunset' }, + type: 'setPreferences', + } as unknown as Parameters[1]); + + expect(state.account.preferences.themeId).toBe('dark'); + }); +}); From da5dd80cc7225f7a1ea140075313e800737f3d34 Mon Sep 17 00:00:00 2001 From: joshistoast Date: Tue, 9 Jun 2026 23:37:54 -0600 Subject: [PATCH 005/153] feat(webv2): expand workbench state for runtime features --- invokeai/frontend/webv2/src/theme/themes.ts | 234 +++++ .../webv2/src/workbench/generation/api.ts | 150 +++ .../webv2/src/workbench/generation/graph.ts | 223 +++++ .../webv2/src/workbench/generation/types.ts | 79 ++ .../webv2/src/workbench/invocation.ts | 31 +- .../webv2/src/workbench/persistence.ts | 64 +- .../frontend/webv2/src/workbench/types.ts | 117 ++- .../webv2/src/workbench/workbenchState.ts | 873 +++++++++++++++++- 8 files changed, 1722 insertions(+), 49 deletions(-) create mode 100644 invokeai/frontend/webv2/src/theme/themes.ts create mode 100644 invokeai/frontend/webv2/src/workbench/generation/api.ts create mode 100644 invokeai/frontend/webv2/src/workbench/generation/graph.ts create mode 100644 invokeai/frontend/webv2/src/workbench/generation/types.ts diff --git a/invokeai/frontend/webv2/src/theme/themes.ts b/invokeai/frontend/webv2/src/theme/themes.ts new file mode 100644 index 00000000000..96f2dce4936 --- /dev/null +++ b/invokeai/frontend/webv2/src/theme/themes.ts @@ -0,0 +1,234 @@ +import type { WorkbenchThemeId } from '../workbench/types'; + +/** + * A single, mode-agnostic set of concrete color values that fully describes one + * workbench theme. Every theme provides the same slots so the semantic-token + * builder in `system.ts` can generate a uniform token contract across themes. + * + * Components never read these slots directly — they consume the semantic tokens + * (`bg.surface`, `fg.muted`, `accent.invoke`, …) that map onto them. Adding a new + * theme is therefore a matter of adding one entry here; no component changes. + */ +export interface ThemeColors { + /** App backdrop behind every panel. */ + shell: string; + /** Default raised panel / rail surface. */ + surface: string; + /** Slightly lifted surface (top bar, headers, popovers). */ + surfaceRaised: string; + /** Center work-area background. */ + center: string; + /** Canvas viewport background. */ + canvas: string; + /** Inset / control surface inside panels. */ + panel: string; + /** Stroke around the panel surface. */ + panelStroke: string; + /** Hairline divider color. */ + line: string; + /** Stronger divider / emphasized border. */ + lineStrong: string; + /** Canvas dot-grid color. */ + dot: string; + /** Primary text. */ + fg: string; + /** Secondary text. */ + fgMuted: string; + /** Tertiary / disabled text. */ + fgSubtle: string; + /** Primary action (Invoke) color. */ + accent: string; + /** Foreground used on top of `accent`. */ + accentFg: string; + /** Muted accent surface (active widget chip background). */ + accentMuted: string; + /** Foreground used on top of `accentMuted`. */ + accentMutedFg: string; + /** Secondary / selection accent (queue, active rail). */ + active: string; + /** Foreground used on top of `active`. */ + activeFg: string; + /** Destructive / error foreground. */ + danger: string; +} + +export interface ThemeDefinition { + id: WorkbenchThemeId; + label: string; + description: string; + /** Native color-scheme hint for form controls, scrollbars, and ` setBoardSearchTerm(event.currentTarget.value)} + onKeyDown={(event) => event.stopPropagation()} + /> + + + + {filteredBoards.length ? ( + filteredBoards.map((board) => { + const isSelected = board.id === gallery.selectedBoardId; + + return ( + onSelectBoard(board.id)}> + + + ); + }) + ) : ( + + No boards match this search. + + )} + + + + + + + ); +}; + +const BoardOptionContent = ({ board, isSelected }: { board: GalleryBoard; isSelected: boolean }) => { + const counts = getBoardCounts(board); + + return ( + + + + {board.name} + + + {counts.imageCount} | {counts.assetCount} + + + ); +}; + +const BoardCover = ({ board }: { board: GalleryBoard }) => { + if (board.coverThumbnailUrl) { + return ( + + ); + } + + return ( + + + + ); +}; + +const GalleryToolbar = ({ + gallery, + onSearch, + onSetImageDensityPercent, + onSetView, +}: { + gallery: GalleryStateView; + onSearch: (searchTerm: string) => void; + onSetImageDensityPercent: (imageDensityPercent: number) => void; + onSetView: (galleryView: GalleryView) => void; +}) => ( + + + + + + + + + {gallery.images.length} total + + + + + onSearch(event.currentTarget.value)} + /> + +); + +const GallerySettingsMenu = ({ + gallery, + onSetImageDensityPercent, +}: { + gallery: GalleryStateView; + onSetImageDensityPercent: (imageDensityPercent: number) => void; +}) => ( + + + + + + + + + + + + + Grid Density + + + {gallery.imageDensityPercent}% + + + event.stopPropagation()} + onValueChange={(event) => onSetImageDensityPercent(event.value[0] ?? gallery.imageDensityPercent)} + > + + + + + + + + + + + + +); + +const getGalleryColumnCount = (imageDensityPercent: number, layout: 'stacked' | 'wide'): number => { + const min = 2; + const max = layout === 'wide' ? 10 : 5; + const percent = Math.min(100, Math.max(0, imageDensityPercent)); + + return Math.round(min + ((max - min) * percent) / 100); +}; + +const getCompactThumbnailSizeRem = (imageDensityPercent: number): number => { + const percent = Math.min(100, Math.max(0, imageDensityPercent)); + + return 7 - (3 * percent) / 100; +}; + +const GalleryImages = ({ + columnCount, + imageDensityPercent, + images, + isLoading, + region, + selectedImageName, + onNavigate, + onSelect, +}: { + columnCount: number; + imageDensityPercent: number; + images: GeneratedImageContract[]; + isLoading: boolean; + region: WorkbenchRegion; + selectedImageName: string | null; + onNavigate: (direction: 'next' | 'previous') => void; + onSelect: (image: GeneratedImageContract) => void; +}) => { + const isLargePlacement = region === 'center' || region === 'bottom'; + const thumbnailAspectRatio = isLargePlacement ? 1 : 4 / 3; + const selectedIndex = images.findIndex((image) => image.imageName === selectedImageName); + const compactThumbnailSizeRem = getCompactThumbnailSizeRem(imageDensityPercent); + const gridTemplateColumns = isLargePlacement + ? `repeat(${columnCount}, minmax(0, 1fr))` + : `repeat(auto-fill, minmax(min(100%, ${compactThumbnailSizeRem}rem), ${compactThumbnailSizeRem}rem))`; + + if (images.length === 0) { + return ( + + + {isLoading ? 'Loading backend gallery...' : 'No images match this board, tab, or search.'} + + + ); + } + + return ( + { + if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { + event.preventDefault(); + onNavigate('next'); + } + + if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { + event.preventDefault(); + onNavigate('previous'); + } + }} + > + + {images.map((image, index) => ( + + ))} + + + ); +}; + +const GalleryThumbnail = ({ + image, + index, + isSelected, + selectedIndex, + thumbnailAspectRatio, + onSelect, +}: { + image: GeneratedImageContract; + index: number; + isSelected: boolean; + selectedIndex: number; + thumbnailAspectRatio: number; + onSelect: (image: GeneratedImageContract) => void; +}) => ( + + + +); diff --git a/invokeai/frontend/webv2/src/workbench/widgets/gallery/GalleryWidgetView.tsx b/invokeai/frontend/webv2/src/workbench/widgets/gallery/GalleryWidgetView.tsx index 10d4e4cb1c7..29e3433b806 100644 --- a/invokeai/frontend/webv2/src/workbench/widgets/gallery/GalleryWidgetView.tsx +++ b/invokeai/frontend/webv2/src/workbench/widgets/gallery/GalleryWidgetView.tsx @@ -1,50 +1,139 @@ -import { Flex, Stack, Text } from '@chakra-ui/react'; +import { useEffect, useState } from 'react'; import { PiImageBold } from 'react-icons/pi'; -import { StatusWidgetChip, WidgetPanelFrame } from '../../components/WidgetFrames'; -import { WidgetFailureBoundary } from '../../components/WidgetFailureBoundary'; +import { StatusWidgetChip } from '../../components/WidgetFrames'; +import { + createGalleryBoard, + listGalleryBoards, + listGalleryImages, + type GalleryBoard, + type GalleryImage, +} from '../../gallery/api'; import type { WidgetViewProps } from '../../types'; +import { useWorkbench } from '../../WorkbenchContext'; +import { GalleryPanelContent } from './GalleryPanelContent'; +import { + getGalleryRecentImagesKey, + getGallerySearchTerm, + getGallerySelectedBoardId, + getGalleryStateView, + getGalleryView, +} from './galleryStateView'; + +interface BackendImagesResult { + images: GalleryImage[]; + queryKey: string; +} + +const EMPTY_BACKEND_IMAGES: GalleryImage[] = []; + +const getGalleryQueryKey = ({ + boardId, + galleryView, + searchTerm, +}: { + boardId: string; + galleryView: string; + searchTerm: string; +}): string => `${galleryView}\0${boardId}\0${searchTerm.trim()}`; export const GalleryWidgetView = ({ presentation, region }: WidgetViewProps) => { - if (region === 'bottom') { - if (presentation === 'expanded') { - return ( - - - Gallery - - - Gallery status and recent outputs will render here once gallery data is connected. - - - ); - } - - return Gallery; - } + const { activeProject, dispatch } = useWorkbench(); + const [backendBoards, setBackendBoards] = useState([]); + const [backendImagesResult, setBackendImagesResult] = useState(null); + const [loadingImageQueryKey, setLoadingImageQueryKey] = useState(null); + const galleryValues = activeProject.widgetStates.gallery.values; + const selectedBoardId = getGallerySelectedBoardId(galleryValues, backendBoards); + const galleryView = getGalleryView(galleryValues); + const searchTerm = getGallerySearchTerm(galleryValues); + const recentImagesKey = getGalleryRecentImagesKey(galleryValues); + const imageQueryKey = getGalleryQueryKey({ boardId: selectedBoardId, galleryView, searchTerm }); + const isAwaitingCurrentImages = backendImagesResult !== null && backendImagesResult.queryKey !== imageQueryKey; + const backendImages = + backendImagesResult === null + ? null + : backendImagesResult.queryKey === imageQueryKey + ? backendImagesResult.images + : EMPTY_BACKEND_IMAGES; + const isLoadingImages = loadingImageQueryKey === imageQueryKey || isAwaitingCurrentImages; + const gallery = getGalleryStateView(galleryValues, backendBoards, backendImages, isLoadingImages); + const isWidePlacement = region === 'center' || (region === 'bottom' && presentation === 'expanded'); + + useEffect(() => { + let isStale = false; + + listGalleryBoards() + .then((boards) => { + if (!isStale) { + setBackendBoards(boards); + } + }) + .catch((error: unknown) => { + dispatch({ message: error instanceof Error ? error.message : String(error), type: 'recordError' }); + }); + + return () => { + isStale = true; + }; + }, [dispatch, recentImagesKey]); + + useEffect(() => { + let isStale = false; + + setLoadingImageQueryKey(imageQueryKey); + listGalleryImages({ + boardId: selectedBoardId, + galleryView, + searchTerm, + }) + .then((images) => { + if (!isStale) { + setBackendImagesResult({ images, queryKey: imageQueryKey }); + } + }) + .catch((error: unknown) => { + if (!isStale) { + setBackendImagesResult({ images: [], queryKey: imageQueryKey }); + dispatch({ message: error instanceof Error ? error.message : String(error), type: 'recordError' }); + } + }) + .finally(() => { + if (!isStale) { + setLoadingImageQueryKey((currentQueryKey) => (currentQueryKey === imageQueryKey ? null : currentQueryKey)); + } + }); + + return () => { + isStale = true; + }; + }, [dispatch, galleryView, imageQueryKey, recentImagesKey, searchTerm, selectedBoardId]); - if (region === 'right') { - return ( - - - - Gallery - - - Gallery controls will render here when this widget is mounted into the right panel. - - - - ); + if (region === 'bottom' && presentation !== 'expanded') { + return Gallery: {gallery.images.length}; } return ( - - - - Gallery view - - - + { + createGalleryBoard(`Board ${backendBoards.length}`) + .then((board) => { + setBackendBoards((boards) => [...boards, board]); + dispatch({ boardId: board.id, type: 'selectGalleryBoard' }); + }) + .catch((error: unknown) => { + dispatch({ message: error instanceof Error ? error.message : String(error), type: 'recordError' }); + }); + }} + onSearch={(searchTerm) => dispatch({ searchTerm, type: 'setGallerySearchTerm' })} + onSetImageDensityPercent={(imageDensityPercent) => + dispatch({ imageDensityPercent, type: 'setGalleryImageDensityPercent' }) + } + onSelectBoard={(boardId) => dispatch({ boardId, type: 'selectGalleryBoard' })} + onSelectImage={(image) => dispatch({ image, type: 'selectGalleryImage' })} + onSetView={(galleryView) => dispatch({ galleryView, type: 'setGalleryView' })} + /> ); }; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/gallery/galleryStateView.ts b/invokeai/frontend/webv2/src/workbench/widgets/gallery/galleryStateView.ts new file mode 100644 index 00000000000..31d7b00f83c --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/gallery/galleryStateView.ts @@ -0,0 +1,86 @@ +import type { GalleryBoard, GalleryImage, GalleryView } from '../../gallery/api'; +import type { GeneratedImageContract } from '../../types'; + +const UNCATEGORIZED_BOARD: GalleryBoard = { + assetCount: 0, + id: 'none', + imageCount: 0, + isVirtual: true, + name: 'Uncategorized', +}; + +export interface GalleryStateView { + boards: GalleryBoard[]; + galleryView: GalleryView; + imageDensityPercent: number; + images: GalleryImage[]; + isLoading: boolean; + searchTerm: string; + selectedBoardId: string; + selectedImageName: string | null; +} + +export const getGalleryView = (values: Record): GalleryView => + values.galleryView === 'assets' ? 'assets' : 'images'; + +export const getGallerySearchTerm = (values: Record): string => + typeof values.searchTerm === 'string' ? values.searchTerm : ''; + +export const getGallerySelectedBoardId = (values: Record, backendBoards: GalleryBoard[]): string => { + const selectedBoardId = typeof values.selectedBoardId === 'string' ? values.selectedBoardId : 'none'; + + if (backendBoards.length === 0 || backendBoards.some((board) => board.id === selectedBoardId)) { + return selectedBoardId; + } + + return 'none'; +}; + +export const getGalleryRecentImagesKey = (values: Record): string => { + if (!Array.isArray(values.recentImages)) { + return ''; + } + + return (values.recentImages as GeneratedImageContract[]).map((image) => image.imageName).join('\0'); +}; + +export const getGalleryStateView = ( + values: Record, + backendBoards: GalleryBoard[], + backendImages: GalleryImage[] | null, + isLoading: boolean +): GalleryStateView => { + const localImages = Array.isArray(values.recentImages) + ? (values.recentImages as GeneratedImageContract[]).map((image) => ({ + ...image, + boardId: 'none', + imageCategory: 'general' as const, + })) + : []; + const images = backendImages ?? (isLoading ? [] : localImages); + const selectedImageName = typeof values.selectedImageName === 'string' ? values.selectedImageName : null; + const galleryView = getGalleryView(values); + const imageDensityPercent = + typeof values.imageDensityPercent === 'number' && Number.isFinite(values.imageDensityPercent) + ? Math.min(100, Math.max(0, values.imageDensityPercent)) + : 50; + const searchTerm = getGallerySearchTerm(values); + const boards = backendBoards.length ? backendBoards : [{ ...UNCATEGORIZED_BOARD, imageCount: images.length }]; + const selectedBoardId = getGallerySelectedBoardId(values, backendBoards); + + return { + boards, + galleryView, + imageDensityPercent, + images, + isLoading, + searchTerm, + selectedBoardId, + selectedImageName: images.some((image) => image.imageName === selectedImageName) ? selectedImageName : null, + }; +}; + +export const getBoardCounts = (board: GalleryBoard): { assetCount: number; imageCount: number } => ({ + assetCount: board.assetCount, + imageCount: board.imageCount, +}); diff --git a/invokeai/frontend/webv2/src/workbench/widgets/gallery/index.ts b/invokeai/frontend/webv2/src/workbench/widgets/gallery/index.ts index ca394c27cca..db8a94ad6e7 100644 --- a/invokeai/frontend/webv2/src/workbench/widgets/gallery/index.ts +++ b/invokeai/frontend/webv2/src/workbench/widgets/gallery/index.ts @@ -2,12 +2,14 @@ import type { WidgetManifest } from '../../types'; import { GalleryWidgetView } from './GalleryWidgetView'; export const galleryWidgetManifest: WidgetManifest = { + bottomPanel: 'expandable', + chrome: { header: 'hidden' }, failurePolicy: { isolateRenderFailure: true, onRegistrationFailure: 'disable' }, icon: 'lucide-react:image', id: 'gallery', label: 'Gallery', labelText: 'Gallery', - regions: ['right', 'center', 'bottom'], + regions: ['left', 'right', 'center', 'bottom'], version: 1, view: GalleryWidgetView, }; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/generate/GenerateSettingsForm.tsx b/invokeai/frontend/webv2/src/workbench/widgets/generate/GenerateSettingsForm.tsx new file mode 100644 index 00000000000..d5f83da63f3 --- /dev/null +++ b/invokeai/frontend/webv2/src/workbench/widgets/generate/GenerateSettingsForm.tsx @@ -0,0 +1,161 @@ +import { Button, HStack, Input, NativeSelect, Stack, Text, Textarea } from '@chakra-ui/react'; +import type { ChangeEvent } from 'react'; + +import { Field } from '../../components/ui/Field'; +import { getSettingsWithModelDefaults } from '../../generation/graph'; +import type { GenerateSettings, MainModelConfig } from '../../generation/types'; + +interface GenerateSettingsFormProps { + isLoadingModels: boolean; + loadError: string | null; + settings: GenerateSettings; + supportedModels: MainModelConfig[]; + onCommitSettings: (nextSettings: GenerateSettings) => void; +} + +export const GenerateSettingsForm = ({ + isLoadingModels, + loadError, + settings, + supportedModels, + onCommitSettings, +}: GenerateSettingsFormProps) => { + const updateText = (key: 'negativePrompt' | 'positivePrompt') => (event: ChangeEvent) => { + onCommitSettings({ ...settings, [key]: event.currentTarget.value }); + }; + + const updateNumber = + (key: 'cfgScale' | 'height' | 'seed' | 'steps' | 'width') => (event: ChangeEvent) => { + const value = Number(event.currentTarget.value); + + if (!Number.isFinite(value)) { + return; + } + + onCommitSettings({ ...settings, [key]: value }); + }; + + return ( + + +