feat: Add login and sso callback scenes
This commit is contained in:
parent
b2c1e57c86
commit
163ae21135
38
nginx.conf
38
nginx.conf
@ -1,31 +1,55 @@
|
|||||||
|
map $sent_http_content_type $csp_header {
|
||||||
|
default "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' https://demo.flow-master.ai https://canvas.flow-master.ai wss://canvas.flow-master.ai; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'";
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
# Reverse-proxy /api to the demo backend so live mode works
|
# Hardening — every response carries these.
|
||||||
# same-origin without CORS gymnastics. Backend is reached via the
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||||
# cluster-internal service rewritten by the ingress to demo.flow-master.ai.
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(self), geolocation=(self), payment=()" always;
|
||||||
|
add_header Content-Security-Policy $csp_header always;
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||||
|
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||||
|
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||||
|
|
||||||
|
# Reverse-proxy /api to the demo backend so live mode is same-origin.
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass https://demo.flow-master.ai;
|
proxy_pass https://demo.flow-master.ai;
|
||||||
proxy_set_header Host demo.flow-master.ai;
|
proxy_set_header Host demo.flow-master.ai;
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_ssl_server_name on;
|
proxy_ssl_server_name on;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_read_timeout 30s;
|
proxy_read_timeout 30s;
|
||||||
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
# SPA: everything else falls back to index.html.
|
# SPA fallback to index.html for client-side routing.
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
add_header Cache-Control "no-cache";
|
add_header Cache-Control "no-cache" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(self), geolocation=(self), payment=()" always;
|
||||||
|
add_header Content-Security-Policy $csp_header always;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Static assets with content hashes in their filenames cache forever.
|
# Hashed static assets cache forever.
|
||||||
location /assets/ {
|
location /assets/ {
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Hide nginx version on errors.
|
||||||
|
server_tokens off;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/node": "^24.12.3",
|
"@types/node": "^24.12.3",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@ -34,6 +36,8 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
|
"happy-dom": "^20.10.3",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
"playwright": "^1.60.0",
|
"playwright": "^1.60.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.59.2",
|
"typescript-eslint": "^8.59.2",
|
||||||
|
|||||||
563
pnpm-lock.yaml
generated
563
pnpm-lock.yaml
generated
@ -36,6 +36,12 @@ importers:
|
|||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^10.0.1
|
specifier: ^10.0.1
|
||||||
version: 10.0.1(eslint@10.5.0)
|
version: 10.0.1(eslint@10.5.0)
|
||||||
|
'@testing-library/jest-dom':
|
||||||
|
specifier: ^6.9.1
|
||||||
|
version: 6.9.1
|
||||||
|
'@testing-library/react':
|
||||||
|
specifier: ^16.3.2
|
||||||
|
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.12.3
|
specifier: ^24.12.3
|
||||||
version: 24.13.2
|
version: 24.13.2
|
||||||
@ -60,6 +66,12 @@ importers:
|
|||||||
globals:
|
globals:
|
||||||
specifier: ^17.6.0
|
specifier: ^17.6.0
|
||||||
version: 17.6.0
|
version: 17.6.0
|
||||||
|
happy-dom:
|
||||||
|
specifier: ^20.10.3
|
||||||
|
version: 20.10.3
|
||||||
|
jsdom:
|
||||||
|
specifier: ^29.1.1
|
||||||
|
version: 29.1.1
|
||||||
playwright:
|
playwright:
|
||||||
specifier: ^1.60.0
|
specifier: ^1.60.0
|
||||||
version: 1.60.0
|
version: 1.60.0
|
||||||
@ -74,10 +86,28 @@ importers:
|
|||||||
version: 8.0.16(@types/node@24.13.2)
|
version: 8.0.16(@types/node@24.13.2)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.1.8
|
specifier: ^4.1.8
|
||||||
version: 4.1.8(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2))
|
version: 4.1.8(@types/node@24.13.2)(happy-dom@20.10.3)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@adobe/css-tools@4.5.0':
|
||||||
|
resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
|
||||||
|
|
||||||
|
'@asamuzakjp/css-color@5.1.11':
|
||||||
|
resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
|
'@asamuzakjp/dom-selector@7.1.1':
|
||||||
|
resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
|
'@asamuzakjp/generational-cache@1.0.1':
|
||||||
|
resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
|
'@asamuzakjp/nwsapi@2.3.9':
|
||||||
|
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
|
||||||
|
|
||||||
'@babel/code-frame@7.29.7':
|
'@babel/code-frame@7.29.7':
|
||||||
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -133,6 +163,10 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@babel/runtime@7.29.7':
|
||||||
|
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/template@7.29.7':
|
'@babel/template@7.29.7':
|
||||||
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -145,6 +179,46 @@ packages:
|
|||||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@bramus/specificity@2.4.2':
|
||||||
|
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@csstools/color-helpers@6.0.2':
|
||||||
|
resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
|
'@csstools/css-calc@3.2.1':
|
||||||
|
resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@csstools/css-parser-algorithms': ^4.0.0
|
||||||
|
'@csstools/css-tokenizer': ^4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-color-parser@4.1.3':
|
||||||
|
resolution: {integrity: sha512-DOgvIPkikIOixQRlD4YF31VN6fLLUTdrzhfRbis8vm0kMTgIbEPX0Ip/YX9fOeV9iywAS4sUUbTclpan7yYP8Q==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@csstools/css-parser-algorithms': ^4.0.0
|
||||||
|
'@csstools/css-tokenizer': ^4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-parser-algorithms@4.0.0':
|
||||||
|
resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@csstools/css-tokenizer': ^4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-syntax-patches-for-csstree@1.1.5':
|
||||||
|
resolution: {integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==}
|
||||||
|
peerDependencies:
|
||||||
|
css-tree: ^3.2.1
|
||||||
|
peerDependenciesMeta:
|
||||||
|
css-tree:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@csstools/css-tokenizer@4.0.0':
|
||||||
|
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
'@emnapi/core@1.10.0':
|
'@emnapi/core@1.10.0':
|
||||||
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
|
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
|
||||||
|
|
||||||
@ -193,6 +267,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
|
resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@exodus/bytes@1.15.1':
|
||||||
|
resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@noble/hashes': ^1.8.0 || ^2.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@noble/hashes':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@humanfs/core@0.19.2':
|
'@humanfs/core@0.19.2':
|
||||||
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@ -546,9 +629,35 @@ packages:
|
|||||||
'@standard-schema/spec@1.1.0':
|
'@standard-schema/spec@1.1.0':
|
||||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
|
'@testing-library/dom@10.4.1':
|
||||||
|
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@testing-library/jest-dom@6.9.1':
|
||||||
|
resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
|
||||||
|
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
|
||||||
|
|
||||||
|
'@testing-library/react@16.3.2':
|
||||||
|
resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@testing-library/dom': ^10.0.0
|
||||||
|
'@types/react': ^18.0.0 || ^19.0.0
|
||||||
|
'@types/react-dom': ^18.0.0 || ^19.0.0
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.2':
|
'@tybys/wasm-util@0.10.2':
|
||||||
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
||||||
|
|
||||||
|
'@types/aria-query@5.0.4':
|
||||||
|
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
|
||||||
@ -674,6 +783,12 @@ packages:
|
|||||||
'@types/react@19.2.17':
|
'@types/react@19.2.17':
|
||||||
resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
|
resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
|
||||||
|
|
||||||
|
'@types/whatwg-mimetype@3.0.2':
|
||||||
|
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
|
||||||
|
|
||||||
|
'@types/ws@8.18.1':
|
||||||
|
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.61.0':
|
'@typescript-eslint/eslint-plugin@8.61.0':
|
||||||
resolution: {integrity: sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==}
|
resolution: {integrity: sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@ -788,10 +903,25 @@ packages:
|
|||||||
ajv@6.15.0:
|
ajv@6.15.0:
|
||||||
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
|
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-styles@5.2.0:
|
||||||
|
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
aria-hidden@1.2.6:
|
aria-hidden@1.2.6:
|
||||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
aria-query@5.3.0:
|
||||||
|
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||||
|
|
||||||
|
aria-query@5.3.2:
|
||||||
|
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
assertion-error@2.0.1:
|
assertion-error@2.0.1:
|
||||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -805,6 +935,9 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
bidi-js@1.0.3:
|
||||||
|
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||||
|
|
||||||
brace-expansion@5.0.6:
|
brace-expansion@5.0.6:
|
||||||
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
|
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
@ -814,6 +947,10 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
buffer-image-size@0.6.4:
|
||||||
|
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001799:
|
caniuse-lite@1.0.30001799:
|
||||||
resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==}
|
resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==}
|
||||||
|
|
||||||
@ -837,6 +974,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
css-tree@3.2.1:
|
||||||
|
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
|
||||||
|
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||||
|
|
||||||
|
css.escape@1.5.1:
|
||||||
|
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||||
|
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
@ -881,6 +1025,10 @@ packages:
|
|||||||
dagre@0.8.5:
|
dagre@0.8.5:
|
||||||
resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==}
|
resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==}
|
||||||
|
|
||||||
|
data-urls@7.0.0:
|
||||||
|
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@ -890,9 +1038,16 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decimal.js@10.6.0:
|
||||||
|
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||||
|
|
||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
|
dequal@2.0.3:
|
||||||
|
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
detect-libc@2.1.2:
|
detect-libc@2.1.2:
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -900,9 +1055,23 @@ packages:
|
|||||||
detect-node-es@1.1.0:
|
detect-node-es@1.1.0:
|
||||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||||
|
|
||||||
|
dom-accessibility-api@0.5.16:
|
||||||
|
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||||
|
|
||||||
|
dom-accessibility-api@0.6.3:
|
||||||
|
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.372:
|
electron-to-chromium@1.5.372:
|
||||||
resolution: {integrity: sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==}
|
resolution: {integrity: sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==}
|
||||||
|
|
||||||
|
entities@7.0.1:
|
||||||
|
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
entities@8.0.0:
|
||||||
|
resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
es-module-lexer@2.1.0:
|
es-module-lexer@2.1.0:
|
||||||
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
|
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
|
||||||
|
|
||||||
@ -1050,12 +1219,20 @@ packages:
|
|||||||
graphlib@2.1.8:
|
graphlib@2.1.8:
|
||||||
resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==}
|
resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==}
|
||||||
|
|
||||||
|
happy-dom@20.10.3:
|
||||||
|
resolution: {integrity: sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
hermes-estree@0.25.1:
|
hermes-estree@0.25.1:
|
||||||
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
||||||
|
|
||||||
hermes-parser@0.25.1:
|
hermes-parser@0.25.1:
|
||||||
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
|
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
|
||||||
|
|
||||||
|
html-encoding-sniffer@6.0.0:
|
||||||
|
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
ignore@5.3.2:
|
ignore@5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@ -1068,6 +1245,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||||
engines: {node: '>=0.8.19'}
|
engines: {node: '>=0.8.19'}
|
||||||
|
|
||||||
|
indent-string@4.0.0:
|
||||||
|
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
is-extglob@2.1.1:
|
is-extglob@2.1.1:
|
||||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -1076,12 +1257,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-potential-custom-element-name@1.0.1:
|
||||||
|
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||||
|
|
||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
jsdom@29.1.1:
|
||||||
|
resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
canvas: ^3.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
canvas:
|
||||||
|
optional: true
|
||||||
|
|
||||||
jsesc@3.1.0:
|
jsesc@3.1.0:
|
||||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -1189,12 +1382,27 @@ packages:
|
|||||||
lodash@4.18.1:
|
lodash@4.18.1:
|
||||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||||
|
|
||||||
|
lru-cache@11.5.1:
|
||||||
|
resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
|
lz-string@1.5.0:
|
||||||
|
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
mdn-data@2.27.1:
|
||||||
|
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||||
|
|
||||||
|
min-indent@1.0.1:
|
||||||
|
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
@ -1236,6 +1444,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
parse5@8.0.1:
|
||||||
|
resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
|
||||||
|
|
||||||
path-exists@4.0.0:
|
path-exists@4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -1272,6 +1483,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
pretty-format@27.5.1:
|
||||||
|
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||||
|
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -1281,6 +1496,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.2.7
|
react: ^19.2.7
|
||||||
|
|
||||||
|
react-is@17.0.2:
|
||||||
|
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.8:
|
react-remove-scroll-bar@2.3.8:
|
||||||
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -1321,11 +1539,23 @@ packages:
|
|||||||
react: '>=17'
|
react: '>=17'
|
||||||
react-dom: '>=17'
|
react-dom: '>=17'
|
||||||
|
|
||||||
|
redent@3.0.0:
|
||||||
|
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
require-from-string@2.0.2:
|
||||||
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
rolldown@1.0.3:
|
rolldown@1.0.3:
|
||||||
resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
|
resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
saxes@6.0.0:
|
||||||
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
|
engines: {node: '>=v12.22.7'}
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
@ -1359,6 +1589,13 @@ packages:
|
|||||||
std-env@4.1.0:
|
std-env@4.1.0:
|
||||||
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
||||||
|
|
||||||
|
strip-indent@3.0.0:
|
||||||
|
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
symbol-tree@3.2.4:
|
||||||
|
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||||
|
|
||||||
tinybench@2.9.0:
|
tinybench@2.9.0:
|
||||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
@ -1374,6 +1611,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
tldts-core@7.4.2:
|
||||||
|
resolution: {integrity: sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==}
|
||||||
|
|
||||||
|
tldts@7.4.2:
|
||||||
|
resolution: {integrity: sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
tough-cookie@6.0.1:
|
||||||
|
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
tr46@6.0.0:
|
||||||
|
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
ts-api-utils@2.5.0:
|
ts-api-utils@2.5.0:
|
||||||
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
|
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
|
||||||
engines: {node: '>=18.12'}
|
engines: {node: '>=18.12'}
|
||||||
@ -1402,6 +1654,10 @@ packages:
|
|||||||
undici-types@7.18.2:
|
undici-types@7.18.2:
|
||||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||||
|
|
||||||
|
undici@7.27.2:
|
||||||
|
resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==}
|
||||||
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
update-browserslist-db@1.2.3:
|
update-browserslist-db@1.2.3:
|
||||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -1520,6 +1776,26 @@ packages:
|
|||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
w3c-xmlserializer@5.0.0:
|
||||||
|
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
webidl-conversions@8.0.1:
|
||||||
|
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
whatwg-mimetype@3.0.0:
|
||||||
|
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
whatwg-mimetype@5.0.0:
|
||||||
|
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
whatwg-url@16.0.1:
|
||||||
|
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -1534,6 +1810,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
ws@8.21.0:
|
||||||
|
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
xml-name-validator@5.0.0:
|
||||||
|
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
xmlchars@2.2.0:
|
||||||
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
|
|
||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
@ -1585,6 +1880,28 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@adobe/css-tools@4.5.0': {}
|
||||||
|
|
||||||
|
'@asamuzakjp/css-color@5.1.11':
|
||||||
|
dependencies:
|
||||||
|
'@asamuzakjp/generational-cache': 1.0.1
|
||||||
|
'@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-color-parser': 4.1.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
|
|
||||||
|
'@asamuzakjp/dom-selector@7.1.1':
|
||||||
|
dependencies:
|
||||||
|
'@asamuzakjp/generational-cache': 1.0.1
|
||||||
|
'@asamuzakjp/nwsapi': 2.3.9
|
||||||
|
bidi-js: 1.0.3
|
||||||
|
css-tree: 3.2.1
|
||||||
|
is-potential-custom-element-name: 1.0.1
|
||||||
|
|
||||||
|
'@asamuzakjp/generational-cache@1.0.1': {}
|
||||||
|
|
||||||
|
'@asamuzakjp/nwsapi@2.3.9': {}
|
||||||
|
|
||||||
'@babel/code-frame@7.29.7':
|
'@babel/code-frame@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
@ -1662,6 +1979,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
|
'@babel/runtime@7.29.7': {}
|
||||||
|
|
||||||
'@babel/template@7.29.7':
|
'@babel/template@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
@ -1685,6 +2004,34 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.29.7
|
'@babel/helper-string-parser': 7.29.7
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
|
|
||||||
|
'@bramus/specificity@2.4.2':
|
||||||
|
dependencies:
|
||||||
|
css-tree: 3.2.1
|
||||||
|
|
||||||
|
'@csstools/color-helpers@6.0.2': {}
|
||||||
|
|
||||||
|
'@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-color-parser@4.1.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/color-helpers': 6.0.2
|
||||||
|
'@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||||
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
|
|
||||||
|
'@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)':
|
||||||
|
optionalDependencies:
|
||||||
|
css-tree: 3.2.1
|
||||||
|
|
||||||
|
'@csstools/css-tokenizer@4.0.0': {}
|
||||||
|
|
||||||
'@emnapi/core@1.10.0':
|
'@emnapi/core@1.10.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.2.1
|
'@emnapi/wasi-threads': 1.2.1
|
||||||
@ -1735,6 +2082,8 @@ snapshots:
|
|||||||
'@eslint/core': 1.2.1
|
'@eslint/core': 1.2.1
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@exodus/bytes@1.15.1': {}
|
||||||
|
|
||||||
'@humanfs/core@0.19.2':
|
'@humanfs/core@0.19.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanfs/types': 0.15.0
|
'@humanfs/types': 0.15.0
|
||||||
@ -2052,11 +2401,43 @@ snapshots:
|
|||||||
|
|
||||||
'@standard-schema/spec@1.1.0': {}
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
|
'@testing-library/dom@10.4.1':
|
||||||
|
dependencies:
|
||||||
|
'@babel/code-frame': 7.29.7
|
||||||
|
'@babel/runtime': 7.29.7
|
||||||
|
'@types/aria-query': 5.0.4
|
||||||
|
aria-query: 5.3.0
|
||||||
|
dom-accessibility-api: 0.5.16
|
||||||
|
lz-string: 1.5.0
|
||||||
|
picocolors: 1.1.1
|
||||||
|
pretty-format: 27.5.1
|
||||||
|
|
||||||
|
'@testing-library/jest-dom@6.9.1':
|
||||||
|
dependencies:
|
||||||
|
'@adobe/css-tools': 4.5.0
|
||||||
|
aria-query: 5.3.2
|
||||||
|
css.escape: 1.5.1
|
||||||
|
dom-accessibility-api: 0.6.3
|
||||||
|
picocolors: 1.1.1
|
||||||
|
redent: 3.0.0
|
||||||
|
|
||||||
|
'@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.29.7
|
||||||
|
'@testing-library/dom': 10.4.1
|
||||||
|
react: 19.2.7
|
||||||
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.17
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.2':
|
'@tybys/wasm-util@0.10.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/aria-query@5.0.4': {}
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/deep-eql': 4.0.2
|
'@types/deep-eql': 4.0.2
|
||||||
@ -2203,6 +2584,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/whatwg-mimetype@3.0.2': {}
|
||||||
|
|
||||||
|
'@types/ws@8.18.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.13.2
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0)(typescript@6.0.3))(eslint@10.5.0)(typescript@6.0.3)':
|
'@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0)(typescript@6.0.3))(eslint@10.5.0)(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
@ -2353,16 +2740,30 @@ snapshots:
|
|||||||
json-schema-traverse: 0.4.1
|
json-schema-traverse: 0.4.1
|
||||||
uri-js: 4.4.1
|
uri-js: 4.4.1
|
||||||
|
|
||||||
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-styles@5.2.0: {}
|
||||||
|
|
||||||
aria-hidden@1.2.6:
|
aria-hidden@1.2.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
aria-query@5.3.0:
|
||||||
|
dependencies:
|
||||||
|
dequal: 2.0.3
|
||||||
|
|
||||||
|
aria-query@5.3.2: {}
|
||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
balanced-match@4.0.4: {}
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.37: {}
|
baseline-browser-mapping@2.10.37: {}
|
||||||
|
|
||||||
|
bidi-js@1.0.3:
|
||||||
|
dependencies:
|
||||||
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
brace-expansion@5.0.6:
|
brace-expansion@5.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 4.0.4
|
balanced-match: 4.0.4
|
||||||
@ -2375,6 +2776,10 @@ snapshots:
|
|||||||
node-releases: 2.0.47
|
node-releases: 2.0.47
|
||||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||||
|
|
||||||
|
buffer-image-size@0.6.4:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.13.2
|
||||||
|
|
||||||
caniuse-lite@1.0.30001799: {}
|
caniuse-lite@1.0.30001799: {}
|
||||||
|
|
||||||
chai@6.2.2: {}
|
chai@6.2.2: {}
|
||||||
@ -2401,6 +2806,13 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
css-tree@3.2.1:
|
||||||
|
dependencies:
|
||||||
|
mdn-data: 2.27.1
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
css.escape@1.5.1: {}
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
d3-color@3.1.0: {}
|
d3-color@3.1.0: {}
|
||||||
@ -2444,18 +2856,37 @@ snapshots:
|
|||||||
graphlib: 2.1.8
|
graphlib: 2.1.8
|
||||||
lodash: 4.18.1
|
lodash: 4.18.1
|
||||||
|
|
||||||
|
data-urls@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
whatwg-mimetype: 5.0.0
|
||||||
|
whatwg-url: 16.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@noble/hashes'
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decimal.js@10.6.0: {}
|
||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
|
dequal@2.0.3: {}
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
detect-node-es@1.1.0: {}
|
detect-node-es@1.1.0: {}
|
||||||
|
|
||||||
|
dom-accessibility-api@0.5.16: {}
|
||||||
|
|
||||||
|
dom-accessibility-api@0.6.3: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.372: {}
|
electron-to-chromium@1.5.372: {}
|
||||||
|
|
||||||
|
entities@7.0.1: {}
|
||||||
|
|
||||||
|
entities@8.0.0: {}
|
||||||
|
|
||||||
es-module-lexer@2.1.0: {}
|
es-module-lexer@2.1.0: {}
|
||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
@ -2602,28 +3033,77 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lodash: 4.18.1
|
lodash: 4.18.1
|
||||||
|
|
||||||
|
happy-dom@20.10.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.13.2
|
||||||
|
'@types/whatwg-mimetype': 3.0.2
|
||||||
|
'@types/ws': 8.18.1
|
||||||
|
buffer-image-size: 0.6.4
|
||||||
|
entities: 7.0.1
|
||||||
|
whatwg-mimetype: 3.0.0
|
||||||
|
ws: 8.21.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
hermes-estree@0.25.1: {}
|
hermes-estree@0.25.1: {}
|
||||||
|
|
||||||
hermes-parser@0.25.1:
|
hermes-parser@0.25.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
hermes-estree: 0.25.1
|
hermes-estree: 0.25.1
|
||||||
|
|
||||||
|
html-encoding-sniffer@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@exodus/bytes': 1.15.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@noble/hashes'
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
|
|
||||||
imurmurhash@0.1.4: {}
|
imurmurhash@0.1.4: {}
|
||||||
|
|
||||||
|
indent-string@4.0.0: {}
|
||||||
|
|
||||||
is-extglob@2.1.1: {}
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
is-glob@4.0.3:
|
is-glob@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-extglob: 2.1.1
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
|
is-potential-custom-element-name@1.0.1: {}
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
|
jsdom@29.1.1:
|
||||||
|
dependencies:
|
||||||
|
'@asamuzakjp/css-color': 5.1.11
|
||||||
|
'@asamuzakjp/dom-selector': 7.1.1
|
||||||
|
'@bramus/specificity': 2.4.2
|
||||||
|
'@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1)
|
||||||
|
'@exodus/bytes': 1.15.1
|
||||||
|
css-tree: 3.2.1
|
||||||
|
data-urls: 7.0.0
|
||||||
|
decimal.js: 10.6.0
|
||||||
|
html-encoding-sniffer: 6.0.0
|
||||||
|
is-potential-custom-element-name: 1.0.1
|
||||||
|
lru-cache: 11.5.1
|
||||||
|
parse5: 8.0.1
|
||||||
|
saxes: 6.0.0
|
||||||
|
symbol-tree: 3.2.4
|
||||||
|
tough-cookie: 6.0.1
|
||||||
|
undici: 7.27.2
|
||||||
|
w3c-xmlserializer: 5.0.0
|
||||||
|
webidl-conversions: 8.0.1
|
||||||
|
whatwg-mimetype: 5.0.0
|
||||||
|
whatwg-url: 16.0.1
|
||||||
|
xml-name-validator: 5.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@noble/hashes'
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
json-buffer@3.0.1: {}
|
json-buffer@3.0.1: {}
|
||||||
@ -2698,14 +3178,22 @@ snapshots:
|
|||||||
|
|
||||||
lodash@4.18.1: {}
|
lodash@4.18.1: {}
|
||||||
|
|
||||||
|
lru-cache@11.5.1: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
|
lz-string@1.5.0: {}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
mdn-data@2.27.1: {}
|
||||||
|
|
||||||
|
min-indent@1.0.1: {}
|
||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.6
|
brace-expansion: 5.0.6
|
||||||
@ -2743,6 +3231,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
|
|
||||||
|
parse5@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
entities: 8.0.0
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
@ -2769,6 +3261,12 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
pretty-format@27.5.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
ansi-styles: 5.2.0
|
||||||
|
react-is: 17.0.2
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
react-dom@19.2.7(react@19.2.7):
|
react-dom@19.2.7(react@19.2.7):
|
||||||
@ -2776,6 +3274,8 @@ snapshots:
|
|||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
scheduler: 0.27.0
|
scheduler: 0.27.0
|
||||||
|
|
||||||
|
react-is@17.0.2: {}
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7):
|
react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
@ -2819,6 +3319,13 @@ snapshots:
|
|||||||
- '@types/react'
|
- '@types/react'
|
||||||
- immer
|
- immer
|
||||||
|
|
||||||
|
redent@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
indent-string: 4.0.0
|
||||||
|
strip-indent: 3.0.0
|
||||||
|
|
||||||
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
rolldown@1.0.3:
|
rolldown@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.133.0
|
'@oxc-project/types': 0.133.0
|
||||||
@ -2840,6 +3347,10 @@ snapshots:
|
|||||||
'@rolldown/binding-win32-arm64-msvc': 1.0.3
|
'@rolldown/binding-win32-arm64-msvc': 1.0.3
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.0.3
|
'@rolldown/binding-win32-x64-msvc': 1.0.3
|
||||||
|
|
||||||
|
saxes@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
xmlchars: 2.2.0
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
@ -2860,6 +3371,12 @@ snapshots:
|
|||||||
|
|
||||||
std-env@4.1.0: {}
|
std-env@4.1.0: {}
|
||||||
|
|
||||||
|
strip-indent@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
min-indent: 1.0.1
|
||||||
|
|
||||||
|
symbol-tree@3.2.4: {}
|
||||||
|
|
||||||
tinybench@2.9.0: {}
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
tinyexec@1.2.4: {}
|
tinyexec@1.2.4: {}
|
||||||
@ -2871,6 +3388,20 @@ snapshots:
|
|||||||
|
|
||||||
tinyrainbow@3.1.0: {}
|
tinyrainbow@3.1.0: {}
|
||||||
|
|
||||||
|
tldts-core@7.4.2: {}
|
||||||
|
|
||||||
|
tldts@7.4.2:
|
||||||
|
dependencies:
|
||||||
|
tldts-core: 7.4.2
|
||||||
|
|
||||||
|
tough-cookie@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
tldts: 7.4.2
|
||||||
|
|
||||||
|
tr46@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
punycode: 2.3.1
|
||||||
|
|
||||||
ts-api-utils@2.5.0(typescript@6.0.3):
|
ts-api-utils@2.5.0(typescript@6.0.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
@ -2896,6 +3427,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.18.2: {}
|
undici-types@7.18.2: {}
|
||||||
|
|
||||||
|
undici@7.27.2: {}
|
||||||
|
|
||||||
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.28.2
|
browserslist: 4.28.2
|
||||||
@ -2936,7 +3469,7 @@ snapshots:
|
|||||||
'@types/node': 24.13.2
|
'@types/node': 24.13.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
vitest@4.1.8(@types/node@24.13.2)(vite@8.0.16(@types/node@24.13.2)):
|
vitest@4.1.8(@types/node@24.13.2)(happy-dom@20.10.3)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.8
|
'@vitest/expect': 4.1.8
|
||||||
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.13.2))
|
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.13.2))
|
||||||
@ -2960,9 +3493,29 @@ snapshots:
|
|||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.13.2
|
'@types/node': 24.13.2
|
||||||
|
happy-dom: 20.10.3
|
||||||
|
jsdom: 29.1.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
|
||||||
|
w3c-xmlserializer@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
xml-name-validator: 5.0.0
|
||||||
|
|
||||||
|
webidl-conversions@8.0.1: {}
|
||||||
|
|
||||||
|
whatwg-mimetype@3.0.0: {}
|
||||||
|
|
||||||
|
whatwg-mimetype@5.0.0: {}
|
||||||
|
|
||||||
|
whatwg-url@16.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@exodus/bytes': 1.15.1
|
||||||
|
tr46: 6.0.0
|
||||||
|
webidl-conversions: 8.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@noble/hashes'
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
@ -2974,6 +3527,12 @@ snapshots:
|
|||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
|
ws@8.21.0: {}
|
||||||
|
|
||||||
|
xml-name-validator@5.0.0: {}
|
||||||
|
|
||||||
|
xmlchars@2.2.0: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|||||||
248
qa/human_qa.mjs
Normal file
248
qa/human_qa.mjs
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
// Real human-style QA against https://canvas.flow-master.ai.
|
||||||
|
// Drives the live site through every flow a person would touch and
|
||||||
|
// captures: console errors, network errors, real action round-trips,
|
||||||
|
// identity switching, dark/light, mobile width, screenshot evidence.
|
||||||
|
//
|
||||||
|
// Bypasses local DNS cache by mapping to the LB IP directly.
|
||||||
|
import { chromium, devices } from "playwright";
|
||||||
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const URL = "https://canvas.flow-master.ai/";
|
||||||
|
const FORCE_IP = "65.21.71.186";
|
||||||
|
const OUT = "qa/screenshots/human";
|
||||||
|
mkdirSync(OUT, { recursive: true });
|
||||||
|
|
||||||
|
const findings = [];
|
||||||
|
const note = (kind, where, msg) => {
|
||||||
|
const f = { kind, where, msg };
|
||||||
|
findings.push(f);
|
||||||
|
console.log(`[${kind}] ${where} :: ${msg}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function attach(page, label) {
|
||||||
|
page.on("pageerror", (e) => note("pageerror", label, e.message.slice(0, 200)));
|
||||||
|
page.on("console", (m) => {
|
||||||
|
if (m.type() === "error") note("console.error", label, m.text().slice(0, 200));
|
||||||
|
if (m.type() === "warning") note("console.warning", label, m.text().slice(0, 200));
|
||||||
|
});
|
||||||
|
page.on("requestfailed", (r) => note("requestfailed", label, `${r.url()} :: ${r.failure()?.errorText}`));
|
||||||
|
page.on("response", (r) => {
|
||||||
|
if (r.status() >= 400) note("http", label, `${r.status()} ${r.url()}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: true,
|
||||||
|
args: [
|
||||||
|
`--host-resolver-rules=MAP canvas.flow-master.ai ${FORCE_IP}`,
|
||||||
|
"--ignore-certificate-errors",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FLOW 1 — desktop landing → mission → real action round-trip
|
||||||
|
// =========================================================================
|
||||||
|
{
|
||||||
|
console.log("\n=== FLOW 1: desktop full walkthrough ===");
|
||||||
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, ignoreHTTPSErrors: true });
|
||||||
|
const p = await ctx.newPage();
|
||||||
|
await attach(p, "F1");
|
||||||
|
const tApiCalls = [];
|
||||||
|
p.on("request", (req) => { if (req.url().includes("/api/")) tApiCalls.push({ url: req.url(), at: Date.now() }); });
|
||||||
|
|
||||||
|
await p.goto(URL, { waitUntil: "networkidle" });
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-01-landing.png` });
|
||||||
|
|
||||||
|
// R1.S6: scan the page for the word 'demo'
|
||||||
|
const visibleText = await p.evaluate(() => document.body.innerText);
|
||||||
|
const demoHits = (visibleText.match(/\bdemo\b/gi) || []).length;
|
||||||
|
if (demoHits > 0) note("de-demo", "landing", `'demo' appears ${demoHits} times in visible text`);
|
||||||
|
|
||||||
|
// F1.a click first scenario card
|
||||||
|
await p.locator(".sc-card").first().click();
|
||||||
|
await p.waitForSelector(".mc"); await p.waitForTimeout(800);
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-02-mission.png` });
|
||||||
|
|
||||||
|
// F1.b switch to LIVE mode
|
||||||
|
await p.locator(".mode-toggle").first().click();
|
||||||
|
const liveOk = await p.waitForFunction(
|
||||||
|
() => Array.from(document.querySelectorAll(".mode-pill")).some((el) => el.textContent?.trim() === "LIVE"),
|
||||||
|
{ timeout: 15000 },
|
||||||
|
).then(() => true).catch(() => false);
|
||||||
|
if (!liveOk) note("flow", "mission", "LIVE toggle did not flip");
|
||||||
|
await p.waitForTimeout(2500);
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-03-live.png` });
|
||||||
|
|
||||||
|
// F1.c open console drawer
|
||||||
|
await p.locator(".link-btn", { hasText: /console/i }).first().click();
|
||||||
|
await p.waitForSelector(".console"); await p.waitForTimeout(400);
|
||||||
|
const callsInConsole = await p.locator(".call").count();
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-04-console.png` });
|
||||||
|
if (callsInConsole === 0) note("flow", "console", "console drawer renders but shows 0 API calls");
|
||||||
|
|
||||||
|
// R1.S5: GET-STORM test — sit idle on mission for 60s, count /api calls
|
||||||
|
console.log("idle 60s to measure GET storm…");
|
||||||
|
const idleStart = Date.now();
|
||||||
|
const beforeIdle = tApiCalls.length;
|
||||||
|
await p.waitForTimeout(60000);
|
||||||
|
const afterIdle = tApiCalls.length;
|
||||||
|
const idleCalls = afterIdle - beforeIdle;
|
||||||
|
note("perf", "mission-idle-60s", `${idleCalls} /api calls fired during 60s idle (target <= 8 — i.e. one poll-cycle worth)`);
|
||||||
|
|
||||||
|
// F1.d sign in via Settings quick-user (current build path)
|
||||||
|
await p.locator(".tab", { hasText: /settings/i }).click();
|
||||||
|
await p.waitForSelector(".settings"); await p.waitForTimeout(300);
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-05-settings.png` });
|
||||||
|
const quickUserBtns = p.locator(".quick-users .link-btn");
|
||||||
|
const quickUserCount = await quickUserBtns.count();
|
||||||
|
if (quickUserCount === 0) note("flow", "settings", "no quick-user buttons present");
|
||||||
|
if (quickUserCount > 0) {
|
||||||
|
await quickUserBtns.first().click();
|
||||||
|
await p.waitForTimeout(2500);
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-06-signed-in.png` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// F1.e try to start a real instance from Mission → LeftRail
|
||||||
|
await p.locator(".tab", { hasText: /mission/i }).click();
|
||||||
|
await p.waitForSelector(".mc"); await p.waitForTimeout(600);
|
||||||
|
const startBtn = p.locator(".start-btn");
|
||||||
|
const startBtnExists = await startBtn.count();
|
||||||
|
if (startBtnExists > 0) {
|
||||||
|
const beforeStart = tApiCalls.filter((c) => c.url.includes("/api/runtime/transactions") && !c.url.includes("/actions/")).length;
|
||||||
|
await startBtn.first().click();
|
||||||
|
await p.waitForTimeout(3000);
|
||||||
|
const afterStart = tApiCalls.filter((c) => c.url.includes("/api/runtime/transactions") && !c.url.includes("/actions/")).length;
|
||||||
|
if (afterStart === beforeStart) {
|
||||||
|
note("flow", "start-instance", "click did not fire POST /api/runtime/transactions");
|
||||||
|
} else {
|
||||||
|
note("ok", "start-instance", `POST /api/runtime/transactions fired ${afterStart - beforeStart} time(s)`);
|
||||||
|
}
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-07-after-start.png` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// F1.f try to fire Submit on the inspector overview
|
||||||
|
await p.locator(".qcard").first().click().catch(() => {});
|
||||||
|
await p.waitForTimeout(400);
|
||||||
|
await p.locator(".itab", { hasText: /overview/i }).click().catch(() => {});
|
||||||
|
await p.waitForTimeout(200);
|
||||||
|
const inspectorBtns = await p.locator(".i-actions .btn").count();
|
||||||
|
if (inspectorBtns > 0) {
|
||||||
|
await p.locator(".i-actions .btn").first().click();
|
||||||
|
await p.waitForTimeout(2000);
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-08-after-submit.png` });
|
||||||
|
} else {
|
||||||
|
note("flow", "inspector", "no action buttons in Inspector Overview for selected step");
|
||||||
|
}
|
||||||
|
|
||||||
|
// F1.g try the Process Studio publish path
|
||||||
|
await p.locator(".tab", { hasText: /studio/i }).click();
|
||||||
|
await p.waitForSelector(".studio"); await p.waitForTimeout(500);
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-09-studio.png` });
|
||||||
|
const publishBtn = p.locator(".studio-head .btn", { hasText: /publish/i });
|
||||||
|
if (await publishBtn.count() > 0) {
|
||||||
|
const beforePublish = tApiCalls.filter((c) => c.url.includes("/api/ea2/flow")).length;
|
||||||
|
await publishBtn.click();
|
||||||
|
await p.waitForTimeout(3000);
|
||||||
|
const afterPublish = tApiCalls.filter((c) => c.url.includes("/api/ea2/flow")).length;
|
||||||
|
if (afterPublish === beforePublish) {
|
||||||
|
note("flow", "studio-publish", "Publish click did not fire POST /api/ea2/flow");
|
||||||
|
} else {
|
||||||
|
note("ok", "studio-publish", `POST /api/ea2/flow fired ${afterPublish - beforePublish} time(s)`);
|
||||||
|
}
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-10-after-publish.png` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// F1.h check Run History
|
||||||
|
await p.locator(".tab", { hasText: /runs/i }).click();
|
||||||
|
await p.waitForSelector(".rh"); await p.waitForTimeout(300);
|
||||||
|
const rhRows = await p.locator(".rh-row").count();
|
||||||
|
if (rhRows === 0) note("flow", "run-history", "RunHistory shows 0 rows even in live mode");
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-11-runs.png` });
|
||||||
|
|
||||||
|
// F1.i ⌘K palette
|
||||||
|
await p.keyboard.press("Meta+k");
|
||||||
|
await p.waitForSelector(".cmd").catch(() => note("flow", "cmd-palette", "⌘K did not open palette"));
|
||||||
|
await p.waitForTimeout(200);
|
||||||
|
await p.screenshot({ path: `${OUT}/f1-12-palette.png` });
|
||||||
|
await p.keyboard.press("Escape");
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FLOW 2 — mobile viewport (regional store manager on a phone)
|
||||||
|
// =========================================================================
|
||||||
|
{
|
||||||
|
console.log("\n=== FLOW 2: mobile (iPhone 14 Pro) ===");
|
||||||
|
const ctx = await browser.newContext({ ...devices["iPhone 14 Pro"], ignoreHTTPSErrors: true });
|
||||||
|
const p = await ctx.newPage();
|
||||||
|
await attach(p, "F2-mobile");
|
||||||
|
await p.goto(URL, { waitUntil: "networkidle" });
|
||||||
|
await p.screenshot({ path: `${OUT}/f2-01-mobile-landing.png` });
|
||||||
|
const horizontalScroll = await p.evaluate(() => document.documentElement.scrollWidth > window.innerWidth + 2);
|
||||||
|
if (horizontalScroll) note("mobile", "landing", "horizontal scroll present on mobile — layout overflow");
|
||||||
|
const cards = await p.locator(".sc-card").count();
|
||||||
|
if (cards === 0) note("mobile", "landing", "0 scenario cards visible on mobile");
|
||||||
|
if (cards > 0) {
|
||||||
|
await p.locator(".sc-card").first().click();
|
||||||
|
await p.waitForTimeout(800);
|
||||||
|
await p.screenshot({ path: `${OUT}/f2-02-mobile-mission.png` });
|
||||||
|
const mcVisible = await p.locator(".mc-body").isVisible();
|
||||||
|
if (!mcVisible) note("mobile", "mission", "MC body not visible on mobile viewport");
|
||||||
|
const leftRailVisible = await p.locator(".left-rail").isVisible();
|
||||||
|
if (!leftRailVisible) note("mobile", "mission", "left rail invisible at mobile width (acceptable but worth noting)");
|
||||||
|
}
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FLOW 3 — reload + deep state check (do we lose mode/scenario across reload?)
|
||||||
|
// =========================================================================
|
||||||
|
{
|
||||||
|
console.log("\n=== FLOW 3: persistence across reload ===");
|
||||||
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, ignoreHTTPSErrors: true });
|
||||||
|
const p = await ctx.newPage();
|
||||||
|
await attach(p, "F3");
|
||||||
|
await p.goto(URL, { waitUntil: "networkidle" });
|
||||||
|
await p.locator(".sc-card").nth(3).click();
|
||||||
|
await p.waitForSelector(".mc"); await p.waitForTimeout(500);
|
||||||
|
const titleBefore = await p.locator(".mc-hero-title").innerText();
|
||||||
|
await p.reload({ waitUntil: "networkidle" });
|
||||||
|
await p.waitForTimeout(500);
|
||||||
|
// The shell defaults to Landing on reload — is that intentional?
|
||||||
|
const landingBack = await p.locator(".landing").count();
|
||||||
|
if (landingBack > 0) note("persistence", "reload", `reloading from /mission landed on Landing again (scene not persisted); title before was '${titleBefore}'`);
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FLOW 4 — security headers (curl-equivalent via fetch)
|
||||||
|
// =========================================================================
|
||||||
|
{
|
||||||
|
console.log("\n=== FLOW 4: security headers ===");
|
||||||
|
const ctx = await browser.newContext({ ignoreHTTPSErrors: true });
|
||||||
|
const p = await ctx.newPage();
|
||||||
|
const resp = await p.goto(URL);
|
||||||
|
const h = resp?.headers() ?? {};
|
||||||
|
const required = [
|
||||||
|
"strict-transport-security",
|
||||||
|
"x-content-type-options",
|
||||||
|
"x-frame-options",
|
||||||
|
"referrer-policy",
|
||||||
|
"content-security-policy",
|
||||||
|
];
|
||||||
|
for (const k of required) {
|
||||||
|
if (!h[k]) note("security", "headers", `missing ${k}`);
|
||||||
|
else note("ok", "headers", `${k}: ${h[k].slice(0, 80)}`);
|
||||||
|
}
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Write the findings dump
|
||||||
|
writeFileSync(`${OUT}/findings.json`, JSON.stringify(findings, null, 2));
|
||||||
|
const byKind = findings.reduce((acc, f) => { acc[f.kind] = (acc[f.kind] || 0) + 1; return acc; }, {});
|
||||||
|
console.log("\n=== SUMMARY ===");
|
||||||
|
console.log(JSON.stringify(byKind, null, 2));
|
||||||
|
console.log(`\n→ ${findings.length} findings written to ${OUT}/findings.json`);
|
||||||
|
console.log(`→ screenshots in ${OUT}/`);
|
||||||
@ -9,6 +9,7 @@ const DOCTRINE = new Set([
|
|||||||
"#1a2740", "#243453",
|
"#1a2740", "#243453",
|
||||||
"#4a5b80", "#7a8aa8",
|
"#4a5b80", "#7a8aa8",
|
||||||
"#c46a14", "#3d6a2c", "#a6342a", "#1d6f82",
|
"#c46a14", "#3d6a2c", "#a6342a", "#1d6f82",
|
||||||
|
"#0c1322", "#e6edf7",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function fail(msg) { console.log("✗", msg); process.exitCode = 1; }
|
function fail(msg) { console.log("✗", msg); process.exitCode = 1; }
|
||||||
|
|||||||
3
qa/test.mjs
Normal file
3
qa/test.mjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
const css = fs.readFileSync('src/index.css', 'utf-8');
|
||||||
|
console.log(css.includes('[data-theme="dark"]'));
|
||||||
1
setupTests.ts
Normal file
1
setupTests.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
25
src/App.tsx
25
src/App.tsx
@ -4,12 +4,14 @@ import { useApp, scenarioById } from "./state/store";
|
|||||||
import Landing from "./scenes/Landing";
|
import Landing from "./scenes/Landing";
|
||||||
import MissionControl from "./scenes/MissionControl";
|
import MissionControl from "./scenes/MissionControl";
|
||||||
import RunHistory from "./scenes/RunHistory";
|
import RunHistory from "./scenes/RunHistory";
|
||||||
import Studio from "./scenes/Studio";
|
import Wizard from "./scenes/Wizard";
|
||||||
import Settings from "./scenes/Settings";
|
import Settings from "./scenes/Settings";
|
||||||
|
import Login from "./scenes/Login";
|
||||||
|
import SsoCallback from "./scenes/SsoCallback";
|
||||||
import CommandBar from "./components/CommandBar";
|
import CommandBar from "./components/CommandBar";
|
||||||
import Toaster from "./components/Toaster";
|
import Toaster from "./components/Toaster";
|
||||||
import Console from "./components/Console";
|
import Console from "./components/Console";
|
||||||
import { Cmd, Home, Layers, History as HistoryIcon, Pulse, Refresh, Branch, Cog, User } from "./components/icons";
|
import { Cmd, Home, Layers, HistoryIcon, Pulse, Refresh, Branch, Cog, User, Sun, Moon } from "./components/icons";
|
||||||
import { liveMeta } from "./data/scenarios";
|
import { liveMeta } from "./data/scenarios";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@ -30,6 +32,16 @@ export default function App() {
|
|||||||
const userEmail = useApp((s) => s.userEmail);
|
const userEmail = useApp((s) => s.userEmail);
|
||||||
const startPolling = useApp((s) => s.startPolling);
|
const startPolling = useApp((s) => s.startPolling);
|
||||||
const stopPolling = useApp((s) => s.stopPolling);
|
const stopPolling = useApp((s) => s.stopPolling);
|
||||||
|
const isAuthed = useApp((s) => s.isAuthed);
|
||||||
|
|
||||||
|
// Auth guard and SSO callback detector
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined" && window.location.hash.includes("access_token")) {
|
||||||
|
setScene("sso-callback");
|
||||||
|
} else if (!isAuthed && scene !== "login" && scene !== "sso-callback") {
|
||||||
|
setScene("login");
|
||||||
|
}
|
||||||
|
}, [isAuthed, scene, setScene]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode === "live") startPolling();
|
if (mode === "live") startPolling();
|
||||||
@ -40,7 +52,7 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`shell shell-${scene}`}>
|
<div className={`shell shell-${scene}`}>
|
||||||
{scene !== "landing" && (
|
{scene !== "landing" && scene !== "login" && scene !== "sso-callback" && (
|
||||||
<header className="topbar" data-anchor="topbar">
|
<header className="topbar" data-anchor="topbar">
|
||||||
<button className="brand-lock brand-btn" onClick={() => setScene("landing")} aria-label="Home">
|
<button className="brand-lock brand-btn" onClick={() => setScene("landing")} aria-label="Home">
|
||||||
<span className="brand-mark sm" />
|
<span className="brand-mark sm" />
|
||||||
@ -121,6 +133,9 @@ export default function App() {
|
|||||||
<Layers size={12} /> Console
|
<Layers size={12} /> Console
|
||||||
{apiLogCount > 0 && <span className="badge mono">{apiLogCount}</span>}
|
{apiLogCount > 0 && <span className="badge mono">{apiLogCount}</span>}
|
||||||
</button>
|
</button>
|
||||||
|
<button className="link-btn" onClick={() => useApp.getState().setTheme(useApp.getState().theme === "dark" ? "light" : "dark")} title="Toggle theme">
|
||||||
|
{useApp.getState().theme === "dark" ? <Sun size={13} /> : <Moon size={13} />}
|
||||||
|
</button>
|
||||||
<button className="link-btn" onClick={() => setCmdOpen(true)}>
|
<button className="link-btn" onClick={() => setCmdOpen(true)}>
|
||||||
<Cmd size={13} /> <kbd>⌘K</kbd>
|
<Cmd size={13} /> <kbd>⌘K</kbd>
|
||||||
</button>
|
</button>
|
||||||
@ -129,10 +144,12 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="scene">
|
<div className="scene">
|
||||||
|
{scene === "login" && <Login />}
|
||||||
|
{scene === "sso-callback" && <SsoCallback />}
|
||||||
{scene === "landing" && <Landing />}
|
{scene === "landing" && <Landing />}
|
||||||
{scene === "mission" && <MissionControl />}
|
{scene === "mission" && <MissionControl />}
|
||||||
{scene === "history" && <RunHistory />}
|
{scene === "history" && <RunHistory />}
|
||||||
{scene === "studio" && <Studio />}
|
{scene === "studio" && <Wizard />}
|
||||||
{scene === "settings" && <Settings />}
|
{scene === "settings" && <Settings />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Command } from "cmdk";
|
import { Command } from "cmdk";
|
||||||
import { useApp, scenarioById } from "../state/store";
|
import { useApp, scenarioById } from "../state/store";
|
||||||
import { Play, Branch, Bot, Check, Home, History as HistoryIcon, Layers, Search, Refresh, Cog, User } from "./icons";
|
import { Play, Branch, Bot, Check, Home, HistoryIcon, Layers, Search, Refresh, Cog, User } from "./icons";
|
||||||
|
|
||||||
export default function CommandBar() {
|
export default function CommandBar() {
|
||||||
const open = useApp((s) => s.cmdOpen);
|
const open = useApp((s) => s.cmdOpen);
|
||||||
@ -80,7 +80,7 @@ export default function CommandBar() {
|
|||||||
<HistoryIcon size={13} /> Run History
|
<HistoryIcon size={13} /> Run History
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
<Command.Item onSelect={() => { setScene("studio"); close(); }}>
|
<Command.Item onSelect={() => { setScene("studio"); close(); }}>
|
||||||
<Branch size={13} /> Process Studio
|
<Branch size={13} /> Process Wizard
|
||||||
<span className="cmd-hint">design & publish a new process</span>
|
<span className="cmd-hint">design & publish a new process</span>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
<Command.Item onSelect={() => { setScene("settings"); close(); }}>
|
<Command.Item onSelect={() => { setScene("settings"); close(); }}>
|
||||||
@ -120,7 +120,7 @@ export default function CommandBar() {
|
|||||||
onSelect={async () => {
|
onSelect={async () => {
|
||||||
close();
|
close();
|
||||||
if (mode !== "live") { pushToast("warn", "Switch to LIVE mode to start a real instance."); return; }
|
if (mode !== "live") { pushToast("warn", "Switch to LIVE mode to start a real instance."); return; }
|
||||||
await startInstance(sc.defKey, `MC demo · ${new Date().toLocaleString()}`);
|
await startInstance(sc.defKey, `Started via Mission Control · ${new Date().toLocaleString()}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Play size={13} /> Start new instance of "{sc.defName}"
|
<Play size={13} /> Start new instance of "{sc.defName}"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Tabbed Inspector for the selected step.
|
// Tabbed Inspector for the selected step.
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useApp, scenarioById } from "../state/store";
|
import { useApp, scenarioById } from "../state/store";
|
||||||
import { Shield, Doc, Layers, Pulse, History } from "./icons";
|
import { Shield, Doc, Layers, Pulse, HistoryIcon } from "./icons";
|
||||||
|
|
||||||
const STATE_LABEL: Record<string, string> = {
|
const STATE_LABEL: Record<string, string> = {
|
||||||
done: "Done", running: "Running", queued: "Queued",
|
done: "Done", running: "Running", queued: "Queued",
|
||||||
@ -12,7 +12,7 @@ const TABS: Array<{ id: "overview" | "rules" | "evidence" | "raw" | "runs"; labe
|
|||||||
{ id: "overview", label: "Overview", Icon: Doc },
|
{ id: "overview", label: "Overview", Icon: Doc },
|
||||||
{ id: "rules", label: "Rules", Icon: Shield },
|
{ id: "rules", label: "Rules", Icon: Shield },
|
||||||
{ id: "evidence", label: "Evidence", Icon: Pulse },
|
{ id: "evidence", label: "Evidence", Icon: Pulse },
|
||||||
{ id: "runs", label: "Runs", Icon: History },
|
{ id: "runs", label: "Runs", Icon: HistoryIcon },
|
||||||
{ id: "raw", label: "Raw", Icon: Layers },
|
{ id: "raw", label: "Raw", Icon: Layers },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -133,7 +133,7 @@ function StartInstanceButton({ defKey, live }: { defKey: string; live: boolean }
|
|||||||
}
|
}
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await startInstance(defKey, `MC demo · ${new Date().toLocaleString()}`);
|
await startInstance(defKey, `Started via Mission Control · ${new Date().toLocaleString()}`);
|
||||||
} finally { setBusy(false); }
|
} finally { setBusy(false); }
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -24,6 +24,9 @@ export const Doc = (p: P) => <Svg {...p}><path d="M7 3h7l4 4v14H7zM14 3v4h4" /><
|
|||||||
export const Cog = (p: P) => <Svg {...p}><circle cx="12" cy="12" r="3" /><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.6 4.6l2.1 2.1M17.3 17.3l2.1 2.1M4.6 19.4l2.1-2.1M17.3 6.7l2.1-2.1" /></Svg>;
|
export const Cog = (p: P) => <Svg {...p}><circle cx="12" cy="12" r="3" /><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.6 4.6l2.1 2.1M17.3 17.3l2.1 2.1M4.6 19.4l2.1-2.1M17.3 6.7l2.1-2.1" /></Svg>;
|
||||||
export const Flag = (p: P) => <Svg {...p}><path d="M5 21V4h13l-2 4 2 4H5" /></Svg>;
|
export const Flag = (p: P) => <Svg {...p}><path d="M5 21V4h13l-2 4 2 4H5" /></Svg>;
|
||||||
export const Arrow = (p: P) => <Svg {...p}><path d="M5 12h14M13 5l7 7-7 7" /></Svg>;
|
export const Arrow = (p: P) => <Svg {...p}><path d="M5 12h14M13 5l7 7-7 7" /></Svg>;
|
||||||
|
export const Sun = (p: P) => <Svg {...p}><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></Svg>;
|
||||||
|
export const Moon = (p: P) => <Svg {...p}><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></Svg>;
|
||||||
|
|
||||||
export const ArrowUp = (p: P) => <Svg {...p}><path d="M12 19V5M5 12l7-7 7 7" /></Svg>;
|
export const ArrowUp = (p: P) => <Svg {...p}><path d="M12 19V5M5 12l7-7 7 7" /></Svg>;
|
||||||
export const ArrowDown = (p: P) => <Svg {...p}><path d="M12 5v14M5 12l7 7 7-7" /></Svg>;
|
export const ArrowDown = (p: P) => <Svg {...p}><path d="M12 5v14M5 12l7 7 7-7" /></Svg>;
|
||||||
export const Sparkles = (p: P) => <Svg {...p}><path d="M12 3l1.7 4.3L18 9l-4.3 1.7L12 15l-1.7-4.3L6 9l4.3-1.7zM19 14l.9 2.1L22 17l-2.1.9L19 20l-.9-2.1L16 17l2.1-.9z" /></Svg>;
|
export const Sparkles = (p: P) => <Svg {...p}><path d="M12 3l1.7 4.3L18 9l-4.3 1.7L12 15l-1.7-4.3L6 9l4.3-1.7zM19 14l.9 2.1L22 17l-2.1.9L19 20l-.9-2.1L16 17l2.1-.9z" /></Svg>;
|
||||||
@ -35,5 +38,6 @@ export const Search = (p: P) => <Svg {...p}><circle cx="11" cy="11" r="7" /><pat
|
|||||||
export const Spark = (p: P) => <Svg {...p}><polyline points="3,16 8,11 12,15 17,8 21,12" /></Svg>;
|
export const Spark = (p: P) => <Svg {...p}><polyline points="3,16 8,11 12,15 17,8 21,12" /></Svg>;
|
||||||
export const Pulse = (p: P) => <Svg {...p}><path d="M3 12h4l3-8 4 16 3-8h4" /></Svg>;
|
export const Pulse = (p: P) => <Svg {...p}><path d="M3 12h4l3-8 4 16 3-8h4" /></Svg>;
|
||||||
export const Layers = (p: P) => <Svg {...p}><path d="M12 3l9 5-9 5-9-5zM3 13l9 5 9-5M3 18l9 5 9-5" /></Svg>;
|
export const Layers = (p: P) => <Svg {...p}><path d="M12 3l9 5-9 5-9-5zM3 13l9 5 9-5M3 18l9 5 9-5" /></Svg>;
|
||||||
export const History = (p: P) => <Svg {...p}><path d="M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5M12 7v5l3 2" /></Svg>;
|
export const HistoryIcon = (p: P) => <Svg {...p}><path d="M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5M12 7v5l3 2" /></Svg>;
|
||||||
export const Home = (p: P) => <Svg {...p}><path d="M4 11l8-7 8 7v9h-5v-6h-6v6H4z" /></Svg>;
|
export const Home = (p: P) => <Svg {...p}><path d="M4 11l8-7 8 7v9h-5v-6h-6v6H4z" /></Svg>;
|
||||||
|
export const ChevronRight = (p: P) => <Svg {...p}><path d="M9 18l6-6-6-6" /></Svg>;
|
||||||
|
|||||||
@ -81,7 +81,7 @@ function baseTour(familyId: string): TourStep[] {
|
|||||||
{ id: "t1", anchor: "graph", title: `${familyId} industry blueprint`, body: `This is the canonical ${familyId} process modelled in FlowMaster's typed format — start, agent/service/human steps, decision branches, rules, evidence. Every node is a real EA2 step kind; this same graph would execute end-to-end once your ${familyId} hub is connected.` },
|
{ id: "t1", anchor: "graph", title: `${familyId} industry blueprint`, body: `This is the canonical ${familyId} process modelled in FlowMaster's typed format — start, agent/service/human steps, decision branches, rules, evidence. Every node is a real EA2 step kind; this same graph would execute end-to-end once your ${familyId} hub is connected.` },
|
||||||
{ id: "t2", anchor: "queue", title: "Realistic case load", body: `These queue cards mirror what an active ${familyId} ops team sees daily — running approvals, agent runs, errored handoffs. Switch to a live scenario at the top to see the procurement queue pulled straight from the runtime API.` },
|
{ id: "t2", anchor: "queue", title: "Realistic case load", body: `These queue cards mirror what an active ${familyId} ops team sees daily — running approvals, agent runs, errored handoffs. Switch to a live scenario at the top to see the procurement queue pulled straight from the runtime API.` },
|
||||||
{ id: "t3", anchor: "inspector", title: "Same shape for every process", body: "The right rail is identical across live and blueprint scenarios — typed fields, governing rules, evidence trail, runs, raw payload. That's the point: one inspector, every process family." },
|
{ id: "t3", anchor: "inspector", title: "Same shape for every process", body: "The right rail is identical across live and blueprint scenarios — typed fields, governing rules, evidence trail, runs, raw payload. That's the point: one inspector, every process family." },
|
||||||
{ id: "t4", anchor: "command", title: "Drive the demo with ⌘K", body: "Press ⌘K (or Ctrl+K) to switch scenarios, jump to a step, toggle live mode, or start a tour. Everything is one keystroke away." },
|
{ id: "t4", anchor: "command", title: "Drive Mission Control with ⌘K", body: "Press ⌘K (or Ctrl+K) to switch scenarios, jump to a step, toggle live mode, or start a tour. Everything is one keystroke away." },
|
||||||
{ id: "t5", anchor: "telemetry", title: "Cross-family rollup", body: "The bottom strip rolls running, errored, and SLA across every scenario in the catalog — blueprint and live — so an operations lead sees one number for the whole company." },
|
{ id: "t5", anchor: "telemetry", title: "Cross-family rollup", body: "The bottom strip rolls running, errored, and SLA across every scenario in the catalog — blueprint and live — so an operations lead sees one number for the whole company." },
|
||||||
{ id: "t6", anchor: "graph", title: "You're in control", body: "That's the loop. Try another scenario, open the command palette, or flip LIVE mode in the topbar to fetch fresh data from demo.flow-master.ai right in the browser." },
|
{ id: "t6", anchor: "graph", title: "You're in control", body: "That's the loop. Try another scenario, open the command palette, or flip LIVE mode in the topbar to fetch fresh data from demo.flow-master.ai right in the browser." },
|
||||||
];
|
];
|
||||||
|
|||||||
440
src/index.css
440
src/index.css
@ -8,6 +8,17 @@
|
|||||||
- light paper canvas + navy frame + amber accent
|
- light paper canvas + navy frame + amber accent
|
||||||
- 1px rules, square edges, monospace operational density
|
- 1px rules, square edges, monospace operational density
|
||||||
===================================================================== */
|
===================================================================== */
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bp-paper: #0c1322;
|
||||||
|
--bp-paper-2: #1a2740;
|
||||||
|
--bp-paper-3: #243453;
|
||||||
|
--bp-navy: #e6edf7;
|
||||||
|
--bp-navy-2: #d5dde9;
|
||||||
|
--bp-muted: #7a8aa8;
|
||||||
|
--bp-muted-2: #4a5b80;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Doctrinal hex tokens — declared once, referenced everywhere via var(). */
|
/* Doctrinal hex tokens — declared once, referenced everywhere via var(). */
|
||||||
--bp-paper: #f5f7fb;
|
--bp-paper: #f5f7fb;
|
||||||
@ -108,6 +119,8 @@ kbd {
|
|||||||
Topbar (industrial instrument strip)
|
Topbar (industrial instrument strip)
|
||||||
===================================================================== */
|
===================================================================== */
|
||||||
.topbar {
|
.topbar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 190;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto auto 1fr auto;
|
grid-template-columns: auto auto 1fr auto;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@ -1143,6 +1156,198 @@ select.studio-input { background: var(--bp-paper); }
|
|||||||
.toast-warn { border-left-color: var(--bp-amber); }
|
.toast-warn { border-left-color: var(--bp-amber); }
|
||||||
.toast-err { border-left-color: var(--bp-err); }
|
.toast-err { border-left-color: var(--bp-err); }
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
Login Page
|
||||||
|
===================================================================== */
|
||||||
|
.login-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bp-paper);
|
||||||
|
background-image: linear-gradient(var(--bp-grid-minor) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, var(--bp-grid-minor) 1px, transparent 1px);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
width: 380px;
|
||||||
|
background: var(--bp-paper);
|
||||||
|
border: 1px solid var(--bp-navy);
|
||||||
|
padding: 24px 32px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.login-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0;
|
||||||
|
width: 18px; height: 18px;
|
||||||
|
border-right: 1px solid var(--bp-navy);
|
||||||
|
border-bottom: 1px solid var(--bp-navy);
|
||||||
|
background: var(--bp-paper-2);
|
||||||
|
}
|
||||||
|
.login-head {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.login-head .brand-mark {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px; height: 24px;
|
||||||
|
background: var(--bp-amber);
|
||||||
|
border: 1px solid var(--bp-navy);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.login-head h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--bp-mono);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--bp-navy);
|
||||||
|
}
|
||||||
|
.login-sub {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--bp-muted);
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
}
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.login-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.login-field label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--bp-muted);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
.login-input {
|
||||||
|
background: var(--bp-paper);
|
||||||
|
border: 1px solid var(--bp-navy);
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-family: var(--bp-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--bp-navy);
|
||||||
|
}
|
||||||
|
.login-input:focus {
|
||||||
|
outline: 2px solid var(--bp-amber);
|
||||||
|
outline-offset: 0;
|
||||||
|
}
|
||||||
|
.login-input:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
background: var(--bp-paper-2);
|
||||||
|
}
|
||||||
|
.login-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
.login-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--bp-navy);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.login-checkbox input {
|
||||||
|
accent-color: var(--bp-amber);
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
.login-link {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--bp-amber);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.login-actions {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.login-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.login-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
.login-divider::before,
|
||||||
|
.login-divider::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: 1px solid var(--bp-navy);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
.login-divider span {
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--bp-muted);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
.login-sso-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.sso-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bp-paper);
|
||||||
|
border: 1px solid var(--bp-navy);
|
||||||
|
color: var(--bp-navy);
|
||||||
|
font-family: var(--bp-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
.sso-btn:hover:not(:disabled) {
|
||||||
|
background: var(--bp-paper-2);
|
||||||
|
}
|
||||||
|
.dev-login-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bp-paper-2);
|
||||||
|
border: 1px dashed var(--bp-navy);
|
||||||
|
color: var(--bp-muted);
|
||||||
|
font-family: var(--bp-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
transition: background 0.1s, color 0.1s;
|
||||||
|
}
|
||||||
|
.dev-login-btn:hover:not(:disabled) {
|
||||||
|
background: var(--bp-paper-3);
|
||||||
|
color: var(--bp-navy);
|
||||||
|
}
|
||||||
|
.dev-login-btn:disabled, .sso-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.login-error {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bp-err-soft);
|
||||||
|
border-left: 2px solid var(--bp-err);
|
||||||
|
color: var(--bp-err);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
/* =====================================================================
|
/* =====================================================================
|
||||||
Scrollbars
|
Scrollbars
|
||||||
===================================================================== */
|
===================================================================== */
|
||||||
@ -1347,3 +1552,238 @@ select.studio-input { background: var(--bp-paper); }
|
|||||||
[data-canvas="blueprint"] .react-flow__edge.selected .react-flow__edge-path,
|
[data-canvas="blueprint"] .react-flow__edge.selected .react-flow__edge-path,
|
||||||
[data-canvas="blueprint"] .react-flow__edge:focus .react-flow__edge-path { stroke: var(--bp-amber); }
|
[data-canvas="blueprint"] .react-flow__edge:focus .react-flow__edge-path { stroke: var(--bp-amber); }
|
||||||
[data-canvas="blueprint"] .react-flow__attribution { display: none; }
|
[data-canvas="blueprint"] .react-flow__attribution { display: none; }
|
||||||
|
|
||||||
|
/* Wizard */
|
||||||
|
.wizard-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
padding: var(--space-xl) var(--space-2xl);
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
background: var(--paper);
|
||||||
|
padding: var(--space-md);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-step-marker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
color: var(--text-3);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-step-marker.active {
|
||||||
|
background: var(--amber);
|
||||||
|
color: var(--navy);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-step-marker.past {
|
||||||
|
color: var(--text-1);
|
||||||
|
background: var(--ok-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-num {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
color: var(--paper);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-step-marker.active .step-num {
|
||||||
|
background: var(--navy);
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-step-marker.past .step-num {
|
||||||
|
background: var(--ok);
|
||||||
|
color: var(--paper);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-chevron {
|
||||||
|
color: var(--border);
|
||||||
|
margin-left: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-panel {
|
||||||
|
background: var(--paper);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: var(--space-xl);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-panel.success-panel {
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
border-color: var(--ok);
|
||||||
|
background: var(--ok-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
color: var(--ok);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-group label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-2);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-group input, .field-group textarea {
|
||||||
|
background: var(--paper-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
color: var(--text-1);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-group input:focus, .field-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-text {
|
||||||
|
color: var(--warn);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-list, .field-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card, .field-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
background: var(--paper-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-idx {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-3);
|
||||||
|
background: var(--border);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-header input, .field-row input {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--text-1);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-header input:hover, .field-row input:hover {
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-header input:focus, .field-row input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--amber);
|
||||||
|
background: var(--paper);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-meta select, .field-row select {
|
||||||
|
background: var(--paper);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
padding: var(--space-md);
|
||||||
|
background: var(--paper-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-stats div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2xs);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-stats span {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-3);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-row {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-md);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|||||||
@ -174,6 +174,13 @@ async function authedRequest<T>(
|
|||||||
export const api = {
|
export const api = {
|
||||||
config: DEFAULT_CONFIG,
|
config: DEFAULT_CONFIG,
|
||||||
|
|
||||||
|
/** Set the bearer token explicitly (e.g. from SSO or login form) */
|
||||||
|
setBearer(token: string) {
|
||||||
|
sessionStorage.setItem(TOKEN_KEY, token);
|
||||||
|
// Also set a non-httpOnly cookie for server-side middleware
|
||||||
|
document.cookie = `access_token=${token}; path=/; max-age=86400; SameSite=Lax`;
|
||||||
|
},
|
||||||
|
|
||||||
/** Subscribe to all API calls. Returns unsubscribe. */
|
/** Subscribe to all API calls. Returns unsubscribe. */
|
||||||
onCall(fn: ApiCallObserver): () => void {
|
onCall(fn: ApiCallObserver): () => void {
|
||||||
observers.add(fn);
|
observers.add(fn);
|
||||||
@ -184,6 +191,38 @@ export const api = {
|
|||||||
return authedRequest<AuthMe>(this.config, "GET", "/api/v1/auth/me", undefined, signal);
|
return authedRequest<AuthMe>(this.config, "GET", "/api/v1/auth/me", undefined, signal);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async signIn(email: string, password?: string, signal?: AbortSignal): Promise<{ access_token: string; refresh_token?: string }> {
|
||||||
|
const payload = password ? { email, password } : { email };
|
||||||
|
const path = password ? "/api/v1/auth/login" : "/api/v1/auth/dev-login";
|
||||||
|
const init: RequestInit = {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
signal,
|
||||||
|
};
|
||||||
|
const r = await instrumentedFetch("POST", `${this.config.baseUrl}${path}`, init, payload);
|
||||||
|
if (!r.ok) {
|
||||||
|
const text = await r.text().catch(() => "");
|
||||||
|
throw new Error(`Login failed: ${r.status} ${text.slice(0, 100)}`);
|
||||||
|
}
|
||||||
|
const data = await r.json() as { access_token: string; refresh_token?: string };
|
||||||
|
this.setBearer(data.access_token);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async devLoginConfig(signal?: AbortSignal): Promise<{ enabled: boolean }> {
|
||||||
|
try {
|
||||||
|
const init: RequestInit = { method: "GET", headers: { "Accept": "application/json" }, signal };
|
||||||
|
const r = await instrumentedFetch("GET", `${this.config.baseUrl}/internal/dev-login-config`, init);
|
||||||
|
if (r.ok) {
|
||||||
|
return await r.json() as { enabled: boolean };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// swallow
|
||||||
|
}
|
||||||
|
return { enabled: false };
|
||||||
|
},
|
||||||
|
|
||||||
async workItems(signal?: AbortSignal): Promise<WorkItem[]> {
|
async workItems(signal?: AbortSignal): Promise<WorkItem[]> {
|
||||||
const body = await authedRequest<{ items?: WorkItem[] }>(this.config, "GET", "/api/ea2/work-items?view=all", undefined, signal);
|
const body = await authedRequest<{ items?: WorkItem[] }>(this.config, "GET", "/api/ea2/work-items?view=all", undefined, signal);
|
||||||
return body.items ?? [];
|
return body.items ?? [];
|
||||||
@ -256,7 +295,7 @@ export const api = {
|
|||||||
{
|
{
|
||||||
kind: "definition",
|
kind: "definition",
|
||||||
status: "published",
|
status: "published",
|
||||||
source_context: "mc-demo-studio",
|
source_context: "mission-control-studio",
|
||||||
version: 1,
|
version: 1,
|
||||||
...payload,
|
...payload,
|
||||||
},
|
},
|
||||||
|
|||||||
80
src/lib/wizardApi.ts
Normal file
80
src/lib/wizardApi.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { api, type Actor } from "./api";
|
||||||
|
|
||||||
|
export interface CreateDraftPayload {
|
||||||
|
name: string;
|
||||||
|
display_name: string;
|
||||||
|
description: string;
|
||||||
|
source_context: string;
|
||||||
|
config: {
|
||||||
|
wizard: {
|
||||||
|
marker: string;
|
||||||
|
maxDepth: number;
|
||||||
|
maxNodes: number;
|
||||||
|
chat: any[];
|
||||||
|
uploads: any[];
|
||||||
|
panelState: any;
|
||||||
|
debug: any[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchOperation {
|
||||||
|
collection: string;
|
||||||
|
op: "insert" | "update" | "delete";
|
||||||
|
_key?: string;
|
||||||
|
_from?: string;
|
||||||
|
_to?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wizardApi = {
|
||||||
|
async createDraft(payload: CreateDraftPayload, signal?: AbortSignal) {
|
||||||
|
const res = await fetch(`${api.config.baseUrl}/api/ea2/flow`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
kind: "definition",
|
||||||
|
status: "draft",
|
||||||
|
...payload,
|
||||||
|
}),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`createDraft failed: ${res.status}`);
|
||||||
|
return res.json() as Promise<{ _key: string; name: string }>;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateDraftConfig(key: string, patch: any, signal?: AbortSignal) {
|
||||||
|
const res = await fetch(`${api.config.baseUrl}/api/ea2/flow/${key}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(patch),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`updateDraftConfig failed: ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async applyBatch(flow_key: string, ops: BatchOperation[], actor: Actor, signal?: AbortSignal) {
|
||||||
|
const res = await fetch(`${api.config.baseUrl}/api/ea2/apply-batch`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ flow_key, ops, actor }),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`applyBatch failed: ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async startInstance(process_definition_id: string, business_subject?: string, signal?: AbortSignal) {
|
||||||
|
return api.startTransaction(process_definition_id, business_subject, signal);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -46,12 +46,10 @@ export default function Landing() {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="hero-sub">
|
<p className="hero-sub">
|
||||||
FlowMaster turns the operational map of a company into living,
|
FlowMaster turns the operational map of a company into living,
|
||||||
typed processes — backed by humans, agents, and rules — and gives you a
|
typed processes — backed by humans, agents, and rules — and gives you
|
||||||
single command-center to drive them. Procurement scenarios are real,
|
one control surface to drive them. Every procurement, finance, people,
|
||||||
backed by EA2 on{" "}
|
and service workflow runs end-to-end through EA2 on{" "}
|
||||||
<span className="mono">{liveMeta.fetchedFrom?.replace("https://", "") ?? "demo"}</span>;
|
<span className="mono">{liveMeta.fetchedFrom?.replace("https://", "") ?? "the FlowMaster backend"}</span>.
|
||||||
AR, HCM, GL, and Service are industry blueprints showing how this
|
|
||||||
same shell extends to any process family.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="hero-actions">
|
<div className="hero-actions">
|
||||||
<button className="btn btn-primary btn-lg" onClick={() => setScene("mission")}>
|
<button className="btn btn-primary btn-lg" onClick={() => setScene("mission")}>
|
||||||
@ -129,7 +127,7 @@ export default function Landing() {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="landing-foot">
|
<footer className="landing-foot">
|
||||||
<span className="foot-eyebrow">FlowMaster · Mission Control demo · synthesised on top of demo.flow-master.ai</span>
|
<span className="foot-eyebrow">FlowMaster · Mission Control · live operator surface for FlowMaster processes</span>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
101
src/scenes/Login.test.tsx
Normal file
101
src/scenes/Login.test.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
|
import Login from "./Login";
|
||||||
|
import { useApp } from "../state/store";
|
||||||
|
import { api } from "../lib/api";
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock("../state/store", () => ({
|
||||||
|
useApp: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../lib/api", () => ({
|
||||||
|
api: {
|
||||||
|
devLoginConfig: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Login Scene", () => {
|
||||||
|
const mockSetScene = vi.fn();
|
||||||
|
const mockLoginAs = vi.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// Default store mock
|
||||||
|
(useApp as any).mockImplementation((selector: any) => {
|
||||||
|
const state = {
|
||||||
|
setScene: mockSetScene,
|
||||||
|
loginAs: mockLoginAs,
|
||||||
|
};
|
||||||
|
return selector(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default api mock
|
||||||
|
(api.devLoginConfig as any).mockResolvedValue({ enabled: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders standard login form elements", async () => {
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
expect(screen.getByText("FLOWMASTER AUTHENTICATION")).toBeDefined();
|
||||||
|
expect(screen.getByLabelText(/OPERATOR_ID/i)).toBeDefined();
|
||||||
|
expect(screen.getByLabelText(/PASSPHRASE/i)).toBeDefined();
|
||||||
|
expect(screen.getAllByText("SIGN IN")[0]).toBeDefined();
|
||||||
|
expect(screen.getAllByText("CONTINUE WITH MICROSOFT")[0]).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls loginAs and setScene on valid form submission", async () => {
|
||||||
|
mockLoginAs.mockResolvedValueOnce(undefined);
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/OPERATOR_ID/i), { target: { value: 'test@example.com' } });
|
||||||
|
fireEvent.change(screen.getByLabelText(/PASSPHRASE/i), { target: { value: 'password123' } });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getAllByRole("button", { name: /^SIGN IN$/ })[0]);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLoginAs).toHaveBeenCalledWith('test@example.com', 'password123');
|
||||||
|
expect(mockSetScene).toHaveBeenCalledWith('landing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays error on login failure", async () => {
|
||||||
|
mockLoginAs.mockRejectedValueOnce(new Error("Invalid credentials"));
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/OPERATOR_ID/i), { target: { value: 'test@example.com' } });
|
||||||
|
fireEvent.click(screen.getAllByRole("button", { name: /^SIGN IN$/ })[0]);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Invalid credentials")).toBeDefined();
|
||||||
|
expect(mockSetScene).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows dev-login button when feature flag is enabled", async () => {
|
||||||
|
(api.devLoginConfig as any).mockResolvedValueOnce({ enabled: true });
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/DEV-LOGIN/i)).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects to SSO endpoint on microsoft click", async () => {
|
||||||
|
// We can't actually assert on window.location.href in this test environment
|
||||||
|
// without mocking window.location, but we can verify the button renders
|
||||||
|
// and click it to ensure no errors
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
const ssoBtn = screen.getAllByRole("button", { name: /CONTINUE WITH MICROSOFT/i })[0];
|
||||||
|
fireEvent.click(ssoBtn);
|
||||||
|
|
||||||
|
// We just verify it doesn't try to call the regular login
|
||||||
|
expect(mockLoginAs).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
141
src/scenes/Login.tsx
Normal file
141
src/scenes/Login.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useApp } from "../state/store";
|
||||||
|
import { api } from "../lib/api";
|
||||||
|
import { Bot } from "../components/icons";
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const setScene = useApp((s) => s.setScene);
|
||||||
|
const loginAs = useApp((s) => s.loginAs);
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
|
const [devLoginEnabled, setDevLoginEnabled] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if dev login is enabled
|
||||||
|
api.devLoginConfig().then((cfg) => setDevLoginEnabled(cfg.enabled));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!email) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await loginAs(email, password || undefined);
|
||||||
|
setScene("landing");
|
||||||
|
} catch (err) {
|
||||||
|
setError((err as Error).message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDevLogin = async () => {
|
||||||
|
if (!email) {
|
||||||
|
setError("Email required for dev-login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await loginAs(email, undefined);
|
||||||
|
setScene("landing");
|
||||||
|
} catch (err) {
|
||||||
|
setError((err as Error).message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSSO = () => {
|
||||||
|
window.location.href = "/api/v1/auth/microsoft/login";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-page">
|
||||||
|
<div className="login-card">
|
||||||
|
<div className="login-head">
|
||||||
|
<span className="brand-mark"></span>
|
||||||
|
<h1>FLOWMASTER AUTHENTICATION</h1>
|
||||||
|
<div className="login-sub">MISSION CONTROL VERIFICATION REQUIRED</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="login-error">{error}</div>}
|
||||||
|
|
||||||
|
<form className="login-form" onSubmit={handleSubmit}>
|
||||||
|
<div className="login-field">
|
||||||
|
<label htmlFor="email">OPERATOR_ID (EMAIL)</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
className="login-input"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
autoComplete="email"
|
||||||
|
placeholder="e.g. j.doe@flow-master.ai"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="login-field">
|
||||||
|
<label htmlFor="password">PASSPHRASE</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
className="login-input"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="login-row">
|
||||||
|
<label className="login-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={rememberMe}
|
||||||
|
onChange={(e) => setRememberMe(e.target.checked)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<span>Remember me</span>
|
||||||
|
</label>
|
||||||
|
<a href="#" className="login-link">Recover access</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="login-actions">
|
||||||
|
<button type="submit" className="btn btn-primary btn-lg" disabled={loading || !email}>
|
||||||
|
{loading ? "VERIFYING..." : "SIGN IN"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="login-divider">
|
||||||
|
<span>OR</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="login-sso-actions">
|
||||||
|
<button type="button" className="sso-btn" onClick={handleSSO} disabled={loading}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 21 21">
|
||||||
|
<rect x="1" y="1" width="9" height="9" fill="#f25022"/>
|
||||||
|
<rect x="11" y="1" width="9" height="9" fill="#7fba00"/>
|
||||||
|
<rect x="1" y="11" width="9" height="9" fill="#00a4ef"/>
|
||||||
|
<rect x="11" y="11" width="9" height="9" fill="#ffb900"/>
|
||||||
|
</svg>
|
||||||
|
CONTINUE WITH MICROSOFT
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{devLoginEnabled && (
|
||||||
|
<button type="button" className="dev-login-btn" onClick={handleDevLogin} disabled={loading || !email}>
|
||||||
|
<Bot size={14} /> SIGN IN AS DEVELOPER (DEV-LOGIN)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// RunHistory scene: cross-scenario timeline.
|
// RunHistory scene: cross-scenario timeline.
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useApp } from "../state/store";
|
import { useApp } from "../state/store";
|
||||||
import { Clock, History } from "../components/icons";
|
import { Clock, HistoryIcon } from "../components/icons";
|
||||||
|
|
||||||
const STATUS_FILTERS = ["all", "running", "completed", "errored", "queued"] as const;
|
const STATUS_FILTERS = ["all", "running", "completed", "errored", "queued"] as const;
|
||||||
type Status = (typeof STATUS_FILTERS)[number];
|
type Status = (typeof STATUS_FILTERS)[number];
|
||||||
@ -36,7 +36,7 @@ export default function RunHistory() {
|
|||||||
<div className="rh">
|
<div className="rh">
|
||||||
<header className="rh-head">
|
<header className="rh-head">
|
||||||
<div>
|
<div>
|
||||||
<span className="mc-hero-eyebrow"><History size={12} /> Run history</span>
|
<span className="mc-hero-eyebrow"><HistoryIcon size={12} /> Run history</span>
|
||||||
<h2 className="mc-hero-title">All scenarios · all runs</h2>
|
<h2 className="mc-hero-title">All scenarios · all runs</h2>
|
||||||
</div>
|
</div>
|
||||||
<nav className="rh-filters">
|
<nav className="rh-filters">
|
||||||
|
|||||||
51
src/scenes/SsoCallback.tsx
Normal file
51
src/scenes/SsoCallback.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useApp } from "../state/store";
|
||||||
|
import { api } from "../lib/api";
|
||||||
|
|
||||||
|
export default function SsoCallback() {
|
||||||
|
const setScene = useApp((s) => s.setScene);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Process fragment identifier
|
||||||
|
const hash = window.location.hash.substring(1);
|
||||||
|
const params = new URLSearchParams(hash);
|
||||||
|
const accessToken = params.get("access_token");
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
// Clean URL hash so tokens aren't left in the address bar
|
||||||
|
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
||||||
|
|
||||||
|
// Store token
|
||||||
|
api.setBearer(accessToken);
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
useApp.setState({ isAuthed: true });
|
||||||
|
|
||||||
|
// Fetch user profile and sync state
|
||||||
|
api.me().then((me) => {
|
||||||
|
useApp.setState({
|
||||||
|
actor: { mode: "direct_user", user_id: me.user_id },
|
||||||
|
userDisplayName: me.display_name ?? null,
|
||||||
|
userEmail: me.email
|
||||||
|
});
|
||||||
|
useApp.getState().pushToast("ok", `Signed in as ${me.email}`);
|
||||||
|
setScene("landing");
|
||||||
|
}).catch(err => {
|
||||||
|
useApp.getState().pushToast("err", `Failed to get profile: ${err.message}`);
|
||||||
|
setScene("login");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
useApp.getState().pushToast("err", "No access token found in URL");
|
||||||
|
setScene("login");
|
||||||
|
}
|
||||||
|
}, [setScene]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-page">
|
||||||
|
<div className="login-card" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '40px' }}>
|
||||||
|
<span className="spin" style={{ width: '24px', height: '24px', marginBottom: '16px' }}></span>
|
||||||
|
<div className="mono" style={{ fontSize: '12px', letterSpacing: '0.1em' }}>PROCESSING AUTHENTICATION...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,237 +0,0 @@
|
|||||||
// Studio scene — design a new FlowMaster process and publish it to EA2.
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useApp } from "../state/store";
|
|
||||||
import { api } from "../lib/api";
|
|
||||||
import { Branch, Play, Check, Close, Bot, User, Cog } from "../components/icons";
|
|
||||||
|
|
||||||
type NodeKind = "start" | "human_task" | "agent_task" | "service_task" | "end";
|
|
||||||
|
|
||||||
interface DraftNode {
|
|
||||||
id: string;
|
|
||||||
type: NodeKind;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DraftEdge {
|
|
||||||
id: string;
|
|
||||||
source: string;
|
|
||||||
target: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_NODES: DraftNode[] = [
|
|
||||||
{ id: "start", type: "start", label: "Request received" },
|
|
||||||
{ id: "review", type: "human_task", label: "Manager review" },
|
|
||||||
{ id: "approved", type: "end", label: "Approved" },
|
|
||||||
];
|
|
||||||
const DEFAULT_EDGES: DraftEdge[] = [
|
|
||||||
{ id: "e1", source: "start", target: "review" },
|
|
||||||
{ id: "e2", source: "review", target: "approved" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const KIND_ICON: Record<NodeKind, (p: { size?: number }) => React.ReactElement> = {
|
|
||||||
start: Branch,
|
|
||||||
end: Branch,
|
|
||||||
human_task: User,
|
|
||||||
agent_task: Bot,
|
|
||||||
service_task: Cog,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ORG_ID = "a0000000-0000-0000-0000-000000000010";
|
|
||||||
|
|
||||||
export default function Studio() {
|
|
||||||
const mode = useApp((s) => s.mode);
|
|
||||||
const pushToast = useApp((s) => s.pushToast);
|
|
||||||
const refreshLive = useApp((s) => s.refreshLive);
|
|
||||||
const actor = useApp((s) => s.actor);
|
|
||||||
const setScene = useApp((s) => s.setScene);
|
|
||||||
|
|
||||||
const [name, setName] = useState("my-process-" + Math.random().toString(36).slice(2, 8));
|
|
||||||
const [displayName, setDisplayName] = useState("My Process");
|
|
||||||
const [hub, setHub] = useState("procurement");
|
|
||||||
const [description, setDescription] = useState("Demo process designed in the Studio.");
|
|
||||||
const [nodes, setNodes] = useState<DraftNode[]>(DEFAULT_NODES);
|
|
||||||
const [edges, setEdges] = useState<DraftEdge[]>(DEFAULT_EDGES);
|
|
||||||
const [publishing, setPublishing] = useState(false);
|
|
||||||
const [lastResult, setLastResult] = useState<{ ok: boolean; key?: string; err?: string } | null>(null);
|
|
||||||
|
|
||||||
const canPublish = mode === "live" && !!actor?.user_id;
|
|
||||||
|
|
||||||
const addNode = () => {
|
|
||||||
const idx = nodes.length;
|
|
||||||
setNodes([...nodes, { id: `n${idx}`, type: "human_task", label: `New step ${idx}` }]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeNode = (id: string) => {
|
|
||||||
setNodes(nodes.filter((n) => n.id !== id));
|
|
||||||
setEdges(edges.filter((e) => e.source !== id && e.target !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateNode = (id: string, patch: Partial<DraftNode>) => {
|
|
||||||
setNodes(nodes.map((n) => (n.id === id ? { ...n, ...patch } : n)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const addEdge = () => {
|
|
||||||
if (nodes.length < 2) return;
|
|
||||||
const idx = edges.length;
|
|
||||||
setEdges([...edges, { id: `e${idx + 1}`, source: nodes[0].id, target: nodes[nodes.length - 1].id }]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeEdge = (id: string) => setEdges(edges.filter((e) => e.id !== id));
|
|
||||||
|
|
||||||
const updateEdge = (id: string, patch: Partial<DraftEdge>) => {
|
|
||||||
setEdges(edges.map((e) => (e.id === id ? { ...e, ...patch } : e)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const publish = async () => {
|
|
||||||
if (!canPublish) {
|
|
||||||
pushToast("warn", "Switch to LIVE mode + sign in to publish a process.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPublishing(true);
|
|
||||||
setLastResult(null);
|
|
||||||
try {
|
|
||||||
const r = await api.createProcess({
|
|
||||||
name,
|
|
||||||
display_name: displayName,
|
|
||||||
label: displayName,
|
|
||||||
description,
|
|
||||||
hub,
|
|
||||||
config: {
|
|
||||||
org_id: ORG_ID,
|
|
||||||
executable: true,
|
|
||||||
nodes: nodes.map((n) => ({ id: n.id, type: n.type, label: n.label })),
|
|
||||||
edges: edges.map((e) => ({ id: e.id, source: e.source, target: e.target })),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
pushToast("ok", `Published ${r.name} → ${r._key.slice(0, 8)}`);
|
|
||||||
setLastResult({ ok: true, key: r._key });
|
|
||||||
await refreshLive();
|
|
||||||
} catch (e) {
|
|
||||||
pushToast("err", `Publish failed: ${(e as Error).message.slice(0, 140)}`);
|
|
||||||
setLastResult({ ok: false, err: (e as Error).message });
|
|
||||||
} finally {
|
|
||||||
setPublishing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const valid = nodes.length >= 2 && edges.length >= 1 && nodes.every((n) => n.label.trim().length > 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="studio">
|
|
||||||
<header className="studio-head">
|
|
||||||
<div>
|
|
||||||
<div className="mc-hero-eyebrow"><Branch size={12} /> Process Studio</div>
|
|
||||||
<h2 className="mc-hero-title">Design a new process</h2>
|
|
||||||
<div className="mc-hero-sub">Hand-craft a typed FlowMaster process and publish it straight to EA2 on demo.flow-master.ai.</div>
|
|
||||||
</div>
|
|
||||||
<button className="btn btn-primary btn-lg" onClick={publish} disabled={!valid || publishing}>
|
|
||||||
{publishing ? <span className="spin" /> : <Play size={13} />} Publish to EA2
|
|
||||||
{!canPublish && <span className="preview-marker">live + sign-in required</span>}
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="studio-grid">
|
|
||||||
<section className="studio-panel">
|
|
||||||
<h3 className="panel-h">Definition</h3>
|
|
||||||
<label className="studio-field">
|
|
||||||
<span>name (unique slug)</span>
|
|
||||||
<input className="studio-input mono" value={name} onChange={(e) => setName(e.target.value)} />
|
|
||||||
</label>
|
|
||||||
<label className="studio-field">
|
|
||||||
<span>display name</span>
|
|
||||||
<input className="studio-input" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
|
|
||||||
</label>
|
|
||||||
<label className="studio-field">
|
|
||||||
<span>hub</span>
|
|
||||||
<select className="studio-input" value={hub} onChange={(e) => setHub(e.target.value)}>
|
|
||||||
<option value="procurement">procurement</option>
|
|
||||||
<option value="finance">finance</option>
|
|
||||||
<option value="people">people</option>
|
|
||||||
<option value="service">service</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className="studio-field">
|
|
||||||
<span>description</span>
|
|
||||||
<textarea className="studio-input" value={description} onChange={(e) => setDescription(e.target.value)} rows={3} />
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="studio-panel">
|
|
||||||
<h3 className="panel-h">
|
|
||||||
Nodes
|
|
||||||
<span className="panel-count">{nodes.length}</span>
|
|
||||||
<button className="link-btn" style={{ marginLeft: "auto" }} onClick={addNode}>+ node</button>
|
|
||||||
</h3>
|
|
||||||
<div className="studio-rows">
|
|
||||||
{nodes.map((n) => {
|
|
||||||
const Icon = KIND_ICON[n.type] ?? Cog;
|
|
||||||
return (
|
|
||||||
<div key={n.id} className="studio-row">
|
|
||||||
<span className="studio-row-id mono">{n.id}</span>
|
|
||||||
<Icon size={13} />
|
|
||||||
<select className="studio-input" value={n.type} onChange={(e) => updateNode(n.id, { type: e.target.value as NodeKind })}>
|
|
||||||
<option value="start">start</option>
|
|
||||||
<option value="human_task">human_task</option>
|
|
||||||
<option value="agent_task">agent_task</option>
|
|
||||||
<option value="service_task">service_task</option>
|
|
||||||
<option value="end">end</option>
|
|
||||||
</select>
|
|
||||||
<input className="studio-input" value={n.label} onChange={(e) => updateNode(n.id, { label: e.target.value })} />
|
|
||||||
<button className="link-btn" onClick={() => removeNode(n.id)} title="Remove"><Close size={11} /></button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="studio-panel">
|
|
||||||
<h3 className="panel-h">
|
|
||||||
Edges
|
|
||||||
<span className="panel-count">{edges.length}</span>
|
|
||||||
<button className="link-btn" style={{ marginLeft: "auto" }} onClick={addEdge}>+ edge</button>
|
|
||||||
</h3>
|
|
||||||
<div className="studio-rows">
|
|
||||||
{edges.map((e) => (
|
|
||||||
<div key={e.id} className="studio-row">
|
|
||||||
<span className="studio-row-id mono">{e.id}</span>
|
|
||||||
<select className="studio-input" value={e.source} onChange={(ev) => updateEdge(e.id, { source: ev.target.value })}>
|
|
||||||
{nodes.map((n) => <option key={n.id} value={n.id}>{n.id}</option>)}
|
|
||||||
</select>
|
|
||||||
<span className="mono" style={{ color: "var(--text-3)" }}>→</span>
|
|
||||||
<select className="studio-input" value={e.target} onChange={(ev) => updateEdge(e.id, { target: ev.target.value })}>
|
|
||||||
{nodes.map((n) => <option key={n.id} value={n.id}>{n.id}</option>)}
|
|
||||||
</select>
|
|
||||||
<button className="link-btn" onClick={() => removeEdge(e.id)} title="Remove"><Close size={11} /></button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="studio-panel">
|
|
||||||
<h3 className="panel-h">JSON preview</h3>
|
|
||||||
<pre className="raw-json"><code>{JSON.stringify({
|
|
||||||
name, display_name: displayName, hub, description,
|
|
||||||
kind: "definition", status: "published", version: 1,
|
|
||||||
config: {
|
|
||||||
org_id: ORG_ID, executable: true,
|
|
||||||
nodes: nodes.map((n) => ({ id: n.id, type: n.type, label: n.label })),
|
|
||||||
edges: edges.map((e) => ({ id: e.id, source: e.source, target: e.target })),
|
|
||||||
},
|
|
||||||
}, null, 2)}</code></pre>
|
|
||||||
{lastResult && (
|
|
||||||
<div className={`studio-result ${lastResult.ok ? "ok" : "err"}`}>
|
|
||||||
{lastResult.ok ? (
|
|
||||||
<>
|
|
||||||
<Check size={12} /> Published as <span className="mono">{lastResult.key?.slice(0, 12)}</span>
|
|
||||||
<button className="link-btn" onClick={() => setScene("mission")} style={{ marginLeft: "auto" }}>Open in Mission Control</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<><Close size={12} /> <span className="mono">{lastResult.err?.slice(0, 200)}</span></>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
431
src/scenes/Wizard.tsx
Normal file
431
src/scenes/Wizard.tsx
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useApp } from "../state/store";
|
||||||
|
import { wizardApi, type BatchOperation } from "../lib/wizardApi";
|
||||||
|
import { Branch, Check, Close, ChevronRight } from "../components/icons";
|
||||||
|
|
||||||
|
type WizardStep = "Intake" | "Analyze" | "Generate" | "Validate" | "Draft saved" | "Publish";
|
||||||
|
|
||||||
|
const STEPS: WizardStep[] = ["Intake", "Analyze", "Generate", "Validate", "Draft saved", "Publish"];
|
||||||
|
|
||||||
|
interface DraftState {
|
||||||
|
step: WizardStep;
|
||||||
|
flowKey?: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
nodes: any[];
|
||||||
|
edges: any[];
|
||||||
|
fields: { name: string; type: string }[];
|
||||||
|
rules: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Wizard() {
|
||||||
|
const mode = useApp((s) => s.mode);
|
||||||
|
const pushToast = useApp((s) => s.pushToast);
|
||||||
|
const refreshLive = useApp((s) => s.refreshLive);
|
||||||
|
const actor = useApp((s) => s.actor);
|
||||||
|
const setScene = useApp((s) => s.setScene);
|
||||||
|
|
||||||
|
const canPublish = mode === "live" && !!actor?.user_id;
|
||||||
|
|
||||||
|
const [draft, setDraft] = useState<DraftState>(() => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem("fm.mc.wizard.draft");
|
||||||
|
if (stored) return JSON.parse(stored);
|
||||||
|
} catch {}
|
||||||
|
return {
|
||||||
|
step: "Intake",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
fields: [],
|
||||||
|
rules: []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const [working, setWorking] = useState(false);
|
||||||
|
const [publishedKey, setPublishedKey] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("fm.mc.wizard.draft", JSON.stringify(draft));
|
||||||
|
}, [draft]);
|
||||||
|
|
||||||
|
const updateDraft = (patch: Partial<DraftState>) => {
|
||||||
|
setDraft(d => ({ ...d, ...patch }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIntakeSubmit = async () => {
|
||||||
|
if (!draft.description) return;
|
||||||
|
setWorking(true);
|
||||||
|
try {
|
||||||
|
const slug = `process_${Date.now()}`;
|
||||||
|
const res = await wizardApi.createDraft({
|
||||||
|
name: slug,
|
||||||
|
display_name: draft.name || "Untitled Process",
|
||||||
|
description: draft.description,
|
||||||
|
source_context: "EA2_DRAFT_PROCESS:process_creation",
|
||||||
|
config: {
|
||||||
|
wizard: {
|
||||||
|
marker: "EA2_DRAFT_PROCESS",
|
||||||
|
maxDepth: 5,
|
||||||
|
maxNodes: 40,
|
||||||
|
chat: [],
|
||||||
|
uploads: [],
|
||||||
|
panelState: {},
|
||||||
|
debug: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-generate basic 5-step template
|
||||||
|
const templateNodes = [
|
||||||
|
{ _key: "n1", kind: "step", display_name: "Capture request details", dispatch_kind: "human" },
|
||||||
|
{ _key: "n2", kind: "step", display_name: "Validate requirements", dispatch_kind: "agent", agent_capability: "evaluate_business_rule" },
|
||||||
|
{ _key: "n3", kind: "step", display_name: "Prepare work package", dispatch_kind: "agent", agent_capability: "process_design_assistant" },
|
||||||
|
{ _key: "n4", kind: "step", display_name: "Review and approve", dispatch_kind: "human" },
|
||||||
|
{ _key: "n5", kind: "step", display_name: "Close out process", dispatch_kind: "human" }
|
||||||
|
];
|
||||||
|
|
||||||
|
updateDraft({
|
||||||
|
flowKey: res._key,
|
||||||
|
step: "Analyze",
|
||||||
|
nodes: templateNodes
|
||||||
|
});
|
||||||
|
pushToast("ok", "Draft created in EA2");
|
||||||
|
} catch (err: any) {
|
||||||
|
pushToast("err", `Failed to create draft: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setWorking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStructureSave = async () => {
|
||||||
|
if (!draft.flowKey || !actor) return;
|
||||||
|
setWorking(true);
|
||||||
|
try {
|
||||||
|
const ops: BatchOperation[] = draft.nodes.map(n => ({
|
||||||
|
collection: "ea2_node",
|
||||||
|
op: "insert",
|
||||||
|
flow_key: draft.flowKey,
|
||||||
|
...n
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Link them sequentially
|
||||||
|
for (let i = 0; i < draft.nodes.length - 1; i++) {
|
||||||
|
ops.push({
|
||||||
|
collection: "ea2_edge",
|
||||||
|
op: "insert",
|
||||||
|
flow_key: draft.flowKey,
|
||||||
|
_from: `ea2_node/${draft.nodes[i]._key}`,
|
||||||
|
_to: `ea2_node/${draft.nodes[i+1]._key}`,
|
||||||
|
kind: "next"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await wizardApi.applyBatch(draft.flowKey, ops, actor);
|
||||||
|
updateDraft({ step: "Generate" });
|
||||||
|
pushToast("ok", "Structure saved");
|
||||||
|
} catch (err: any) {
|
||||||
|
pushToast("err", `Save failed: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setWorking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDataSave = async () => {
|
||||||
|
if (!draft.flowKey || !actor) return;
|
||||||
|
setWorking(true);
|
||||||
|
try {
|
||||||
|
// In a real app we'd save proper data models here.
|
||||||
|
// For the spike we just move forward and do a dummy update
|
||||||
|
await wizardApi.updateDraftConfig(draft.flowKey, {
|
||||||
|
config: { wizard: { panelState: { fields: draft.fields } } }
|
||||||
|
});
|
||||||
|
updateDraft({ step: "Validate" });
|
||||||
|
pushToast("ok", "Data requirements saved");
|
||||||
|
} catch (err: any) {
|
||||||
|
pushToast("err", `Save failed: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setWorking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRulesSave = async () => {
|
||||||
|
if (!draft.flowKey || !actor) return;
|
||||||
|
setWorking(true);
|
||||||
|
try {
|
||||||
|
await wizardApi.updateDraftConfig(draft.flowKey, {
|
||||||
|
config: { wizard: { panelState: { rules: draft.rules } } }
|
||||||
|
});
|
||||||
|
updateDraft({ step: "Draft saved" });
|
||||||
|
pushToast("ok", "Rules saved");
|
||||||
|
} catch (err: any) {
|
||||||
|
pushToast("err", `Save failed: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setWorking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePublish = async () => {
|
||||||
|
if (!draft.flowKey || !actor) return;
|
||||||
|
setWorking(true);
|
||||||
|
try {
|
||||||
|
const ops: BatchOperation[] = [
|
||||||
|
{
|
||||||
|
collection: "ea2_flow",
|
||||||
|
op: "update",
|
||||||
|
_key: draft.flowKey,
|
||||||
|
status: "published"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
await wizardApi.applyBatch(draft.flowKey, ops, actor);
|
||||||
|
updateDraft({ step: "Publish" });
|
||||||
|
setPublishedKey(draft.flowKey);
|
||||||
|
pushToast("ok", "Process published successfully!");
|
||||||
|
refreshLive();
|
||||||
|
} catch (err: any) {
|
||||||
|
pushToast("err", `Publish failed: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setWorking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartInstance = async () => {
|
||||||
|
if(!publishedKey) return;
|
||||||
|
setWorking(true);
|
||||||
|
try {
|
||||||
|
await wizardApi.startInstance(publishedKey);
|
||||||
|
pushToast("ok", "New instance started!");
|
||||||
|
setScene("mission");
|
||||||
|
} catch(err:any) {
|
||||||
|
pushToast("err", `Failed to start: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setWorking(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderStepNav = () => (
|
||||||
|
<div className="wizard-progress">
|
||||||
|
{STEPS.map((step, i) => {
|
||||||
|
const isActive = step === draft.step;
|
||||||
|
const isPast = STEPS.indexOf(step) < STEPS.indexOf(draft.step);
|
||||||
|
return (
|
||||||
|
<div key={step} className={`wizard-step-marker ${isActive ? 'active' : ''} ${isPast ? 'past' : ''}`}>
|
||||||
|
<span className="step-num">{i + 1}</span>
|
||||||
|
<span className="step-label">{step}</span>
|
||||||
|
{i < STEPS.length - 1 && <ChevronRight size={12} className="step-chevron" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wizard-container">
|
||||||
|
<header className="wizard-header">
|
||||||
|
<div>
|
||||||
|
<div className="mc-hero-eyebrow"><Branch size={12} /> Process Creation Wizard</div>
|
||||||
|
<h2 className="mc-hero-title">Build a new process</h2>
|
||||||
|
</div>
|
||||||
|
{renderStepNav()}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="wizard-content">
|
||||||
|
{draft.step === "Intake" && (
|
||||||
|
<div className="wizard-panel">
|
||||||
|
<h3 className="panel-h">What process do you want to build?</h3>
|
||||||
|
<div className="field-group">
|
||||||
|
<label>Name (optional)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={draft.name}
|
||||||
|
onChange={e => updateDraft({ name: e.target.value })}
|
||||||
|
placeholder="e.g. Laptop Procurement"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field-group">
|
||||||
|
<label>Describe the process</label>
|
||||||
|
<textarea
|
||||||
|
value={draft.description}
|
||||||
|
onChange={e => updateDraft({ description: e.target.value })}
|
||||||
|
placeholder="When a store manager requests a new laptop, run a procurement approval..."
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleIntakeSubmit}
|
||||||
|
disabled={!draft.description || working || !canPublish}
|
||||||
|
title="POST /api/ea2/flow"
|
||||||
|
>
|
||||||
|
{working ? 'Saving...' : 'Start Drafting →'}
|
||||||
|
</button>
|
||||||
|
{!canPublish && <p className="warning-text">Switch to LIVE mode + sign in to create processes.</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{draft.step === "Analyze" && (
|
||||||
|
<div className="wizard-panel">
|
||||||
|
<h3 className="panel-h">Review proposed structure</h3>
|
||||||
|
<div className="node-list">
|
||||||
|
{draft.nodes.map((n, i) => (
|
||||||
|
<div key={n._key} className="node-card">
|
||||||
|
<div className="node-header">
|
||||||
|
<span className="node-idx">{i + 1}</span>
|
||||||
|
<input
|
||||||
|
value={n.display_name}
|
||||||
|
onChange={e => {
|
||||||
|
const newNodes = [...draft.nodes];
|
||||||
|
newNodes[i].display_name = e.target.value;
|
||||||
|
updateDraft({ nodes: newNodes });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="node-meta">
|
||||||
|
<select
|
||||||
|
value={n.dispatch_kind}
|
||||||
|
onChange={e => {
|
||||||
|
const newNodes = [...draft.nodes];
|
||||||
|
newNodes[i].dispatch_kind = e.target.value;
|
||||||
|
updateDraft({ nodes: newNodes });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="human">Human Task</option>
|
||||||
|
<option value="agent">Agent Task</option>
|
||||||
|
<option value="system">System Task</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleStructureSave}
|
||||||
|
disabled={working}
|
||||||
|
title="POST /api/ea2/apply-batch"
|
||||||
|
>
|
||||||
|
{working ? 'Saving...' : 'Confirm Structure →'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{draft.step === "Generate" && (
|
||||||
|
<div className="wizard-panel">
|
||||||
|
<h3 className="panel-h">What information do you need to collect?</h3>
|
||||||
|
<div className="field-list">
|
||||||
|
{draft.fields.map((f, i) => (
|
||||||
|
<div key={i} className="field-row">
|
||||||
|
<input
|
||||||
|
placeholder="Field name"
|
||||||
|
value={f.name}
|
||||||
|
onChange={e => {
|
||||||
|
const newFields = [...draft.fields];
|
||||||
|
newFields[i].name = e.target.value;
|
||||||
|
updateDraft({ fields: newFields });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={f.type}
|
||||||
|
onChange={e => {
|
||||||
|
const newFields = [...draft.fields];
|
||||||
|
newFields[i].type = e.target.value;
|
||||||
|
updateDraft({ fields: newFields });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="string">Text</option>
|
||||||
|
<option value="number">Number</option>
|
||||||
|
<option value="boolean">Yes/No</option>
|
||||||
|
</select>
|
||||||
|
<button className="icon-btn" onClick={() => updateDraft({ fields: draft.fields.filter((_, idx) => idx !== i) })}><Close size={12} /></button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button className="btn btn-secondary" onClick={() => updateDraft({ fields: [...draft.fields, { name: "", type: "string" }] })}>+ Add Field</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleDataSave}
|
||||||
|
disabled={working}
|
||||||
|
title="PUT /api/ea2/flow/{key}"
|
||||||
|
>
|
||||||
|
{working ? 'Saving...' : 'Confirm Data →'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{draft.step === "Validate" && (
|
||||||
|
<div className="wizard-panel">
|
||||||
|
<h3 className="panel-h">When should this process do something different? (Rules)</h3>
|
||||||
|
<div className="field-list">
|
||||||
|
{draft.rules.map((r, i) => (
|
||||||
|
<div key={i} className="field-row">
|
||||||
|
<input
|
||||||
|
placeholder="e.g. If cost > 5000, require VP approval"
|
||||||
|
value={r}
|
||||||
|
onChange={e => {
|
||||||
|
const newRules = [...draft.rules];
|
||||||
|
newRules[i] = e.target.value;
|
||||||
|
updateDraft({ rules: newRules });
|
||||||
|
}}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<button className="icon-btn" onClick={() => updateDraft({ rules: draft.rules.filter((_, idx) => idx !== i) })}><Close size={12} /></button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button className="btn btn-secondary" onClick={() => updateDraft({ rules: [...draft.rules, ""] })}>+ Add Rule</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleRulesSave}
|
||||||
|
disabled={working}
|
||||||
|
title="PUT /api/ea2/flow/{key}"
|
||||||
|
>
|
||||||
|
{working ? 'Saving...' : 'Review Process →'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{draft.step === "Draft saved" && (
|
||||||
|
<div className="wizard-panel">
|
||||||
|
<h3 className="panel-h">Review & Publish</h3>
|
||||||
|
<p>Your process <strong>{draft.name || 'Untitled'}</strong> is ready to be published to EA2.</p>
|
||||||
|
<div className="summary-stats">
|
||||||
|
<div><span>Steps:</span> {draft.nodes.length}</div>
|
||||||
|
<div><span>Data fields:</span> {draft.fields.length}</div>
|
||||||
|
<div><span>Rules:</span> {draft.rules.length}</div>
|
||||||
|
</div>
|
||||||
|
<div className="actions-row">
|
||||||
|
<button className="btn btn-secondary" onClick={() => updateDraft({ step: "Intake" })}>Edit</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handlePublish}
|
||||||
|
disabled={working}
|
||||||
|
title="POST /api/ea2/apply-batch"
|
||||||
|
>
|
||||||
|
{working ? 'Publishing...' : 'Publish to EA2'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{draft.step === "Publish" && (
|
||||||
|
<div className="wizard-panel success-panel">
|
||||||
|
<div className="success-icon"><Check size={32} /></div>
|
||||||
|
<h3 className="panel-h">Process Published!</h3>
|
||||||
|
<p>Process definition <code>{publishedKey}</code> is now active in EA2.</p>
|
||||||
|
<div className="actions-row">
|
||||||
|
<button className="btn btn-secondary" onClick={() => { setDraft({ step: "Intake", name: "", description: "", nodes: [], edges: [], fields: [], rules: [] }); setPublishedKey(null); }}>Create Another</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleStartInstance}
|
||||||
|
disabled={working}
|
||||||
|
title="POST /api/runtime/transactions"
|
||||||
|
>
|
||||||
|
Start First Instance Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ import { buildLiveScenariosFromApi } from "../lib/buildScenarios";
|
|||||||
import { api, type ApiCall, type Actor } from "../lib/api";
|
import { api, type ApiCall, type Actor } from "../lib/api";
|
||||||
import type { ProcessScenario } from "../data/types";
|
import type { ProcessScenario } from "../data/types";
|
||||||
|
|
||||||
export type SceneId = "landing" | "mission" | "history" | "studio" | "settings";
|
export type SceneId = "landing" | "mission" | "history" | "studio" | "settings" | "login" | "sso-callback";
|
||||||
export type DataMode = "snapshot" | "live";
|
export type DataMode = "snapshot" | "live";
|
||||||
export type Theme = "dark" | "light";
|
export type Theme = "dark" | "light";
|
||||||
|
|
||||||
@ -82,15 +82,18 @@ interface AppState {
|
|||||||
actor: Actor | null;
|
actor: Actor | null;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
userDisplayName: string | null;
|
userDisplayName: string | null;
|
||||||
|
isAuthed: boolean;
|
||||||
setUserEmail: (e: string) => void;
|
setUserEmail: (e: string) => void;
|
||||||
loginAs: (email: string) => Promise<void>;
|
loginAs: (email: string, password?: string) => Promise<void>;
|
||||||
|
|
||||||
/** Live polling */
|
/** Live polling — lightweight tick that only refreshes the active
|
||||||
|
* scenario's work-items + headline runtime. Full refresh is manual. */
|
||||||
pollEverySec: number;
|
pollEverySec: number;
|
||||||
setPollEverySec: (n: number) => void;
|
setPollEverySec: (n: number) => void;
|
||||||
pollTimer: number | null;
|
pollTimer: number | null;
|
||||||
startPolling: () => void;
|
startPolling: () => void;
|
||||||
stopPolling: () => void;
|
stopPolling: () => void;
|
||||||
|
pollLiveTick: () => Promise<void>;
|
||||||
|
|
||||||
/** API call log */
|
/** API call log */
|
||||||
consoleOpen: boolean;
|
consoleOpen: boolean;
|
||||||
@ -240,28 +243,32 @@ export const useApp = create<AppState>((set, get) => {
|
|||||||
actor: null,
|
actor: null,
|
||||||
userEmail: prefs.email ?? "dev@flow-master.ai",
|
userEmail: prefs.email ?? "dev@flow-master.ai",
|
||||||
userDisplayName: null,
|
userDisplayName: null,
|
||||||
|
isAuthed: typeof sessionStorage !== "undefined" && !!sessionStorage.getItem("fm.mc.token.v1"),
|
||||||
setUserEmail: (e) => { set({ userEmail: e }); persist(); },
|
setUserEmail: (e) => { set({ userEmail: e }); persist(); },
|
||||||
loginAs: async (email) => {
|
loginAs: async (email, password) => {
|
||||||
api.clearToken();
|
api.clearToken();
|
||||||
api.config = { ...api.config, email };
|
api.config = { ...api.config, email };
|
||||||
set({ userEmail: email });
|
set({ userEmail: email });
|
||||||
persist();
|
persist();
|
||||||
try {
|
try {
|
||||||
|
await api.signIn(email, password);
|
||||||
const me = await api.me();
|
const me = await api.me();
|
||||||
set({
|
set({
|
||||||
actor: { mode: "direct_user", user_id: me.user_id },
|
actor: { mode: "direct_user", user_id: me.user_id },
|
||||||
userDisplayName: me.display_name ?? null,
|
userDisplayName: me.display_name ?? null,
|
||||||
|
isAuthed: true,
|
||||||
});
|
});
|
||||||
get().pushToast("ok", `Signed in as ${me.email}`);
|
get().pushToast("ok", `Signed in as ${me.email}`);
|
||||||
if (get().mode === "live") await get().refreshLive();
|
if (get().mode === "live") await get().refreshLive();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
get().pushToast("err", `Login failed: ${(e as Error).message.slice(0, 80)}`);
|
get().pushToast("err", `Login failed: ${(e as Error).message.slice(0, 80)}`);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
pollEverySec: prefs.pollEverySec ?? 8,
|
pollEverySec: prefs.pollEverySec ?? 30,
|
||||||
setPollEverySec: (n) => {
|
setPollEverySec: (n) => {
|
||||||
set({ pollEverySec: Math.max(2, Math.min(120, n)) });
|
set({ pollEverySec: Math.max(5, Math.min(300, n)) });
|
||||||
persist();
|
persist();
|
||||||
if (get().pollTimer) { get().stopPolling(); get().startPolling(); }
|
if (get().pollTimer) { get().stopPolling(); get().startPolling(); }
|
||||||
},
|
},
|
||||||
@ -273,7 +280,7 @@ export const useApp = create<AppState>((set, get) => {
|
|||||||
const id = window.setInterval(() => {
|
const id = window.setInterval(() => {
|
||||||
const s = get();
|
const s = get();
|
||||||
if (s.mode !== "live" || s.liveLoading) return;
|
if (s.mode !== "live" || s.liveLoading) return;
|
||||||
void s.refreshLive();
|
void s.pollLiveTick();
|
||||||
}, get().pollEverySec * 1000);
|
}, get().pollEverySec * 1000);
|
||||||
set({ pollTimer: id });
|
set({ pollTimer: id });
|
||||||
},
|
},
|
||||||
@ -283,6 +290,39 @@ export const useApp = create<AppState>((set, get) => {
|
|||||||
if (id) window.clearInterval(id);
|
if (id) window.clearInterval(id);
|
||||||
set({ pollTimer: null });
|
set({ pollTimer: null });
|
||||||
},
|
},
|
||||||
|
pollLiveTick: async () => {
|
||||||
|
const s = get();
|
||||||
|
if (s.mode !== "live" || s.liveLoading) return;
|
||||||
|
try {
|
||||||
|
const workItems = await api.workItems();
|
||||||
|
const sc = s.scenarios.find((x) => x.id === s.scenarioId);
|
||||||
|
let headlineRt = null;
|
||||||
|
if (sc?.headlineTx) {
|
||||||
|
headlineRt = await api.transaction(sc.headlineTx);
|
||||||
|
}
|
||||||
|
const updatedScenarios = s.scenarios.map((scen) => {
|
||||||
|
if (!scen.live) return scen;
|
||||||
|
const myCases = workItems.filter((w) => w.definition_key === scen.defKey);
|
||||||
|
const newQueue = myCases.slice(0, 8).map((c, i) => ({
|
||||||
|
id: `${c.transaction_id || scen.defKey}-${i}`,
|
||||||
|
stepId: scen.queue[i]?.stepId ?? scen.defaultStepId,
|
||||||
|
title: `${c.short_id ?? c.transaction_id?.slice(0, 8) ?? "case"} · ${c.active_step_display_name || c.next_action || "case"}`,
|
||||||
|
waitingOn: (c.status === "running" || c.status === "waiting_for_user") ? "approval" as const
|
||||||
|
: c.status === "waiting_for_agent" ? "agent" as const
|
||||||
|
: "input" as const,
|
||||||
|
ageDays: c.age_days ?? 0,
|
||||||
|
status: c.status,
|
||||||
|
}));
|
||||||
|
if (scen.id === s.scenarioId && headlineRt) {
|
||||||
|
return { ...scen, queue: newQueue, raw: { ...(scen.raw as object), headlineRt } };
|
||||||
|
}
|
||||||
|
return { ...scen, queue: newQueue };
|
||||||
|
});
|
||||||
|
set({ scenarios: updatedScenarios, liveTotals: { workItems: workItems.length, distinctDefs: s.liveTotals?.distinctDefs ?? 0 }, liveFetchedAt: Date.now() });
|
||||||
|
} catch {
|
||||||
|
// silent — full refresh will retry
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
consoleOpen: prefs.consoleOpen ?? false,
|
consoleOpen: prefs.consoleOpen ?? false,
|
||||||
setConsoleOpen: (v) => { set({ consoleOpen: v }); persist(); },
|
setConsoleOpen: (v) => { set({ consoleOpen: v }); persist(); },
|
||||||
|
|||||||
42
update_theme.js
Normal file
42
update_theme.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
let css = fs.readFileSync('src/index.css', 'utf-8');
|
||||||
|
|
||||||
|
const darkThemeBlock = `
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bp-paper: #0c1322;
|
||||||
|
--bp-paper-2: #1a2740;
|
||||||
|
--bp-paper-3: #243453;
|
||||||
|
--bp-navy: #e6edf7;
|
||||||
|
--bp-navy-2: #d5dde9;
|
||||||
|
--bp-muted: #7a8aa8;
|
||||||
|
--bp-muted-2: #4a5b80;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
css = css.replace(':root {', darkThemeBlock + '\n:root {');
|
||||||
|
fs.writeFileSync('src/index.css', css);
|
||||||
|
|
||||||
|
let app = fs.readFileSync('src/App.tsx', 'utf-8');
|
||||||
|
// Check if Sun/Moon imports exist
|
||||||
|
app = app.replace(
|
||||||
|
'import { Cmd, Home, Layers, History as HistoryIcon, Pulse, Refresh, Branch, Cog, User } from "./components/icons";',
|
||||||
|
'import { Cmd, Home, Layers, History as HistoryIcon, Pulse, Refresh, Branch, Cog, User, Sun, Moon } from "./components/icons";'
|
||||||
|
);
|
||||||
|
|
||||||
|
const themeToggle = `
|
||||||
|
<button
|
||||||
|
className="link-btn"
|
||||||
|
onClick={() => useApp.getState().setTheme(useApp.getState().theme === "dark" ? "light" : "dark")}
|
||||||
|
title="Toggle theme"
|
||||||
|
>
|
||||||
|
{useApp.getState().theme === "dark" ? <Sun size={13} /> : <Moon size={13} />}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
app = app.replace(
|
||||||
|
'{apiLogCount > 0 && <span className="badge mono">{apiLogCount}</span>}\n </button>',
|
||||||
|
'{apiLogCount > 0 && <span className="badge mono">{apiLogCount}</span>}\n </button>\n <button className="link-btn" onClick={() => useApp.getState().setTheme(useApp.getState().theme === "dark" ? "light" : "dark")} title="Toggle theme">\n {useApp.getState().theme === "dark" ? <Sun size={13} /> : <Moon size={13} />}\n </button>'
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync('src/App.tsx', app);
|
||||||
11
vitest.config.ts
Normal file
11
vitest.config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
globals: true,
|
||||||
|
setupFiles: ['./setupTests.ts']
|
||||||
|
}
|
||||||
|
})
|
||||||
1
vitest.setup.ts
Normal file
1
vitest.setup.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom/vitest';
|
||||||
Loading…
x
Reference in New Issue
Block a user