diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8c383fe --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto +node/ui/** text eol=lf +node/ui-src/** text eol=lf diff --git a/.github/workflows/platform-tests.yml b/.github/workflows/platform-tests.yml index e2646cb..9db6fbb 100644 --- a/.github/workflows/platform-tests.yml +++ b/.github/workflows/platform-tests.yml @@ -50,6 +50,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Verify committed UI assets + run: npm run verify:ui-assets + - name: Run syntax checks run: npm run lint @@ -102,3 +105,37 @@ jobs: env: CRP_NATIVE_KEYRING_SMOKE: "1" run: node scripts/native-keyring-smoke.mjs + + canonical-ui: + name: Canonical Linux UI build + runs-on: ubuntu-latest + defaults: + run: + working-directory: node + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22.19.0 + package-manager-cache: false + + - name: Install dependencies + run: npm ci + + - name: Generate canonical UI artifact + run: npm run build:ui + + - name: Upload canonical UI artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: crp-canonical-ui-${{ github.run_id }} + path: node/ui/** + include-hidden-files: true + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/release-preflight.yml b/.github/workflows/release-preflight.yml index 3abcc00..4f284d8 100644 --- a/.github/workflows/release-preflight.yml +++ b/.github/workflows/release-preflight.yml @@ -9,6 +9,7 @@ on: - "node/bin/**" - "node/src/**" - "node/ui/**" + - "node/ui-src/**" - "node/scripts/**" - "node/test/package-content.test.mjs" - "node/test/native-keyring-smoke.test.mjs" @@ -17,6 +18,7 @@ on: - "node/package-lock.json" - "node/.changeset/**" - "node/RELEASING.md" + - ".gitattributes" permissions: contents: read @@ -37,12 +39,15 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 22.19.0 package-manager-cache: false - name: Install dependencies run: npm ci + - name: Verify canonical UI checkout + run: npm run verify:ui-build + - name: Run syntax checks run: npm run lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 05e99ad..3db7e8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,32 @@ permissions: id-token: write jobs: + canonical-ui: + name: Verify canonical UI checkout + runs-on: ubuntu-latest + defaults: + run: + working-directory: node + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22.19.0 + package-manager-cache: false + + - name: Install dependencies + run: npm ci + + - name: Verify canonical UI checkout + run: npm run verify:ui-build + release: + needs: canonical-ui runs-on: ubuntu-latest defaults: run: diff --git a/AGENTS.md b/AGENTS.md index 9c02b5f..445f770 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ - CLI and supervisor entrypoint: `node/bin/crp.mjs` - Proxy worker: `node/src/server.mjs` - Provider, credential, and control-plane modules: `node/src/` -- Local Web management UI: `node/ui/` (target architecture) +- Local Web management UI source: `node/ui-src/app/` and `node/ui-src/src/`; generated static export: `node/ui/` - Tests: `node/test/` ## Working Rules @@ -127,6 +127,12 @@ - Unbounded database maintenance must not run synchronously before the Worker readiness acknowledgement. - Replacement background maintenance must wait for confirmed cancellation of prior work on the same resource. - Upgrade tests must cover nonempty persisted databases and maintenance exceeding startup deadlines. +- Next.js migrations must move ordinary business components out of the reserved `pages` directory before building. +- CSP hash sources must retain their required single quotes and pass real-browser hydration verification. +- Tailwind migrations must define the legacy CSS cascade order and verify that new utilities take effect. +- Authenticated preview diagnostics must not print response headers containing `Set-Cookie`. +- Static UI release validation must use a clean checkout with canonical LF line endings. +- Canonical verification must compare the untouched checked-in UI before any build replaces it. ## Required Checks diff --git a/README.md b/README.md index 33c0015..c86cbec 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ npx @cluic/codex-remote-proxy ui `crp ui` starts or discovers the local supervisor and opens the management UI. A saved explicit language choice wins; otherwise the interface uses the first supported Chinese or English browser/system preference and falls back to English. Inferred language is not persisted. -The current development UI is implemented in `node/ui-src/` with React, TypeScript, and Vite. Those tools are build-time only: the package and Admin server still ship exactly `ui/index.html`, `ui/app.js`, and `ui/styles.css`, with no frontend runtime server, remote font, CDN, telemetry, source map, or dynamic chunk. +The management UI is implemented in `node/ui-src/` with Next.js App Router, React, TypeScript, Tailwind CSS, shadcn source components backed by Base UI, and Lucide icons. The default Webpack build produces a reviewed static export and asset manifest under `node/ui/`; the package needs no frontend runtime server and the Admin server serves every route and chunk from the same loopback origin. The UI uses no remote font, CDN, telemetry, or source map. Clean Ubuntu Node 22 is the canonical build environment: release gates compare its generated output with the checkout before any candidate build replaces `node/ui/`. Linux, macOS, and Windows validate the committed source digest, manifest, CSP, resources, package allowlist, and their platform-specific tests. ## What You Can Manage @@ -241,7 +241,7 @@ Tests use temporary homes, synthetic credentials, injected adapters, and loopbac The serial `core-chain` gate exercises the real CLI, Admin server, registry/provider service, WorkerManager, forked proxy worker, fixed ports, provider switching with an in-flight request, restart, shutdown, and secret scans. It deliberately substitutes an in-memory credential adapter and loopback upstreams, so it does not prove native credential access or a real external provider. -Release evidence must include lint, UI typecheck/build/exact three-file verification, the deterministic Node suite, Chromium English/Chinese responsive coverage, the exact package-content allowlist, runtime audit, and the visual comparison recorded in `design-qa.md`. Deterministic fixtures do not claim real Codex history, native credentials, login-start execution, or an external provider; those remain platform/human gates for the reviewed release tree. +Release evidence must include lint, UI typecheck/build/exact manifest verification, the deterministic Node suite, Chromium English/Chinese responsive coverage, the exact package-content allowlist, runtime audit, and the visual comparison recorded in `design-qa.md`. Deterministic fixtures do not claim real Codex history, native credentials, login-start execution, or an external provider; those remain platform/human gates for the reviewed release tree. Supervisor discovery uses a bounded 2-second liveness probe while normal Admin operations use a separate 30-second timeout, so a successful provider test is not misreported as `SUPERVISOR_UNAVAILABLE`. Proxy targets are joined structurally, so base URLs with or without a trailing slash produce one path separator. diff --git a/README.zh-CN.md b/README.zh-CN.md index 1b9dfc9..8679b6c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -30,7 +30,7 @@ npx @cluic/codex-remote-proxy ui `crp ui` 会启动或发现本地 Supervisor,并打开管理界面。已保存的明确语言选择优先;否则按浏览器/系统语言偏好选择第一个受支持的中文或英文,均不匹配时默认英文。推断出的语言不会写入浏览器存储。 -当前开发版界面使用 `node/ui-src/` 中的 React、TypeScript 与 Vite 实现。这些工具只参与构建;发布包和 Admin Server 仍然只交付 `ui/index.html`、`ui/app.js` 与 `ui/styles.css`,不需要前端运行时服务器,也不包含远程字体、CDN、遥测、source map 或动态 chunk。 +管理界面使用 `node/ui-src/` 中的 Next.js App Router、React、TypeScript、Tailwind CSS、基于 Base UI 的 shadcn 源码组件和 Lucide 图标实现。默认 Webpack 构建会在 `node/ui/` 生成经过审核的静态导出及资源清单;发布包不需要前端运行时服务器,Admin Server 从同一个回环来源提供全部页面和 chunk。界面不包含远程字体、CDN、遥测或 source map。干净的 Ubuntu Node 22 是规范构建环境:发布门禁会在任何候选构建覆盖 `node/ui/` 前,将其生成结果与 checkout 比较。Linux、macOS 和 Windows 会校验已提交的源摘要、清单、CSP、资源、package allowlist 及各自的平台测试。 ## 可以管理什么 @@ -240,7 +240,7 @@ npm pack --dry-run --json --ignore-scripts 串行 `core-chain` 门禁会覆盖真实 CLI、Admin 服务、registry/provider service、WorkerManager、fork 出的代理 Worker、固定端口、存在进行中请求时的提供商切换、重启、关闭和密钥扫描。该门禁会有意替换为内存凭据适配器和 loopback 上游,因此不能证明原生凭据读取或真实外部提供商链路。 -发布证据必须包含 lint、UI 类型检查/构建/精确三文件同步验证、确定性 Node 测试、Chromium 英中双语响应式矩阵、精确发布包白名单、生产依赖审计,以及 `design-qa.md` 中的视觉对比。确定性 fixture 不代表真实 Codex 历史、原生凭据、登录启动执行或外部 provider 证据;这些仍属于对应发布代码树的平台/人工门禁。 +发布证据必须包含 lint、UI 类型检查/构建/精确资源清单验证、确定性 Node 测试、Chromium 英中双语响应式矩阵、精确发布包白名单、生产依赖审计,以及 `design-qa.md` 中的视觉对比。确定性 fixture 不代表真实 Codex 历史、原生凭据、登录启动执行或外部 provider 证据;这些仍属于对应发布代码树的平台/人工门禁。 Supervisor 发现使用有界的 2 秒探活,普通 Admin 操作另用 30 秒超时,因此已经成功的 provider test 不会再被误报为 `SUPERVISOR_UNAVAILABLE`。代理目标通过结构化方式拼接,无论 base URL 是否带尾斜杠都只产生一个路径分隔符。 diff --git a/design-qa.md b/design-qa.md index 1aa77f9..7f9e24e 100644 --- a/design-qa.md +++ b/design-qa.md @@ -1,5 +1,15 @@ # CRP Overview Redesign QA +## Next.js management UI migration — 2026-09-05 + +This section supersedes the earlier Vite and fixed three-file UI architecture descriptions. The real CRP management UI now uses a Next.js App Router static export with React, Tailwind CSS, shadcn source components backed by Base UI, and Lucide icons. It keeps the existing same-origin Admin API, in-memory control/CSRF state, explicit fragmentless management recovery, bilingual copy, and every Provider, mapping, routing, Capture, access-key, Metrics, and System mutation contract; no demo data is present in production routes. + +- The approved compact light-green console direction now covers all routes. Overview keeps live routing decisions, account quota, Metrics and the 84-day Token heatmap; its route decision surface uses a deep-green operational rail without changing scheduler semantics. +- Dialogs, mobile navigation, forwarding details, buttons, badges, separators, tooltips, and menus use the shared shadcn/Base UI layer. Forwarding details retain on-demand abortable loading, Summary/Request/Response separation, exact raw-body copy, record stepping, Escape/backdrop behavior, and focus return. +- The deterministic Webpack static export is served from the existing loopback Admin origin through a generated asset manifest. CSP allows only the exact reviewed inline bootstrap hashes for each generated route; external assets, remote fonts, source maps, inline style attributes, and runtime frontend servers remain absent. Release CI separately reproduces the manifest on every supported platform. +- Final UI verification passed: typecheck, the deterministic static build, byte-exact manifest verification, lint across 78 source files, the exact package-content check 3/3, and Chromium E2E 73/73. Full and runtime dependency audits reported zero vulnerabilities. The full `npm test` run reached the serial fixed-port core-real-chain gate but could not bind `127.0.0.1:15100` because the user's existing CRP service owns it; that service was not stopped or changed, and this is not reported as a core-chain pass. +- Settled Overview evidence: `output/next-ui-migration-2026-09-05/pages/overview-settled-1440x900.png` and `output/next-ui-migration-2026-09-05/pages/overview-settled-390x844.png`. Final forwarding list/detail evidence is under `output/next-ui-migration-2026-09-05/pages/e2e-release-final/`. The isolated, synthetic-data, fragmentless preview is available locally at `http://localhost:4318/`; it does not use the user's running CRP service. + ## 0.4.20 startup regression follow-up — 2026-09-05 Root cause: the forwarding metadata covering index was created synchronously inside Capture initialization, before the proxy Worker could send its configured acknowledgement. On a nonempty, body-heavy database, one-time index construction could exceed the existing five-second acknowledgement deadline. A running `crp update` correctly rolled back when activation failed; an update performed after shutdown did not need to restart a previously stopped Worker, so the same failure appeared later at `crp start`. The npm 0.4.20 artifact's integrity and all 46 shipped files were verified against the reviewed release commit, ruling out a mismatched package as the explanation. diff --git a/node/.changeset/next-ui-migration.md b/node/.changeset/next-ui-migration.md new file mode 100644 index 0000000..ac4450f --- /dev/null +++ b/node/.changeset/next-ui-migration.md @@ -0,0 +1,5 @@ +--- +"@cluic/codex-remote-proxy": patch +--- + +Migrate the complete local management UI to a static Next.js App Router build using Tailwind CSS and shadcn components backed by Base UI while preserving the existing Admin API and security behavior. diff --git a/node/README.md b/node/README.md index 8998e04..49c56e6 100644 --- a/node/README.md +++ b/node/README.md @@ -21,7 +21,9 @@ npx @cluic/codex-remote-proxy ui `crp ui` is the normal setup and management entry point. It starts or discovers the loopback supervisor, opens the local management UI, and supports complete English and Simplified Chinese interfaces. -The current development source is a responsive React + TypeScript SPA under `ui-src/`, built with Vite. Build tools and source are not shipped: the package contains exactly `ui/index.html`, `ui/app.js`, and `ui/styles.css` for the existing same-origin Admin server. +The responsive management UI under `ui-src/` uses Next.js App Router, React, TypeScript, Tailwind CSS, shadcn source components backed by Base UI, and Lucide icons. Build tools and source are not shipped: the default Webpack-backed `build:ui` creates a reviewed static export and `.crp-ui-manifest.json` under `ui/`, and the Admin server serves its pages, Flight data, and hashed chunks from the same loopback origin without a frontend runtime server. Clean Ubuntu Node 22 is the canonical compiler environment. Release gates run `verify:ui-build` against the untouched checkout there; platform jobs use `verify:ui-assets` to bind the committed manifest to its source digest and security policy while retaining their native checks. + +Release artifacts use a clean Ubuntu Node 22 Webpack build as the canonical output. Platform jobs validate the committed manifest and security policy alongside their native tests; local host builds must not replace the canonical `ui/` tree. ## Product Behavior @@ -178,11 +180,11 @@ node --test test/package-content.test.mjs test/native-keyring-smoke.test.mjs tes npm pack --dry-run --json --ignore-scripts ``` -The current package-content test requires the exact reviewed 47-file allowlist, including provider presets/build metadata, the route preview and provider scheduler, client-key store/private-token support, Forwarding Records service, the isolated Capture index worker, start-at-login service, and exactly three generated UI assets. It rejects UI development source, runtime state, credentials, tests, Changesets, logs, databases, and generated output outside the reviewed UI files. Deterministic tests use temporary homes, synthetic credentials, injected credential adapters, and loopback upstreams. +The package-content test compares the exact reviewed allowlist, including provider presets/build metadata, the route preview and provider scheduler, client-key store/private-token support, Forwarding Records service, the isolated Capture index worker, start-at-login service, and every file declared by the generated UI asset manifest. It rejects UI development source, runtime state, credentials, tests, Changesets, logs, databases, and generated output outside the reviewed UI files. Deterministic tests use temporary homes, synthetic credentials, injected credential adapters, and loopback upstreams. The serial `core-chain` group uses the production CLI/Admin/registry/provider/WorkerManager/forked-worker path and proves switching, in-flight snapshots, restart, shutdown, cleanup, and secret scans. Its injected memory credential adapter and loopback upstreams do not satisfy the separate real native-keyring/external-provider gate. -Release evidence must include lint, UI typecheck/build/exact-output verification, deterministic Node tests, Chromium English/Chinese responsive coverage, the exact 44-file package allowlist, runtime audit, and the comparison recorded in `../design-qa.md`. Deterministic fixtures do not prove real native-keyring, login-start execution, or external-upstream behavior; those remain platform/human gates. +Release evidence must include lint, UI typecheck/build/exact-manifest verification, deterministic Node tests, Chromium English/Chinese responsive coverage, the exact reviewed package allowlist, runtime audit, and the comparison recorded in `../design-qa.md`. Deterministic fixtures do not prove real native-keyring, login-start execution, or external-upstream behavior; those remain platform/human gates. Supervisor discovery applies a 2-second liveness probe and returns a client with a separate 30-second operation timeout. Proxy forwarding joins base and incoming URLs structurally, preserving base paths and query parameters while avoiding duplicate path separators. The retained `provider add --api-key ` behavior and broader child-environment minimization remain explicit future follow-up work and do not block local core completion. diff --git a/node/RELEASING.md b/node/RELEASING.md index a70c082..b4bd7a8 100644 --- a/node/RELEASING.md +++ b/node/RELEASING.md @@ -42,7 +42,7 @@ Run from `node/`: ```bash npm run lint npm run typecheck:ui -npm run build:ui +npm run verify:ui-assets npm run verify:ui-build npm test node --test test/codex-config.test.mjs test/codex-history-repair.test.mjs @@ -54,6 +54,12 @@ npm pack --dry-run --json --ignore-scripts npm run changeset -- status ``` +The production UI is a Next.js App Router static export produced by the default Webpack path. `build:ui` refreshes the reviewed `ui/.crp-ui-manifest.json`, route HTML and Flight data, and hashed local chunks; `verify:ui-build` reproduces that complete tree in the canonical environment and compares it with the untouched checkout. `verify:ui-assets` binds the committed source digest to the manifest and reuses the complete resource, CSP, source-map, and external-reference policy without rebuilding. Release review must use the manifest and package-content allowlist rather than a fixed three-file assumption. + +The UI build ID fingerprints the UI source and dependency graph while normalizing the package-lock root version fields. A Changesets version-only release PR therefore keeps the committed UI manifest compatible without rebuilding frontend assets; dependency graph changes still require a fresh `build:ui` output. + +Canonical UI candidate artifacts are generated with the default Webpack build on a clean Ubuntu Node 22 runner and uploaded only for review. Release preflight and the release workflow run `verify:ui-build` before any candidate build can replace `ui/`; macOS, Windows, and Linux platform jobs run `verify:ui-assets`, the package allowlist, and their platform-specific tests. Push source and validation changes first, download the candidate produced for that exact commit, inspect it, replace `ui/`, refresh the literal package allowlist, and commit the reviewed result before it can pass the checkout comparison. Do not replace canonical artifacts with a local host build. + Historical M2D/V7 working-tree evidence on Node 22.19 on 2026-07-16: - exact full suite: 451/451 total (`401` unit-core + `8` isolated capture + `41` ordinary integration + `1` serial core-chain); diff --git a/node/package-lock.json b/node/package-lock.json index d5fa75c..04daf00 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -16,31 +16,111 @@ "crp": "bin/crp.mjs" }, "devDependencies": { + "@base-ui/react": "1.8.0", "@changesets/cli": "^2.31.0", "@playwright/test": "1.61.1", + "@tailwindcss/postcss": "4.3.3", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "6.0.3", - "lucide-react": "1.24.0", - "react": "19.2.7", - "react-dom": "19.2.7", - "typescript": "7.0.2", - "vite": "8.1.5" + "class-variance-authority": "0.7.1", + "cn": "0.2.5", + "lucide-react": "1.41.0", + "next": "16.3.4", + "react": "19.2.8", + "react-dom": "19.2.8", + "tailwindcss": "4.3.3", + "tw-animate-css": "1.4.0", + "typescript": "5.9.3" }, "engines": { "node": ">=22.13.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@base-ui/react": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.8.0.tgz", + "integrity": "sha512-P0/1sxo6SBVZOklKMIedvTWqw2s2IQzi9x5bIVsXu980cuSOD4NeuRSs+/L7LZQfDkZP/uRZyGPyfFl/B1oH+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "0.4.0", + "@floating-ui/react-dom": "^2.1.9", + "@floating-ui/utils": "^0.2.12", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@date-fns/tz": "^1.2.0", + "@types/react": "^17 || ^18 || ^19", + "date-fns": "^4.0.0", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@date-fns/tz": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "date-fns": { + "optional": true + } + } + }, + "node_modules/@base-ui/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@floating-ui/utils": "^0.2.12", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@changesets/apply-release-plan": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.1.1.tgz", @@ -317,508 +397,1014 @@ "tslib": "^2.4.0" } }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "dev": true, "license": "MIT", "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@manypkg/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.5.5", - "@types/node": "^12.7.1", - "find-up": "^4.1.0", - "fs-extra": "^8.1.0" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@manypkg/find-root/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@manypkg/find-root/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "@floating-ui/dom": "^1.8.0" }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/@manypkg/get-packages": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", - "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.5.5", - "@changesets/types": "^4.0.1", - "@manypkg/find-root": "^1.1.0", - "fs-extra": "^8.1.0", - "globby": "^11.0.0", - "read-yaml-file": "^1.1.0" + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", - "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "dev": true, "license": "MIT" }, - "node_modules/@manypkg/get-packages/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, + "optional": true, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=18" } }, - "node_modules/@napi-rs/keyring": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.3.0.tgz", - "integrity": "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==", - "license": "MIT", + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 10" + "node": ">=20.9.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@napi-rs/keyring-darwin-arm64": "1.3.0", - "@napi-rs/keyring-darwin-x64": "1.3.0", - "@napi-rs/keyring-freebsd-x64": "1.3.0", - "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", - "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", - "@napi-rs/keyring-linux-arm64-musl": "1.3.0", - "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", - "@napi-rs/keyring-linux-x64-gnu": "1.3.0", - "@napi-rs/keyring-linux-x64-musl": "1.3.0", - "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", - "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", - "@napi-rs/keyring-win32-x64-msvc": "1.3.0" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, - "node_modules/@napi-rs/keyring-darwin-arm64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.3.0.tgz", - "integrity": "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==", + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ - "arm64" + "x64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 10" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" } }, - "node_modules/@napi-rs/keyring-darwin-x64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.3.0.tgz", - "integrity": "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==", + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ - "x64" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@napi-rs/keyring-freebsd-x64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.3.0.tgz", - "integrity": "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==", + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], - "license": "MIT", + "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "freebsd" + "darwin" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.3.0.tgz", - "integrity": "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==", + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], - "license": "MIT", + "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@napi-rs/keyring-linux-arm64-gnu": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.3.0.tgz", - "integrity": "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==", + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], - "license": "MIT", + "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@napi-rs/keyring-linux-arm64-musl": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.3.0.tgz", - "integrity": "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==", + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", "cpu": [ - "arm64" + "ppc64" ], - "license": "MIT", + "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.3.0.tgz", - "integrity": "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==", + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", "cpu": [ "riscv64" ], - "license": "MIT", + "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@napi-rs/keyring-linux-x64-gnu": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.3.0.tgz", - "integrity": "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==", + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ - "x64" + "s390x" ], - "license": "MIT", + "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@napi-rs/keyring-linux-x64-musl": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.3.0.tgz", - "integrity": "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==", + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], - "license": "MIT", + "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@napi-rs/keyring-win32-arm64-msvc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.3.0.tgz", - "integrity": "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==", + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], - "license": "MIT", + "dev": true, + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "win32" + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" ], "engines": { - "node": ">= 10" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" } }, - "node_modules/@napi-rs/keyring-win32-ia32-msvc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.3.0.tgz", - "integrity": "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==", + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ - "ia32" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">= 10" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" } }, - "node_modules/@napi-rs/keyring-win32-x64-msvc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.3.0.tgz", - "integrity": "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==", + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", "cpu": [ - "x64" + "ppc64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">= 10" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "url": "https://opencollective.com/libvips" }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@emnapi/runtime": "^1.11.3" }, "engines": { - "node": ">= 8" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "node_modules/@img/sharp-wasm32/node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@playwright/test": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", - "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "Apache-2.0", + "optional": true, "dependencies": { - "playwright": "1.61.1" - }, - "bin": { - "playwright": "cli.js" + "@img/sharp-wasm32": "0.35.4" }, "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "android" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ - "arm64" + "ia32" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@manypkg/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" + } + }, + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/find-root/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", + "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@changesets/types": "^4.0.1", + "@manypkg/find-root": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "read-yaml-file": "^1.1.0" + } + }, + "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", + "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/get-packages/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@napi-rs/keyring": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.3.0.tgz", + "integrity": "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/keyring-darwin-arm64": "1.3.0", + "@napi-rs/keyring-darwin-x64": "1.3.0", + "@napi-rs/keyring-freebsd-x64": "1.3.0", + "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", + "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", + "@napi-rs/keyring-linux-arm64-musl": "1.3.0", + "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-musl": "1.3.0", + "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", + "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", + "@napi-rs/keyring-win32-x64-msvc": "1.3.0" + } + }, + "node_modules/@napi-rs/keyring-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.3.0.tgz", + "integrity": "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-freebsd-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.3.0.tgz", + "integrity": "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.3.0.tgz", + "integrity": "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.3.0.tgz", + "integrity": "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-arm64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.3.0.tgz", + "integrity": "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-ia32-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.3.0.tgz", + "integrity": "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-x64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.3.0.tgz", + "integrity": "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", - "cpu": [ - "x64" - ], + "node_modules/@next/env": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.4.tgz", + "integrity": "sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "license": "MIT" }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "node_modules/@next/swc-darwin-arm64": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.4.tgz", + "integrity": "sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "node_modules/@next/swc-darwin-x64": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.4.tgz", + "integrity": "sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.4.tgz", + "integrity": "sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==", "cpu": [ "arm64" ], @@ -829,15 +1415,15 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.4.tgz", + "integrity": "sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", @@ -846,15 +1432,15 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.4.tgz", + "integrity": "sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==", "cpu": [ - "s390x" + "x64" ], "dev": true, "license": "MIT", @@ -863,13 +1449,13 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.4.tgz", + "integrity": "sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==", "cpu": [ "x64" ], @@ -880,510 +1466,419 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.4.tgz", + "integrity": "sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.4.tgz", + "integrity": "sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 8" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", - "cpu": [ - "arm64" - ], + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 8" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", - "cpu": [ - "x64" - ], + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 8" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", "dev": true, - "license": "MIT", - "optional": true, + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/node": { - "version": "25.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", - "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", "dev": true, - "license": "MIT", - "optional": true, - "peer": true, + "license": "Apache-2.0", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "tslib": "^2.8.0" } }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.2.2" + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" } }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "aix" + "android" ], "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", "cpu": [ "arm" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", "cpu": [ - "s390x" + "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", "cpu": [ - "x64" + "wasm32" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, "engines": { - "node": ">=16.20.0" + "node": ">=14.0.0" } }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=16.20.0" + "node": ">= 20" } }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=16.20.0" + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" } }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], + "node_modules/@types/node": { + "version": "25.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", + "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" + "peer": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" } }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } + "@types/react": "^19.2.0" } }, "node_modules/ansi-colors": { @@ -1423,6 +1918,19 @@ "node": ">=8" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/better-path-resolve": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", @@ -1449,6 +1957,27 @@ "node": ">=8" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chardet": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", @@ -1456,6 +1985,49 @@ "dev": true, "license": "MIT" }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cn": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/cn/-/cn-0.2.5.tgz", + "integrity": "sha512-OCjZtMeQfXbI4Es1+EIjkd77gvWzaE689gD8KhfexlqjClC06qR1MQBR+Z35ZMSPNEBWyHiItW1Soy0UvwNv9w==", + "dev": true, + "license": "MIT", + "bin": { + "cn": "bin/cn.mjs" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1511,6 +2083,20 @@ "node": ">=8" } }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -1777,10 +2363,20 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -2092,15 +2688,25 @@ "license": "MIT" }, "node_modules/lucide-react": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz", - "integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==", + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.41.0.tgz", + "integrity": "sha512-6lksP35l6KszDKUeRTi4LV7i6DEe0Yzl2ALJm9j4c5xEYN91GdW1xGsawGMOg2mgjF5GHBVX8pKX9kP+cWsP3Q==", "dev": true, "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2136,9 +2742,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -2154,6 +2760,60 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/next": { + "version": "16.3.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.4.tgz", + "integrity": "sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/env": "16.3.4", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.4", + "@next/swc-darwin-x64": "16.3.4", + "@next/swc-linux-arm64-gnu": "16.3.4", + "@next/swc-linux-arm64-musl": "16.3.4", + "@next/swc-linux-x64-gnu": "16.3.4", + "@next/swc-linux-x64-musl": "16.3.4", + "@next/swc-win32-arm64-msvc": "16.3.4", + "@next/swc-win32-x64-msvc": "16.3.4", + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, "node_modules/outdent": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", @@ -2326,9 +2986,9 @@ } }, "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "dev": true, "funding": [ { @@ -2346,7 +3006,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2409,9 +3069,9 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "dev": true, "license": "MIT", "engines": { @@ -2419,16 +3079,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/read-yaml-file": { @@ -2458,9 +3118,9 @@ } }, "node_modules/read-yaml-file/node_modules/js-yaml": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", - "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "license": "MIT", "dependencies": { @@ -2471,6 +3131,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/reselect": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", + "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -2492,40 +3159,6 @@ "node": ">=0.10.0" } }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2565,9 +3198,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -2577,6 +3210,57 @@ "node": ">=10" } }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2674,65 +3358,62 @@ "node": ">=4" } }, - "node_modules/term-size": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", - "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" + "client-only": "0.0.1" }, "engines": { - "node": ">=12.0.0" + "node": ">= 12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "node": ">=6" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/to-regex-range": { @@ -2753,42 +3434,30 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } }, "node_modules/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=16.20.0" - }, - "optionalDependencies": { - "@typescript/typescript-aix-ppc64": "7.0.2", - "@typescript/typescript-darwin-arm64": "7.0.2", - "@typescript/typescript-darwin-x64": "7.0.2", - "@typescript/typescript-freebsd-arm64": "7.0.2", - "@typescript/typescript-freebsd-x64": "7.0.2", - "@typescript/typescript-linux-arm": "7.0.2", - "@typescript/typescript-linux-arm64": "7.0.2", - "@typescript/typescript-linux-loong64": "7.0.2", - "@typescript/typescript-linux-mips64el": "7.0.2", - "@typescript/typescript-linux-ppc64": "7.0.2", - "@typescript/typescript-linux-riscv64": "7.0.2", - "@typescript/typescript-linux-s390x": "7.0.2", - "@typescript/typescript-linux-x64": "7.0.2", - "@typescript/typescript-netbsd-arm64": "7.0.2", - "@typescript/typescript-netbsd-x64": "7.0.2", - "@typescript/typescript-openbsd-arm64": "7.0.2", - "@typescript/typescript-openbsd-x64": "7.0.2", - "@typescript/typescript-sunos-x64": "7.0.2", - "@typescript/typescript-win32-arm64": "7.0.2", - "@typescript/typescript-win32-x64": "7.0.2" + "node": ">=14.17" } }, "node_modules/undici-types": { @@ -2810,110 +3479,14 @@ "node": ">= 4.0.0" } }, - "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "dev": true, "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/which": { diff --git a/node/package.json b/node/package.json index 809784b..541f84a 100644 --- a/node/package.json +++ b/node/package.json @@ -37,7 +37,8 @@ "guide": "node bin/crp.mjs guide", "dev:cli": "node bin/crp.mjs", "lint": "node scripts/check-source.mjs", - "build:ui": "vite build --config vite.config.mjs", + "build:ui": "node scripts/build-next-ui.mjs", + "verify:ui-assets": "node scripts/verify-ui-assets.mjs", "typecheck:ui": "tsc -p tsconfig.ui.json", "verify:ui-build": "node scripts/verify-ui-build.mjs", "test": "node scripts/run-tests.mjs", @@ -52,6 +53,15 @@ "publishConfig": { "access": "public" }, + "overrides": { + "@changesets/parse": { + "js-yaml": "4.3.1" + }, + "nanoid": "3.3.18", + "read-yaml-file": { + "js-yaml": "3.15.2" + } + }, "engines": { "node": ">=22.13.0" }, @@ -60,15 +70,20 @@ "fzstd": "^0.1.1" }, "devDependencies": { + "@base-ui/react": "1.8.0", "@changesets/cli": "^2.31.0", "@playwright/test": "1.61.1", + "@tailwindcss/postcss": "4.3.3", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "6.0.3", - "lucide-react": "1.24.0", - "react": "19.2.7", - "react-dom": "19.2.7", - "typescript": "7.0.2", - "vite": "8.1.5" + "class-variance-authority": "0.7.1", + "cn": "0.2.5", + "lucide-react": "1.41.0", + "next": "16.3.4", + "react": "19.2.8", + "react-dom": "19.2.8", + "tailwindcss": "4.3.3", + "tw-animate-css": "1.4.0", + "typescript": "5.9.3" } } diff --git a/node/scripts/build-next-ui.mjs b/node/scripts/build-next-ui.mjs new file mode 100644 index 0000000..e82b884 --- /dev/null +++ b/node/scripts/build-next-ui.mjs @@ -0,0 +1,326 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { execFile } from "node:child_process"; +import { + copyFile, + lstat, + mkdir, + readFile, + readdir, + rename, + rm, + writeFile +} from "node:fs/promises"; +import { dirname, extname, join, relative, resolve, sep } from "node:path"; +import { promisify } from "node:util"; +import { pathToFileURL } from "node:url"; + +const execFileAsync = promisify(execFile); +const packageRoot = resolve(import.meta.dirname, ".."); +const uiSourceRoot = join(packageRoot, "ui-src"); +const generatedUiRoot = join(packageRoot, "ui"); +const manifestName = ".crp-ui-manifest.json"; +const generatedBuildDirectory = ".crp-next-ui-build"; +const ignoredSourceDirectories = new Set([ + ".next", + ".crp-next-ui-build", + "node_modules", + "out" +]); +const ignoredSourceFiles = new Set([".DS_Store", "tsconfig.tsbuildinfo"]); +const sourceMapPattern = /(?:^|\/)\S+\.map$/; +const assetUrlPattern = /\b(?:src|href)=(["'])(.*?)\1/gi; +const cssExternalUrlPattern = /(?:@import\s+|url\(\s*["']?)((?:https?:)?\/\/[^\s"')]+)/gi; +const javascriptExternalFetchPattern = /\bfetch\(\s*["']((?:https?:)?\/\/[^"']+)/g; +const inlineScriptPattern = /]*)>([\s\S]*?)<\/script\s*>/gi; + +function compareStableNames(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function usage() { + return "Usage: node scripts/build-next-ui.mjs [--verify]"; +} + +function assertNoUnknownArguments(argumentsList) { + if (argumentsList.length > 1 || argumentsList.length === 1 && argumentsList[0] !== "--verify") { + throw new Error(usage()); + } +} + +function pathWithin(root, candidate) { + const relativePath = relative(root, candidate); + return relativePath === "" || !relativePath.startsWith(`..${sep}`) && relativePath !== ".." && !relativePath.startsWith("../"); +} + +async function listFiles(root, { ignoredDirectories = new Set(), ignoredFiles = new Set() } = {}) { + const files = []; + async function visit(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + for (const entry of entries.sort((left, right) => compareStableNames(left.name, right.name))) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + if (!ignoredDirectories.has(entry.name)) await visit(path); + continue; + } + if (entry.isSymbolicLink()) throw new Error(`Symbolic links are prohibited: ${path}`); + if (!entry.isFile()) throw new Error(`Unsupported filesystem entry: ${path}`); + if (!ignoredFiles.has(entry.name)) files.push(path); + } + } + await visit(root); + return files.sort(compareStableNames); +} + +export function normalizeUiSourceBytes(bytes) { + return Buffer.from(bytes.toString("utf8").replace(/\r\n?/g, "\n"), "utf8"); +} + +export async function sourceDigest() { + const files = await listFiles(uiSourceRoot, { + ignoredDirectories: ignoredSourceDirectories, + ignoredFiles: ignoredSourceFiles + }); + const hash = createHash("sha256"); + for (const file of files) { + const relativePath = relative(uiSourceRoot, file).replaceAll(sep, "/"); + const bytes = normalizeUiSourceBytes(await readFile(file)); + hash.update(relativePath); + hash.update("\0"); + hash.update(String(bytes.length)); + hash.update("\0"); + hash.update(bytes); + hash.update("\0"); + } + const lockfile = JSON.parse(await readFile(join(packageRoot, "package-lock.json"), "utf8")); + const normalizedLockfile = JSON.stringify(normalizeLockfileForUiDigest(lockfile)); + hash.update("package-lock.json\0"); + hash.update(String(Buffer.byteLength(normalizedLockfile, "utf8"))); + hash.update("\0"); + hash.update(normalizedLockfile); + return `crp-${hash.digest("hex")}`; +} + +function buildEnvironment({ buildId, distDir }) { + const allowed = ["HOME", "LANG", "LC_ALL", "LC_CTYPE", "NODE_OPTIONS", "PATH", "TMPDIR", "TZ"]; + const environment = Object.fromEntries( + allowed.flatMap((key) => process.env[key] === undefined ? [] : [[key, process.env[key]]]) + ); + return { + ...environment, + CI: "true", + CRP_UI_BUILD_ID: buildId, + CRP_UI_DIST_DIR: distDir, + NODE_ENV: "production" + }; +} + +export function normalizeLockfileForUiDigest(lockfile) { + const normalized = structuredClone(lockfile); + const releaseVersion = "__crp_release_version__"; + normalized.version = releaseVersion; + if (normalized.packages?.[""] && typeof normalized.packages[""] === "object") { + normalized.packages[""].version = releaseVersion; + } + return normalized; +} + +function hashInlineScript(source) { + return `sha256-${createHash("sha256").update(source, "utf8").digest("base64")}`; +} + +function assertSafeGeneratedHtml(html, filename) { + assert.doesNotMatch(html, /)/i, `${filename} contains an inline style element`); + assert.doesNotMatch(html, /\sstyle\s*=/i, `${filename} contains an inline style attribute`); + assert.doesNotMatch(html, /\son[a-z]+\s*=/i, `${filename} contains an inline event handler`); + const scripts = []; + for (const match of html.matchAll(inlineScriptPattern)) { + if (/\bsrc\s*=/i.test(match[1])) continue; + scripts.push(hashInlineScript(match[2])); + } + return [...new Set(scripts)].sort(); +} + +function pathnameForHtml(relativePath) { + if (relativePath === "index.html") return ["/", "/index.html"]; + if (relativePath === "404.html" || relativePath === "_not-found.html") return []; + if (!relativePath.endsWith(".html")) return []; + return [`/${relativePath.slice(0, -".html".length)}`]; +} + +function pathnameForStaticAsset(relativePath) { + return `/${relativePath}`; +} + +function assertSafeReference(value, filename, knownPaths) { + if (value.length === 0 || value.startsWith("#") || value.startsWith("data:")) return; + if (value.startsWith("http://") || value.startsWith("https://") || value.startsWith("//")) { + throw new Error(`${filename} references an external resource: ${value}`); + } + if (!value.startsWith("/")) throw new Error(`${filename} references a non-root resource: ${value}`); + const pathname = value.split("?", 1)[0]; + if (!knownPaths.has(pathname)) throw new Error(`${filename} references an unknown UI resource: ${value}`); +} + +export async function inspectUiAssets(outputRoot, buildId) { + const files = await listFiles(outputRoot, { ignoredFiles: new Set([manifestName]) }); + const assets = {}; + const html = []; + for (const file of files) { + const relativePath = relative(outputRoot, file).replaceAll(sep, "/"); + if (sourceMapPattern.test(relativePath)) throw new Error(`Source maps are prohibited: ${relativePath}`); + const extension = extname(relativePath).toLowerCase(); + const inlineScriptHashes = extension === ".html" + ? assertSafeGeneratedHtml(await readFile(file, "utf8"), relativePath) + : []; + if (extension === ".html") html.push({ relativePath, file, inlineScriptHashes }); + if (relativePath !== "404.html" && relativePath !== "_not-found.html") { + const paths = extension === ".html" + ? pathnameForHtml(relativePath) + : [pathnameForStaticAsset(relativePath)]; + for (const pathname of paths) { + if (Object.hasOwn(assets, pathname)) throw new Error(`Duplicate UI route: ${pathname}`); + assets[pathname] = { file: relativePath, inlineScriptHashes }; + } + } + } + const notFound = html.find((entry) => entry.relativePath === "404.html") + ?? html.find((entry) => entry.relativePath === "_not-found.html"); + if (!notFound || !Object.hasOwn(assets, "/") || !Object.hasOwn(assets, "/index.html")) { + throw new Error("Next static export is missing a required HTML document"); + } + const knownPaths = new Set(Object.keys(assets)); + for (const entry of html) { + const source = await readFile(entry.file, "utf8"); + for (const match of source.matchAll(assetUrlPattern)) { + assertSafeReference(match[2], entry.relativePath, knownPaths); + } + } + for (const file of files) { + const relativePath = relative(outputRoot, file).replaceAll(sep, "/"); + if (!/\.(?:css|js|mjs)$/i.test(relativePath)) continue; + const source = await readFile(file, "utf8"); + const externalPattern = relativePath.endsWith(".css") + ? cssExternalUrlPattern + : javascriptExternalFetchPattern; + for (const match of source.matchAll(externalPattern)) { + throw new Error(`${relativePath} contains an external URL: ${match[0]}`); + } + assert.doesNotMatch(source, /sourceMappingURL=/, `${relativePath} contains a source map reference`); + } + const manifest = { + version: 1, + buildId, + assets: Object.fromEntries(Object.entries(assets).sort(([left], [right]) => left.localeCompare(right))), + notFound: { + file: notFound.relativePath, + inlineScriptHashes: notFound.inlineScriptHashes + } + }; + return manifest; +} + +async function buildManifest(outputRoot, buildId) { + const manifest = await inspectUiAssets(outputRoot, buildId); + await writeFile(join(outputRoot, manifestName), `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); + return manifest; +} + +export async function verifyCommittedUiAssets() { + const buildId = await sourceDigest(); + const expectedManifest = await inspectUiAssets(generatedUiRoot, buildId); + const committedManifest = JSON.parse(await readFile(join(generatedUiRoot, manifestName), "utf8")); + assert.deepEqual( + committedManifest, + expectedManifest, + "committed UI manifest does not match the source and reviewed assets" + ); + return expectedManifest; +} + +async function copyTree(source, destination) { + await mkdir(destination, { recursive: true }); + for (const sourcePath of await listFiles(source)) { + const relativePath = relative(source, sourcePath); + const destinationPath = resolve(destination, relativePath); + if (!pathWithin(destination, destinationPath)) throw new Error("Generated UI path escapes the staging directory"); + await mkdir(dirname(destinationPath), { recursive: true }); + await copyFile(sourcePath, destinationPath); + } +} + +async function normalizeGeneratedJavaScript(root) { + for (const file of await listFiles(root)) { + if (!/\.js$/i.test(file)) continue; + const source = await readFile(file, "utf8"); + const normalized = source.replace(/[ \t]+$/gm, ""); + if (normalized !== source) await writeFile(file, normalized, "utf8"); + } +} + +async function replaceGeneratedUi(outputRoot) { + const stagingRoot = await mkdir(join(packageRoot, ".ui-next-stage"), { recursive: true }) + .then(() => join(packageRoot, ".ui-next-stage")); + await rm(stagingRoot, { recursive: true, force: true }); + await copyTree(outputRoot, stagingRoot); + const backupRoot = join(packageRoot, ".ui-next-backup"); + await rm(backupRoot, { recursive: true, force: true }); + let movedCurrent = false; + try { + await rename(generatedUiRoot, backupRoot); + movedCurrent = true; + await rename(stagingRoot, generatedUiRoot); + } catch (error) { + if (movedCurrent) { + try { await rename(backupRoot, generatedUiRoot); } catch {} + } + throw error; + } finally { + await rm(stagingRoot, { recursive: true, force: true }); + if (movedCurrent) await rm(backupRoot, { recursive: true, force: true }); + } +} + +async function compareTrees(expectedRoot, actualRoot) { + const [expectedFiles, actualFiles] = await Promise.all([listFiles(expectedRoot), listFiles(actualRoot)]); + const expectedRelative = expectedFiles.map((file) => relative(expectedRoot, file).replaceAll(sep, "/")); + const actualRelative = actualFiles.map((file) => relative(actualRoot, file).replaceAll(sep, "/")); + assert.deepEqual(actualRelative, expectedRelative, "committed UI file paths are out of sync"); + for (const relativePath of expectedRelative) { + const [expected, actual] = await Promise.all([ + readFile(join(expectedRoot, relativePath)), + readFile(join(actualRoot, relativePath)) + ]); + assert.deepEqual(actual, expected, `${relativePath} is out of sync`); + } +} + +async function main() { + assertNoUnknownArguments(process.argv.slice(2)); + const verifyOnly = process.argv[2] === "--verify"; + const buildId = await sourceDigest(); + const outputRoot = join(uiSourceRoot, generatedBuildDirectory); + await rm(outputRoot, { recursive: true, force: true }); + try { + await execFileAsync( + process.execPath, + [join(packageRoot, "node_modules", "next", "dist", "bin", "next"), "build", uiSourceRoot, "--webpack"], + { + cwd: packageRoot, + env: buildEnvironment({ buildId, distDir: generatedBuildDirectory }), + maxBuffer: 20 * 1024 * 1024 + } + ); + await normalizeGeneratedJavaScript(outputRoot); + await buildManifest(outputRoot, buildId); + if (verifyOnly) await compareTrees(outputRoot, generatedUiRoot); + else await replaceGeneratedUi(outputRoot); + } finally { + await rm(outputRoot, { recursive: true, force: true }); + } +} + +const directExecution = typeof process.argv[1] === "string" + && import.meta.url === pathToFileURL(resolve(process.argv[1])).href; + +if (directExecution) await main(); diff --git a/node/scripts/preview-next-ui.mjs b/node/scripts/preview-next-ui.mjs new file mode 100644 index 0000000..f42b9a5 --- /dev/null +++ b/node/scripts/preview-next-ui.mjs @@ -0,0 +1,46 @@ +import http from "node:http"; + +import { createFixtureHarness } from "../test/e2e/crp-ui-fixture.mjs"; + +const host = "127.0.0.1"; +const port = Number(process.env.CRP_PREVIEW_PORT ?? "4318"); +if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("CRP_PREVIEW_PORT is invalid."); + +const fixture = await createFixtureHarness(); +fixture.harness.seedProviders({ + providers: [ + { id: "provider-a", name: "Provider Alpha", baseUrl: fixture.harness.upstreamBaseUrl }, + { id: "provider-b", name: "Provider Beta", baseUrl: fixture.harness.upstreamBaseUrl } + ], + activeProviderId: "provider-a" +}); +const target = new URL(fixture.harness.origin); +const session = await fetch(`${target.origin}/api/v1/session`, { + method: "POST", + headers: { authorization: `Bearer ${fixture.harness.controlToken}` } +}); +const previewCookie = session.headers.getSetCookie?.()[0] ?? session.headers.get("set-cookie"); +if (!session.ok || !previewCookie) throw new Error("Unable to create the isolated preview session."); + +const server = http.createServer((request, response) => { + const headers = { ...request.headers, host: target.host }; + if (headers.origin !== undefined) headers.origin = target.origin; + const proxy = http.request({ host: target.hostname, port: target.port, method: request.method, path: request.url, headers }, (upstream) => { + response.writeHead(upstream.statusCode ?? 502, { ...upstream.headers, "set-cookie": previewCookie }); + upstream.pipe(response); + }); + proxy.once("error", () => response.writeHead(502).end()); + request.pipe(proxy); +}); +const shutdown = async () => { + await new Promise((resolvePromise) => server.close(resolvePromise)); + await fixture.cleanup(); +}; +server.once("error", async (error) => { + await fixture.cleanup(); + throw error; +}); +await new Promise((resolvePromise) => server.listen(port, host, resolvePromise)); +process.stdout.write(`CRP Next UI preview: http://localhost:${port}/\n`); +process.once("SIGINT", () => void shutdown().then(() => process.exit(0))); +process.once("SIGTERM", () => void shutdown().then(() => process.exit(0))); diff --git a/node/scripts/verify-ui-assets.mjs b/node/scripts/verify-ui-assets.mjs new file mode 100644 index 0000000..eeb8884 --- /dev/null +++ b/node/scripts/verify-ui-assets.mjs @@ -0,0 +1,5 @@ +import { verifyCommittedUiAssets } from "./build-next-ui.mjs"; + +await verifyCommittedUiAssets(); + +console.log("Committed UI assets match the manifest and security policy."); diff --git a/node/scripts/verify-ui-build.mjs b/node/scripts/verify-ui-build.mjs index 6307b6f..fa178ef 100644 --- a/node/scripts/verify-ui-build.mjs +++ b/node/scripts/verify-ui-build.mjs @@ -1,102 +1,14 @@ -import assert from "node:assert/strict"; import { execFile } from "node:child_process"; -import { mkdtemp, readFile, readdir, rm, stat } from "node:fs/promises"; -import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { promisify } from "node:util"; -import { gzipSync } from "node:zlib"; const execFileAsync = promisify(execFile); const packageRoot = resolve(import.meta.dirname, ".."); -const committedRoot = join(packageRoot, "ui"); -const expectedFiles = ["app.js", "index.html", "styles.css"]; -const budgets = { - "app.js": 300 * 1024, - "index.html": 20 * 1024, - "styles.css": 50 * 1024 -}; -const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]); +await execFileAsync( + process.execPath, + [join(packageRoot, "scripts", "build-next-ui.mjs"), "--verify"], + { cwd: packageRoot, maxBuffer: 20 * 1024 * 1024 } +); -function assertAllowedAbsoluteUrl(rawUrl, context) { - if (rawUrl === "http://" || rawUrl === "https://" || rawUrl.startsWith("http://$") || rawUrl.startsWith("https://$")) { - return; - } - let parsed; - try { - parsed = new URL(rawUrl); - } catch { - assert.fail(`${context} contains an invalid absolute URL: ${rawUrl}`); - } - assert.ok(LOOPBACK_HOSTS.has(parsed.hostname), `${context} contains a non-loopback absolute URL: ${rawUrl}`); -} - -function assertNoHardcodedRemoteUrls(source, context) { - for (const match of source.matchAll(/https?:\/\/(?:\[[0-9A-Fa-f:]+\]|[A-Za-z0-9._~-]+)(?::\d+)?(?:\/[A-Za-z0-9._~!$&'()*+,;=:@%/?#-]*)?/g)) { - assertAllowedAbsoluteUrl(match[0], context); - } -} - -function assertNoForbiddenMarkup(html) { - assert.doesNotMatch(html, /)/i, "inline style elements are prohibited"); - assert.doesNotMatch(html, /]*\bsrc=)[^>]*>/i, "inline scripts are prohibited"); - assert.doesNotMatch(html, /\son[a-z]+\s*=/i, "inline event handlers are prohibited"); - for (const match of html.matchAll(/\b(?:src|href|action|poster)=["'](https?:\/\/[^"']+)["']/gi)) { - assertAllowedAbsoluteUrl(match[1], "built HTML resource"); - } -} - -async function assertUiSourceUrls(directory) { - for (const entry of await readdir(directory, { withFileTypes: true })) { - const path = join(directory, entry.name); - if (entry.isDirectory()) await assertUiSourceUrls(path); - else if (/\.(?:ts|tsx|html)$/.test(entry.name)) { - assertNoHardcodedRemoteUrls(await readFile(path, "utf8"), `UI source ${entry.name}`); - } - } -} - -const temporaryRoot = await mkdtemp(join(tmpdir(), "crp-ui-build-")); -const temporaryOutput = join(temporaryRoot, "ui"); - -try { - await assertUiSourceUrls(join(packageRoot, "ui-src")); - await execFileAsync( - process.execPath, - [ - join(packageRoot, "node_modules", "vite", "bin", "vite.js"), - "build", - "--config", - join(packageRoot, "vite.config.mjs"), - "--outDir", - temporaryOutput, - "--emptyOutDir" - ], - { cwd: packageRoot } - ); - - const actualFiles = (await readdir(temporaryOutput)).sort(); - assert.deepEqual(actualFiles, expectedFiles, "the UI build must contain exactly three files"); - - for (const filename of expectedFiles) { - const [generated, committed] = await Promise.all([ - readFile(join(temporaryOutput, filename)), - readFile(join(committedRoot, filename)) - ]); - assert.deepEqual(generated, committed, `${filename} is not synchronized with ui-src`); - assert.equal((await stat(join(temporaryOutput, filename))).isFile(), true); - assert.ok(gzipSync(generated).length <= budgets[filename], `${filename} exceeds its gzip budget`); - } - - assertNoForbiddenMarkup(await readFile(join(temporaryOutput, "index.html"), "utf8")); - const javascript = await readFile(join(temporaryOutput, "app.js"), "utf8"); - assert.doesNotMatch(javascript, /sourceMappingURL=/, "source maps are prohibited"); - for (const match of javascript.matchAll(/\bfetch\(["'](https?:\/\/[^"']+)["']/g)) { - assertAllowedAbsoluteUrl(match[1], "runtime request"); - } - assert.match(javascript, /SPDX-License-Identifier: MIT AND ISC/, "bundled license notice is required"); -} finally { - await rm(temporaryRoot, { recursive: true, force: true }); -} - -console.log("UI build matches the committed three-file output and size/security policy."); +console.log("Next static UI output matches the committed manifest and security policy."); diff --git a/node/src/supervisor/admin-server.mjs b/node/src/supervisor/admin-server.mjs index ad7933f..76df13c 100644 --- a/node/src/supervisor/admin-server.mjs +++ b/node/src/supervisor/admin-server.mjs @@ -1,13 +1,12 @@ import { randomBytes } from "node:crypto"; import http from "node:http"; import { readFile } from "node:fs/promises"; -import { extname, join } from "node:path"; - import { sanitizeActivityValue } from "./activity-store.mjs"; import { FORWARDING_DETAIL_LIMITS } from "./forwarding-records-service.mjs"; import { listProviderPresets } from "../providers/provider-presets.mjs"; import { getPublicBuildInfo } from "../shared/build-info.mjs"; import { CrpError, toPublicError } from "../shared/errors.mjs"; +import { createUiContentSecurityPolicy, loadUiAssets } from "./ui-assets.mjs"; const API_PREFIX = "/api/v1"; const PUBLIC_PROVIDER_FIELDS = [ @@ -280,7 +279,7 @@ function setSafeHeaders(response) { response.setHeader("cache-control", "no-store"); response.setHeader("x-content-type-options", "nosniff"); response.setHeader("referrer-policy", "no-referrer"); - response.setHeader("content-security-policy", "default-src 'self'; frame-ancestors 'none'; base-uri 'none'"); + response.setHeader("content-security-policy", createUiContentSecurityPolicy()); } function sendJson(response, status, payload, extraHeaders = {}) { @@ -1276,29 +1275,6 @@ function positiveQueryInteger(url, name, fallback, { min = 0, max }) { return value; } -function uiAsset(pathname) { - let decoded; - try { - decoded = decodeURIComponent(pathname); - } catch { - return null; - } - if (/[\\\u0000-\u001f\u007f]/.test(decoded) - || decoded.split("/").includes("..")) { - return null; - } - const explicit = new Map([ - ["/", ["index.html", "text/html; charset=utf-8"]], - ["/index.html", ["index.html", "text/html; charset=utf-8"]], - ["/favicon.ico", [null, "image/x-icon"]], - ["/styles.css", ["styles.css", "text/css; charset=utf-8"]], - ["/app.js", ["app.js", "text/javascript; charset=utf-8"]] - ]); - if (explicit.has(decoded)) return explicit.get(decoded); - if (extname(decoded) === "") return explicit.get("/"); - return null; -} - export function createAdminServer({ auth, providerService, @@ -1334,6 +1310,8 @@ export function createAdminServer({ throw new TypeError("Admin shutdown options are invalid."); } + const uiAssets = typeof uiDir === "string" && uiDir.length > 0 ? loadUiAssets(uiDir) : null; + const runCodexExclusive = createSerialGate(); const runWhenCodexReady = (operation) => runCodexExclusive(async () => { if (typeof codexService?.runWhenReady === "function") { @@ -1465,15 +1443,6 @@ export function createAdminServer({ const apiNamespace = url.pathname === "/api" || url.pathname.startsWith("/api/"); if (!apiNamespace) { - const asset = uiAsset(url.pathname); - if (!asset) { - throw apiError( - "UI_NOT_FOUND", - "The local UI resource was not found.", - "Open the CRP UI root.", - 404 - ); - } if (request.method !== "GET" && request.method !== "HEAD") { response.setHeader("allow", "GET, HEAD"); throw apiError( @@ -1483,7 +1452,7 @@ export function createAdminServer({ 405 ); } - if (typeof uiDir !== "string" || uiDir.length === 0) { + if (uiAssets === null) { throw apiError( "UI_NOT_FOUND", "The local UI resource was not found.", @@ -1491,13 +1460,16 @@ export function createAdminServer({ 404 ); } - if (asset[0] === null) { - sendBytes(response, 204, Buffer.alloc(0), asset[1], { head: true }); + const asset = uiAssets.resolve(request.url); + if (!asset) { + response.setHeader("content-security-policy", uiAssets.contentSecurityPolicy(uiAssets.notFound)); + const bytes = await readFile(uiAssets.notFound.path); + sendBytes(response, 404, bytes, uiAssets.notFound.contentType, { head: request.method === "HEAD" }); return; } let bytes; try { - bytes = await readFile(join(uiDir, asset[0])); + bytes = await readFile(asset.path); } catch { throw apiError( "UI_NOT_FOUND", @@ -1506,7 +1478,8 @@ export function createAdminServer({ 404 ); } - sendBytes(response, 200, bytes, asset[1], { head: request.method === "HEAD" }); + response.setHeader("content-security-policy", uiAssets.contentSecurityPolicy(asset)); + sendBytes(response, 200, bytes, asset.contentType, { head: request.method === "HEAD" }); return; } auth.authorize({ diff --git a/node/src/supervisor/ui-assets.mjs b/node/src/supervisor/ui-assets.mjs new file mode 100644 index 0000000..efcd7ae --- /dev/null +++ b/node/src/supervisor/ui-assets.mjs @@ -0,0 +1,168 @@ +import { readFileSync, statSync } from "node:fs"; +import { extname, resolve, sep } from "node:path"; + +const MANIFEST_NAME = ".crp-ui-manifest.json"; +const HASH_PATTERN = /^sha256-[A-Za-z0-9+/]{43}=$/; +const ASSET_PATH_PATTERN = /^\/[A-Za-z0-9._/-]*$/; +const FILE_PATH_PATTERN = /^[A-Za-z0-9._/-]+$/; +const CONTENT_TYPES = new Map([ + [".css", "text/css; charset=utf-8"], + [".html", "text/html; charset=utf-8"], + [".ico", "image/x-icon"], + [".js", "text/javascript; charset=utf-8"], + [".json", "application/json; charset=utf-8"], + [".svg", "image/svg+xml"], + [".txt", "text/x-component; charset=utf-8"], + [".webp", "image/webp"], + [".png", "image/png"], + [".jpg", "image/jpeg"], + [".jpeg", "image/jpeg"], + [".gif", "image/gif"], + [".avif", "image/avif"] +]); + +function isPlainObject(value) { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function assertExactKeys(value, keys, context) { + if (!isPlainObject(value) || Object.keys(value).some((key) => !keys.includes(key))) { + throw new TypeError(`${context} is invalid`); + } +} + +function normalizeAssetPath(pathname, context) { + if (typeof pathname !== "string" + || !ASSET_PATH_PATTERN.test(pathname) + || pathname.includes("//") + || pathname.split("/").some((part) => part === "." || part === "..")) { + throw new TypeError(`${context} path is invalid`); + } + return pathname; +} + +function normalizeFilePath(file, context) { + if (typeof file !== "string" + || !FILE_PATH_PATTERN.test(file) + || file.startsWith("/") + || file.includes("//") + || file.split("/").some((part) => part === "." || part === "..")) { + throw new TypeError(`${context} file is invalid`); + } + const contentType = CONTENT_TYPES.get(extname(file).toLowerCase()); + if (!contentType) throw new TypeError(`${context} file type is invalid`); + return { file, contentType }; +} + +function normalizeHashes(value, context) { + if (!Array.isArray(value) + || value.some((hash) => typeof hash !== "string" || !HASH_PATTERN.test(hash)) + || new Set(value).size !== value.length) { + throw new TypeError(`${context} inline script hashes are invalid`); + } + return Object.freeze([...value].sort()); +} + +function parseAsset(pathname, value, root) { + assertExactKeys(value, ["file", "inlineScriptHashes"], `UI asset ${pathname}`); + const { file, contentType } = normalizeFilePath(value.file, `UI asset ${pathname}`); + const resolved = resolve(root, file); + if (!resolved.startsWith(`${root}${sep}`)) throw new TypeError(`UI asset ${pathname} escapes the UI root`); + let stats; + try { + stats = statSync(resolved); + } catch (error) { + throw new TypeError(`UI asset ${pathname} is missing`, { cause: error }); + } + if (!stats.isFile()) throw new TypeError(`UI asset ${pathname} is not a file`); + return Object.freeze({ + file, + path: resolved, + contentType, + inlineScriptHashes: normalizeHashes(value.inlineScriptHashes, `UI asset ${pathname}`) + }); +} + +function rawAssetPath(requestTarget) { + if (typeof requestTarget !== "string" || !requestTarget.startsWith("/")) return null; + const queryIndex = requestTarget.indexOf("?"); + const pathname = queryIndex === -1 ? requestTarget : requestTarget.slice(0, queryIndex); + if (!ASSET_PATH_PATTERN.test(pathname) + || pathname.includes("//") + || pathname.split("/").some((part) => part === "." || part === "..")) { + return null; + } + return pathname; +} + +export function createUiContentSecurityPolicy(inlineScriptHashes = []) { + const hashes = normalizeHashes(inlineScriptHashes, "UI CSP"); + return [ + "default-src 'self'", + `script-src 'self'${hashes.length > 0 ? ` ${hashes.map((hash) => `'${hash}'`).join(" ")}` : ""}`, + "script-src-attr 'none'", + "style-src-elem 'self'", + "style-src-attr 'none'", + "connect-src 'self'", + "img-src 'self'", + "font-src 'self'", + "media-src 'none'", + "object-src 'none'", + "worker-src 'none'", + "base-uri 'none'", + "frame-ancestors 'none'", + "form-action 'self'" + ].join("; "); +} + +export function loadUiAssets(uiDir) { + if (typeof uiDir !== "string" || uiDir.length === 0) { + throw new TypeError("UI directory is invalid"); + } + const root = resolve(uiDir); + let manifest; + try { + manifest = JSON.parse(readFileSync(resolve(root, MANIFEST_NAME), "utf8")); + } catch (error) { + throw new TypeError("UI manifest is unavailable", { cause: error }); + } + assertExactKeys(manifest, ["version", "buildId", "assets", "notFound"], "UI manifest"); + if (manifest.version !== 1 + || typeof manifest.buildId !== "string" + || !/^[A-Za-z0-9_-]{16,128}$/.test(manifest.buildId) + || !isPlainObject(manifest.assets)) { + throw new TypeError("UI manifest is invalid"); + } + + const assets = new Map(); + for (const [pathname, value] of Object.entries(manifest.assets)) { + const normalizedPath = normalizeAssetPath(pathname, "UI asset"); + if (normalizedPath === "/.crp-ui-manifest.json") throw new TypeError("UI manifest must not be public"); + const asset = parseAsset(normalizedPath, value, root); + assets.set(normalizedPath, asset); + } + if (!assets.has("/") || !assets.has("/index.html")) { + throw new TypeError("UI manifest is missing the root document"); + } + const notFound = parseAsset("notFound", manifest.notFound, root); + if (assets.get("/")?.contentType !== "text/html; charset=utf-8" + || assets.get("/index.html")?.contentType !== "text/html; charset=utf-8" + || notFound.contentType !== "text/html; charset=utf-8") { + throw new TypeError("UI manifest documents must be HTML"); + } + + return Object.freeze({ + buildId: manifest.buildId, + resolve(requestTarget) { + const pathname = rawAssetPath(requestTarget); + if (pathname === null) return null; + return assets.get(pathname) ?? null; + }, + notFound, + contentSecurityPolicy(asset) { + return createUiContentSecurityPolicy(asset.inlineScriptHashes); + } + }); +} diff --git a/node/test/e2e/crp-ui-fixture.mjs b/node/test/e2e/crp-ui-fixture.mjs index 3fc1e58..ebc87b7 100644 --- a/node/test/e2e/crp-ui-fixture.mjs +++ b/node/test/e2e/crp-ui-fixture.mjs @@ -1,7 +1,7 @@ import { test as base, expect } from "@playwright/test"; import assert from "node:assert/strict"; import { randomBytes } from "node:crypto"; -import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import http from "node:http"; import os from "node:os"; import { join, resolve } from "node:path"; @@ -1753,10 +1753,7 @@ export async function createFixtureHarness({ failAt = null, onResource = () => { resources.tempRoot = mkdtempSync(join(os.tmpdir(), "crp-ui-e2e-")); onResource("tempRoot", resources.tempRoot); const uiRoot = join(resources.tempRoot, "ui"); - mkdirSync(uiRoot, { recursive: true }); - for (const asset of ["index.html", "styles.css", "app.js"]) { - copyFileSync(join(REPO_UI_ROOT, asset), join(uiRoot, asset)); - } + cpSync(REPO_UI_ROOT, uiRoot, { recursive: true, dereference: false, errorOnExist: true }); const controlTokenPath = join(resources.tempRoot, "control-token"); resources.upstream = await createMockUpstream(); onResource("upstreamOrigin", resources.upstream.origin); @@ -2299,6 +2296,7 @@ export async function assertLayoutIntegrity(page) { const rect = element.getBoundingClientRect(); const sidebar = element.closest(".sidebar"); return !element.closest(".visually-hidden") + && !element.matches("[data-base-ui-focus-guard]") && !element.closest(".layout-contained-scroll") && !element.closest("[hidden]") && !element.closest("dialog:not([open])") @@ -2348,6 +2346,7 @@ export async function assertLayoutIntegrity(page) { const rect = element.getBoundingClientRect(); const sidebar = element.closest(".sidebar"); return style.display !== "none" && style.visibility !== "hidden" + && !element.matches("[data-base-ui-focus-guard]") && !element.closest(".visually-hidden") && !element.closest(".layout-contained-scroll") && !element.closest("dialog:not([open])") diff --git a/node/test/e2e/next-navigation.spec.mjs b/node/test/e2e/next-navigation.spec.mjs new file mode 100644 index 0000000..b0601dc --- /dev/null +++ b/node/test/e2e/next-navigation.spec.mjs @@ -0,0 +1,129 @@ +import { assertNoSecrets, expect, test } from "./crp-ui-fixture.mjs"; + +async function openRoute(page, crp, route = "overview") { + await page.goto(`${crp.origin}/${route}#token=${crp.controlToken}`); + await expect(page.locator("html")).toHaveAttribute("aria-busy", "false"); +} + +async function observeCspViolations(page) { + await page.evaluate(() => { + window.__crpCspViolations = []; + window.addEventListener("securitypolicyviolation", (event) => { + window.__crpCspViolations.push({ + blockedURI: event.blockedURI, + effectiveDirective: event.effectiveDirective + }); + }); + }); +} + +async function cspViolations(page) { + return await page.evaluate(() => window.__crpCspViolations ?? []); +} + +test.beforeEach(async ({ crp }) => { + crp.seedProviders({ + providers: [{ id: "provider-1", name: "Primary API", baseUrl: crp.upstreamBaseUrl }], + activeProviderId: "provider-1" + }); +}); + +test("keeps one browser session and in-memory management state across Next Flight navigation", async ({ page, crp }) => { + const sessionRequests = []; + const unexpectedOrigins = []; + page.on("request", (request) => { + const url = new URL(request.url()); + if (url.origin !== crp.origin) unexpectedOrigins.push(url.origin); + if (url.pathname === "/api/v1/session") sessionRequests.push(request); + }); + + await openRoute(page, crp); + await observeCspViolations(page); + await expect(page).toHaveURL(`${crp.origin}/overview`); + await expect(page.getByTestId("app-shell")).toBeVisible(); + const documentIdentity = await page.evaluate(() => { + window.__crpDocumentIdentity ??= crypto.randomUUID(); + return window.__crpDocumentIdentity; + }); + + const flight = page.waitForResponse((response) => ( + new URL(response.url()).pathname === "/providers.txt" + && response.status() === 200 + && response.headers()["content-type"]?.startsWith("text/x-component") === true + )); + await page.getByRole("link", { name: "Providers", exact: true }).click(); + await flight; + await expect(page).toHaveURL(`${crp.origin}/providers`); + await expect(page.getByTestId("app-shell")).toBeVisible(); + expect(await page.evaluate(() => window.__crpDocumentIdentity)).toBe(documentIdentity); + await page.goBack(); + await expect(page).toHaveURL(`${crp.origin}/overview`); + expect(await page.evaluate(() => window.__crpDocumentIdentity)).toBe(documentIdentity); + await page.goForward(); + await expect(page).toHaveURL(`${crp.origin}/providers`); + expect(await page.evaluate(() => window.__crpDocumentIdentity)).toBe(documentIdentity); + expect(sessionRequests).toHaveLength(1); + expect(unexpectedOrigins).toEqual([]); + expect(await cspViolations(page)).toEqual([]); + await assertNoSecrets(page, crp); +}); + +test("hard reload without a fragment remains read-only and never resumes management automatically", async ({ page, crp }) => { + const nonGetRequests = []; + await openRoute(page, crp); + const sessionCallsBeforeReload = crp.calls.filter((call) => call.operation === "session").length; + page.on("request", (request) => { + if (new URL(request.url()).origin === crp.origin && request.method() !== "GET") { + nonGetRequests.push({ pathname: new URL(request.url()).pathname, method: request.method() }); + } + }); + + await page.goto(`${crp.origin}/overview`); + await expect(page.locator("html")).toHaveAttribute("aria-busy", "false"); + await expect(page.getByText("Read-only session", { exact: true })).toBeVisible(); + await expect(page.getByRole("button", { name: "Restore management", exact: true })).toBeVisible(); + expect(crp.calls.filter((call) => call.operation === "session")).toHaveLength(sessionCallsBeforeReload); + expect(nonGetRequests).toEqual([]); + await assertNoSecrets(page, crp); +}); + +test("Next route metadata never overrides the localized client title after navigation", async ({ page, crp }) => { + await openRoute(page, crp, "setup"); + await expect(page).toHaveTitle("Set up Codex Remote Proxy | CRP"); + await page.locator("#locale-select").selectOption("zh-CN"); + await expect(page).toHaveTitle("设置 Codex Remote Proxy | CRP"); + await page.getByRole("link", { name: "概览", exact: true }).click(); + await expect(page).toHaveTitle("概览 | CRP"); + await assertNoSecrets(page, crp); +}); + +test("mobile navigation opens and closes without CSP violations or stale focus restoration", async ({ page, crp }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await openRoute(page, crp); + await observeCspViolations(page); + const trigger = page.getByRole("button", { name: "Open navigation", exact: true }); + await trigger.click(); + const navigation = page.getByRole("dialog", { name: "Navigation" }); + await expect(navigation).toBeVisible(); + await navigation.getByRole("button", { name: "Close navigation", exact: true }).click(); + await expect(navigation).toBeHidden(); + await expect(trigger).toBeFocused(); + expect(await cspViolations(page)).toEqual([]); + await assertNoSecrets(page, crp); +}); + +test("Base UI tooltip and dropdown keep their portal interactions inside the strict CSP", async ({ page, crp }) => { + await openRoute(page, crp); + await observeCspViolations(page); + const refresh = page.getByRole("button", { name: "Refresh status", exact: true }); + await refresh.hover(); + await expect(page.locator("[data-slot='tooltip-content']")).toHaveText("Refresh status"); + const more = page.getByRole("button", { name: "More console actions", exact: true }); + await more.click(); + await expect(page.getByRole("menu")).toBeVisible(); + await expect(page.getByRole("menuitem", { name: "System", exact: true })).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(page.getByRole("menu")).toBeHidden(); + expect(await cspViolations(page)).toEqual([]); + await assertNoSecrets(page, crp); +}); diff --git a/node/test/e2e/onboarding.spec.mjs b/node/test/e2e/onboarding.spec.mjs index 37775e4..b1cdafd 100644 --- a/node/test/e2e/onboarding.spec.mjs +++ b/node/test/e2e/onboarding.spec.mjs @@ -284,7 +284,7 @@ test("clears provider secrets before both local validation and backend failure r await page.getByLabel("API key").fill(localSecret); await observePasswordValues(page); await page.getByRole("button", { name: "Save provider" }).click(); - await expect(page.getByRole("alert")).toBeVisible(); + await expect(page.locator(".form-error")).toBeVisible(); await expect(page.getByLabel("API key")).toHaveValue(""); const localSnapshots = await stopPasswordObserver(page); expect(JSON.stringify(localSnapshots)).not.toContain(localSecret); @@ -295,7 +295,7 @@ test("clears provider secrets before both local validation and backend failure r await page.getByLabel("API key").fill(backendSecret); await observePasswordValues(page); await page.getByRole("button", { name: "Save provider" }).click(); - await expect(page.getByRole("alert")).toBeVisible(); + await expect(page.getByTestId("global-message").locator(".notice-danger")).toBeVisible(); await expect(page.getByLabel("API key")).toHaveValue(""); const backendSnapshots = await stopPasswordObserver(page); expect(JSON.stringify(backendSnapshots)).not.toContain(backendSecret); diff --git a/node/test/e2e/provider-switch.spec.mjs b/node/test/e2e/provider-switch.spec.mjs index b3c8356..bfa4b37 100644 --- a/node/test/e2e/provider-switch.spec.mjs +++ b/node/test/e2e/provider-switch.spec.mjs @@ -707,15 +707,17 @@ test("keeps every V8 page unclipped in both locales at desktop, tablet, and mobi if (mobile) { const menu = page.getByRole("button", { name: /Open navigation|打开导航/ }); await menu.click(); - await expect(page.locator(".sidebar-close")).toBeFocused(); + const mobileDrawer = page.getByRole("dialog", { name: /Primary navigation|主导航/ }); + await expect(mobileDrawer.locator(".sidebar-close")).toBeFocused(); await page.keyboard.press("Escape"); await expect(menu).toBeFocused(); } for (const matrix of matrices) { if (mobile) { await page.getByRole("button", { name: /Open navigation|打开导航/ }).click(); - await page.locator("#locale-select").selectOption(matrix.locale); - await page.locator(".sidebar-close").click(); + const mobileDrawer = page.getByRole("dialog", { name: /Primary navigation|主导航/ }); + await mobileDrawer.getByLabel(/Language|语言/).selectOption(matrix.locale); + await mobileDrawer.locator(".sidebar-close").click(); } else { await page.locator("#locale-select").selectOption(matrix.locale); } @@ -731,10 +733,7 @@ test("keeps every V8 page unclipped in both locales at desktop, tablet, and mobi }); } await test.step(`${viewport.width}x${viewport.height} ${matrix.locale} setup`, async () => { - await page.evaluate(() => { - history.pushState(null, "", "/setup"); - dispatchEvent(new PopStateEvent("popstate")); - }); + await page.goto(new URL("/setup", page.url()).href); await expect(page.getByTestId("page-setup")).toBeVisible(); await assertLayoutIntegrity(page); }); diff --git a/node/test/e2e/restart-and-errors.spec.mjs b/node/test/e2e/restart-and-errors.spec.mjs index 6d46f64..7ad89df 100644 --- a/node/test/e2e/restart-and-errors.spec.mjs +++ b/node/test/e2e/restart-and-errors.spec.mjs @@ -200,7 +200,7 @@ test("does not enter the stopped state when the supervisor identity changed", as await page.getByRole("button", { name: "Exit CRP" }).click(); const confirmation = page.getByRole("dialog", { name: "Exit CRP?" }); await confirmation.getByRole("button", { name: "Exit CRP" }).click(); - const alert = page.getByRole("alert"); + const alert = page.getByTestId("global-message").locator(".notice-danger"); await expect(alert).toContainText("CRP could not complete the operation"); await alert.locator("summary").click(); await expect(alert).toContainText("SUPERVISOR_IDENTITY_CHANGED"); @@ -230,7 +230,7 @@ test("rejects a non-minimal shutdown acceptance without going offline", async ({ await page.getByRole("button", { name: "Exit CRP" }).click(); const confirmation = page.getByRole("dialog", { name: "Exit CRP?" }); await confirmation.getByRole("button", { name: "Exit CRP" }).click(); - const alert = page.getByRole("alert"); + const alert = page.getByTestId("global-message").locator(".notice-danger"); await expect(alert).toContainText("CRP could not complete the operation"); await alert.locator("summary").click(); await expect(alert).toContainText("INTERNAL_ERROR"); @@ -301,7 +301,7 @@ for (const [label, payload] of [ const dialog = await openProviderTest(page, "Provider Alpha"); await dialog.getByLabel("Test model").fill("gpt-5.1-codex-mini"); await dialog.getByRole("button", { name: "Run test" }).click(); - const alert = page.getByRole("alert"); + const alert = page.getByTestId("global-message").locator(".notice-danger"); await expect(alert).toContainText("CRP could not complete the operation"); await dialog.getByRole("button", { name: "Cancel" }).click(); await alert.locator("summary").click(); @@ -378,7 +378,7 @@ test("folds allowlisted technical error details and omits unknown fields", async await openCrp(page, crp); await page.getByRole("link", { name: "System" }).click(); await page.getByRole("button", { name: "Generate diagnostic summary" }).click(); - const alert = page.getByRole("alert"); + const alert = page.getByTestId("global-message").locator(".notice-danger"); const technical = alert.locator("details"); await expect(technical).not.toHaveAttribute("open", ""); await technical.locator("summary").click(); diff --git a/node/test/integration/admin-server.test.mjs b/node/test/integration/admin-server.test.mjs index 5e72176..301a3ef 100644 --- a/node/test/integration/admin-server.test.mjs +++ b/node/test/integration/admin-server.test.mjs @@ -1,5 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; import * as realFileOperations from "node:fs"; import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; import { EventEmitter } from "node:events"; @@ -20,6 +21,8 @@ import { CrpError } from "../../src/shared/errors.mjs"; const SECRET = "admin-api-complete-secret-sentinel"; const CREDENTIAL_REF = "credential-ref-must-not-pass"; const LOCAL_ACCESS_TOKEN = "A".repeat(43); +const TEST_FLIGHT_SCRIPT = "self.__crpFlight=1;"; +const TEST_FLIGHT_HASH = `sha256-${createHash("sha256").update(TEST_FLIGHT_SCRIPT, "utf8").digest("base64")}`; const NO_HISTORY_REPAIR = Object.freeze({ required: false, completed: false, @@ -732,13 +735,31 @@ function createServices() { }; } -async function createHarness(t, overrides = {}) { - const dir = mkdtempSync(join(os.tmpdir(), "crp-admin-server-")); - const uiDir = join(dir, "ui"); +function writeAdminUiFixture(uiDir) { mkdirSync(uiDir, { recursive: true }); - writeFileSync(join(uiDir, "index.html"), "CRP test UI\n"); + writeFileSync(join(uiDir, "index.html"), `CRP test UI\n`); + writeFileSync(join(uiDir, "404.html"), "CRP test UI not found\n"); writeFileSync(join(uiDir, "styles.css"), "body { color: black; }\n"); writeFileSync(join(uiDir, "app.js"), "globalThis.__crpTest = true;\n"); + writeFileSync(join(uiDir, "providers.txt"), "CRP test Flight payload\n"); + writeFileSync(join(uiDir, ".crp-ui-manifest.json"), `${JSON.stringify({ + version: 1, + buildId: "crp-admin-test-build", + assets: { + "/": { file: "index.html", inlineScriptHashes: [TEST_FLIGHT_HASH] }, + "/index.html": { file: "index.html", inlineScriptHashes: [TEST_FLIGHT_HASH] }, + "/styles.css": { file: "styles.css", inlineScriptHashes: [] }, + "/app.js": { file: "app.js", inlineScriptHashes: [] }, + "/providers.txt": { file: "providers.txt", inlineScriptHashes: [] } + }, + notFound: { file: "404.html", inlineScriptHashes: [] } + }, null, 2)}\n`); +} + +async function createHarness(t, overrides = {}) { + const dir = mkdtempSync(join(os.tmpdir(), "crp-admin-server-")); + const uiDir = join(dir, "ui"); + writeAdminUiFixture(uiDir); const controlTokenPath = join(dir, "control-token"); const auth = new SessionAuth({ controlTokenPath }); const services = createServices(); @@ -2584,30 +2605,34 @@ test("returns stable sanitized errors and strict route, method, and path failure assertNoSensitiveResponse(conflict); }); -test("serves only explicit static assets with safe headers and an index fallback", async (t) => { +test("serves only manifest-listed static assets with strict headers and Flight MIME", async (t) => { const harness = await createHarness(t); for (const [path, type, content] of [ ["/", "text/html; charset=utf-8", "CRP test UI"], ["/index.html", "text/html; charset=utf-8", "CRP test UI"], ["/styles.css", "text/css; charset=utf-8", "color: black"], ["/app.js", "text/javascript; charset=utf-8", "__crpTest"], - ["/providers/provider-1", "text/html; charset=utf-8", "CRP test UI"] + ["/providers.txt?_rsc=cache-buster", "text/x-component; charset=utf-8", "Flight payload"] ]) { const result = await harness.request(path); assert.equal(result.response.status, 200); assert.equal(result.response.headers.get("content-type"), type); assert.equal(result.response.headers.get("cache-control"), "no-store"); assert.equal(result.response.headers.get("x-content-type-options"), "nosniff"); + assert.match(result.response.headers.get("content-security-policy"), /script-src 'self'/); + if (path === "/") assert.match(result.response.headers.get("content-security-policy"), new RegExp(TEST_FLIGHT_HASH)); assert.match(result.text, new RegExp(content)); } - const favicon = await harness.request("/favicon.ico"); - assert.equal(favicon.response.status, 204); - assert.equal(favicon.text, ""); - assert.equal(favicon.response.headers.get("cache-control"), "no-store"); - - const missingAsset = await harness.request("/missing.css"); + const missingAsset = await harness.request("/providers/provider-1"); assert.equal(missingAsset.response.status, 404); + assert.match(missingAsset.text, /not found/); + assert.equal(missingAsset.response.headers.get("content-type"), "text/html; charset=utf-8"); + const privateManifest = await harness.request("/.crp-ui-manifest.json"); + assert.equal(privateManifest.response.status, 404); + assert.equal(privateManifest.text.includes("crp-admin-test-build"), false); + const traversal = await harness.request("/providers.txt", { rawPath: "/providers/../providers.txt" }); + assert.equal(traversal.response.status, 404); const postUi = await harness.request("/", { method: "POST" }); assert.equal(postUi.response.status, 405); }); @@ -2633,6 +2658,8 @@ function supervisorDependencies(t, { runtimeConfigPath: join(home, ".codex-remote-proxy", "node", "proxy-config.json"), capturePath: join(home, ".codex-remote-proxy", "traffic.sqlite3") }; + const uiDir = join(home, "ui"); + writeAdminUiFixture(uiDir); const order = []; const worker = { getPublicState: () => workerState(), @@ -2699,6 +2726,7 @@ function supervisorDependencies(t, { const options = { home, paths, + uiDir, pid: 4242, now: () => "2026-07-13T03:00:00.000Z", privateTokenLoader: ({ path }) => { diff --git a/node/test/package-content.test.mjs b/node/test/package-content.test.mjs index 5b58b16..9ae514f 100644 --- a/node/test/package-content.test.mjs +++ b/node/test/package-content.test.mjs @@ -45,14 +45,94 @@ const REVIEWED_PACKAGE_PATHS = new Set([ "src/supervisor/supervisor-client.mjs", "src/supervisor/supervisor-entry.mjs", "src/supervisor/supervisor.mjs", + "src/supervisor/ui-assets.mjs", "src/supervisor/worker-manager.mjs", "src/worker/account-routing-state.mjs", "src/worker/protocol.mjs", "src/worker/runtime-settings.mjs", "src/worker/worker-entry.mjs", - "ui/app.js", + "ui/.crp-ui-manifest.json", + "ui/404.html", + "ui/__next.__PAGE__.txt", + "ui/__next._full.txt", + "ui/__next._tree.txt", + "ui/_next/static/chunks/158-1bfb115fdb023826.js", + "ui/_next/static/chunks/313-2f8466e707e35ec0.js", + "ui/_next/static/chunks/996-a0f483af1b22140d.js", + "ui/_next/static/chunks/app/_global-error/page-72850c6b7154b5cf.js", + "ui/_next/static/chunks/app/_not-found/page-72850c6b7154b5cf.js", + "ui/_next/static/chunks/app/activity/page-d34dd020730b4798.js", + "ui/_next/static/chunks/app/forwarding/page-d34dd020730b4798.js", + "ui/_next/static/chunks/app/global-error-c3441224e90990a1.js", + "ui/_next/static/chunks/app/layout-1e45b7c268af9839.js", + "ui/_next/static/chunks/app/model-mappings/page-d34dd020730b4798.js", + "ui/_next/static/chunks/app/not-found-f9320e13bb9b8591.js", + "ui/_next/static/chunks/app/overview/page-d34dd020730b4798.js", + "ui/_next/static/chunks/app/page-d34dd020730b4798.js", + "ui/_next/static/chunks/app/providers/page-d34dd020730b4798.js", + "ui/_next/static/chunks/app/routing-rules/page-d34dd020730b4798.js", + "ui/_next/static/chunks/app/setup/page-d34dd020730b4798.js", + "ui/_next/static/chunks/app/system/page-d34dd020730b4798.js", + "ui/_next/static/chunks/c7879cf7-33323d49ba6da96a.js", + "ui/_next/static/chunks/framework-16b238f3c63ce812.js", + "ui/_next/static/chunks/main-33617d8be64c197f.js", + "ui/_next/static/chunks/main-app-bae101f4363f8895.js", + "ui/_next/static/chunks/next/dist/client/components/builtin/app-error-72850c6b7154b5cf.js", + "ui/_next/static/chunks/next/dist/client/components/builtin/forbidden-72850c6b7154b5cf.js", + "ui/_next/static/chunks/next/dist/client/components/builtin/unauthorized-72850c6b7154b5cf.js", + "ui/_next/static/chunks/polyfills-42372ed130431b0a.js", + "ui/_next/static/chunks/webpack-595b3be39883ebd5.js", + "ui/_next/static/crp-c000f69fcd8493278df3809ab25b8d42f0687ec4255264dbe4c1f6e6d42c6d1a/_buildManifest.js", + "ui/_next/static/crp-c000f69fcd8493278df3809ab25b8d42f0687ec4255264dbe4c1f6e6d42c6d1a/_ssgManifest.js", + "ui/_next/static/css/5ee22adf52b0619f.css", + "ui/_next/static/css/9e1661e816f29201.css", + "ui/_not-found.html", + "ui/_not-found.txt", + "ui/_not-found/__next._full.txt", + "ui/_not-found/__next._not-found.__PAGE__.txt", + "ui/_not-found/__next._tree.txt", + "ui/activity.html", + "ui/activity.txt", + "ui/activity/__next._full.txt", + "ui/activity/__next._tree.txt", + "ui/activity/__next.activity.__PAGE__.txt", + "ui/forwarding.html", + "ui/forwarding.txt", + "ui/forwarding/__next._full.txt", + "ui/forwarding/__next._tree.txt", + "ui/forwarding/__next.forwarding.__PAGE__.txt", "ui/index.html", - "ui/styles.css" + "ui/index.txt", + "ui/model-mappings.html", + "ui/model-mappings.txt", + "ui/model-mappings/__next._full.txt", + "ui/model-mappings/__next._tree.txt", + "ui/model-mappings/__next.model-mappings.__PAGE__.txt", + "ui/overview.html", + "ui/overview.txt", + "ui/overview/__next._full.txt", + "ui/overview/__next._tree.txt", + "ui/overview/__next.overview.__PAGE__.txt", + "ui/providers.html", + "ui/providers.txt", + "ui/providers/__next._full.txt", + "ui/providers/__next._tree.txt", + "ui/providers/__next.providers.__PAGE__.txt", + "ui/routing-rules.html", + "ui/routing-rules.txt", + "ui/routing-rules/__next._full.txt", + "ui/routing-rules/__next._tree.txt", + "ui/routing-rules/__next.routing-rules.__PAGE__.txt", + "ui/setup.html", + "ui/setup.txt", + "ui/setup/__next._full.txt", + "ui/setup/__next._tree.txt", + "ui/setup/__next.setup.__PAGE__.txt", + "ui/system.html", + "ui/system.txt", + "ui/system/__next._full.txt", + "ui/system/__next._tree.txt", + "ui/system/__next.system.__PAGE__.txt" ]); const FORBIDDEN_TOP_LEVEL_DIRECTORIES = new Set([ ".changeset", @@ -168,7 +248,7 @@ test("npm pack invocation uses an explicit command interpreter only on Windows", test("exact package path comparison detects every extra and missing path", () => { const reviewed = [...REVIEWED_PACKAGE_PATHS]; - assert.equal(REVIEWED_PACKAGE_PATHS.size, 47); + assert.equal(REVIEWED_PACKAGE_PATHS.size, 127); assert.deepEqual(comparePackagePaths(reviewed), { missing: [], unexpected: [] }); const runtimeExtras = [ @@ -188,8 +268,8 @@ test("exact package path comparison detects every extra and missing path", () => ); assert.deepEqual( - comparePackagePaths(reviewed.filter((path) => path !== "ui/app.js")), - { missing: ["ui/app.js"], unexpected: [] } + comparePackagePaths(reviewed.filter((path) => path !== "ui/index.html")), + { missing: ["ui/index.html"], unexpected: [] } ); }); diff --git a/node/test/release-workflows.test.mjs b/node/test/release-workflows.test.mjs index 6e4cd32..ae3b528 100644 --- a/node/test/release-workflows.test.mjs +++ b/node/test/release-workflows.test.mjs @@ -5,6 +5,10 @@ import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { + normalizeLockfileForUiDigest, + normalizeUiSourceBytes +} from "../scripts/build-next-ui.mjs"; import { syncLockfileVersion } from "../scripts/sync-lockfile-version.mjs"; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); @@ -17,17 +21,23 @@ function readWorkflowText(name) { } function extractStepBlock(workflowText, stepName) { + const blocks = extractStepBlocks(workflowText, stepName); + assert.equal(blocks.length, 1, `expected one active step named ${stepName}`); + return blocks[0]; +} + +function extractStepBlocks(workflowText, stepName) { const lines = workflowText.split("\n"); const marker = ` - name: ${stepName}`; const starts = lines .map((line, index) => line === marker ? index : -1) .filter((index) => index !== -1); - assert.equal(starts.length, 1, `expected one active step named ${stepName}`); - const start = starts[0]; - const next = lines.findIndex((line, index) => ( - index > start && line.startsWith(" - name: ") - )); - return lines.slice(start, next === -1 ? lines.length : next).join("\n"); + return starts.map((start, index) => { + const next = starts[index + 1] ?? lines.findIndex((line, lineIndex) => ( + lineIndex > start && line.startsWith(" - name: ") + )); + return lines.slice(start, next === -1 ? lines.length : next).join("\n"); + }); } function extractTopLevelChildKeys(workflowText, sectionName) { @@ -270,6 +280,39 @@ test("package versioning keeps package and lockfile root versions synchronized", ); }); +test("UI build digest ignores version-only lockfile changes", () => { + const lockfile = { + name: packageName, + version: "0.4.21", + lockfileVersion: 3, + packages: { + "": { name: packageName, version: "0.4.21", dependencies: { stable: "1.0.0" } }, + "node_modules/stable": { version: "1.0.0", integrity: "sha512-stable" } + } + }; + const nextVersion = structuredClone(lockfile); + nextVersion.version = "0.4.22"; + nextVersion.packages[""].version = "0.4.22"; + + assert.deepEqual( + normalizeLockfileForUiDigest(lockfile), + normalizeLockfileForUiDigest(nextVersion) + ); + + nextVersion.packages["node_modules/stable"].version = "1.0.1"; + assert.notDeepEqual( + normalizeLockfileForUiDigest(lockfile), + normalizeLockfileForUiDigest(nextVersion) + ); +}); + +test("UI build digest normalizes source line endings", () => { + assert.deepEqual( + normalizeUiSourceBytes(Buffer.from("first\r\nsecond\rthird\n", "utf8")), + Buffer.from("first\nsecond\nthird\n", "utf8") + ); +}); + test("lockfile version sync preserves the dependency graph and is byte-idempotent", (t) => { const directory = mkdtempSync(join(tmpdir(), "crp-lock-version-")); t.after(() => rmSync(directory, { recursive: true, force: true })); @@ -330,12 +373,42 @@ test("lockfile version sync fails before writing mismatched package metadata", ( test("every workflow checkout disables persisted credentials", () => { for (const name of ["release.yml", "release-preflight.yml", "platform-tests.yml"]) { - const checkout = extractStepBlock(readWorkflowText(name), "Checkout"); - assert.match(checkout, /^ uses: actions\/checkout@v4$/m); - assert.match(checkout, /^ persist-credentials: false$/m); + for (const checkout of extractStepBlocks(readWorkflowText(name), "Checkout")) { + assert.match(checkout, /^ uses: actions\/checkout@v4$/m); + assert.match(checkout, /^ persist-credentials: false$/m); + } } }); +test("platform matrix verifies committed UI assets and canonical Linux build uploads output", () => { + const workflow = readWorkflowText("platform-tests.yml"); + const verify = extractStepBlock(workflow, "Verify committed UI assets"); + assert.match(verify, /^ run: npm run verify:ui-assets$/m); + const generate = extractStepBlock(workflow, "Generate canonical UI artifact"); + assert.match(generate, /^ run: npm run build:ui$/m); + const canonicalStart = workflow.indexOf(" canonical-ui:\n"); + assert.notEqual(canonicalStart, -1); + assert.match(workflow.slice(canonicalStart), /^ node-version: 22\.19\.0$/m); + const upload = extractStepBlock(workflow, "Upload canonical UI artifact"); + assert.match(upload, /include-hidden-files: true/); + assert.match(upload, /^ path: node\/ui\/\*\*$/m); +}); + +test("canonical UI verification compares the checkout before candidate generation or publication", () => { + const preflight = readWorkflowText("release-preflight.yml"); + const preflightVerify = extractStepBlock(preflight, "Verify canonical UI checkout"); + assert.match(preflightVerify, /^ run: npm run verify:ui-build$/m); + assert.equal(preflightVerify.includes("npm run build:ui"), false); + assert.match(preflight, /^ node-version: 22\.19\.0$/m); + + const release = readWorkflowText("release.yml"); + const releaseVerify = extractStepBlock(release, "Verify canonical UI checkout"); + assert.match(releaseVerify, /^ run: npm run verify:ui-build$/m); + assert.match(release, /^ release:\n needs: canonical-ui$/m); + assert.match(release, /^ node-version: 22\.19\.0$/m); + assert.match(release, /^ node-version: 24$/m); +}); + test("Linux native smoke proves Secret Service and the default collection before Node", () => { const workflow = readWorkflowText("platform-tests.yml"); const installScript = extractStepBlock(workflow, "Install Linux Secret Service"); diff --git a/node/test/ui-build.test.mjs b/node/test/ui-build.test.mjs new file mode 100644 index 0000000..ffe97b4 --- /dev/null +++ b/node/test/ui-build.test.mjs @@ -0,0 +1,16 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { sourceDigest, verifyCommittedUiAssets } from "../scripts/build-next-ui.mjs"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +test("committed UI assets bind the normalized source digest to the complete security manifest", async () => { + const manifest = JSON.parse(await readFile(join(packageRoot, "ui", ".crp-ui-manifest.json"), "utf8")); + assert.equal(manifest.buildId, await sourceDigest()); + const inspected = await verifyCommittedUiAssets(); + assert.deepEqual(manifest, inspected); +}); diff --git a/node/tsconfig.ui.json b/node/tsconfig.ui.json index 0d3288a..5158820 100644 --- a/node/tsconfig.ui.json +++ b/node/tsconfig.ui.json @@ -11,13 +11,22 @@ "forceConsistentCasingInFileNames": true, "module": "ESNext", "moduleResolution": "Bundler", - "types": ["vite/client"], + "baseUrl": ".", + "paths": { + "@/*": ["ui-src/src/*"] + }, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx", + "jsx": "preserve", + "plugins": [{ "name": "next" }], "noUncheckedIndexedAccess": true, "noFallthroughCasesInSwitch": true }, - "include": ["ui-src/src/**/*.ts", "ui-src/src/**/*.tsx"] + "include": [ + "ui-src/**/*.ts", + "ui-src/**/*.tsx", + "ui-src/next-env.d.ts", + "ui-src/.crp-next-ui-build/types/**/*.ts" + ] } diff --git a/node/ui-src/MIGRATION-CONTRACT.md b/node/ui-src/MIGRATION-CONTRACT.md new file mode 100644 index 0000000..8d9b8e4 --- /dev/null +++ b/node/ui-src/MIGRATION-CONTRACT.md @@ -0,0 +1,57 @@ +# CRP UI migration contract + +This directory is the production Next.js App Router source for the bundled CRP +console. The exported files remain a same-origin static client of the existing +Admin API; production UI code must never use demo data. + +## Runtime and routing + +- `CrpApp` in `src/app.tsx` owns the single `CrpApi`, browser session state, + workspace data, mutation serialization, notices, and terminal states. +- The root layout mounts `CrpApp` once. Navigation uses Next `Link`/router APIs + so the in-memory CSRF token survives route changes. +- A direct visit or hard reload without `#token=...` starts read-only. Cookie + possession alone never restores mutation access; the user must invoke the + existing explicit resume action. +- The supported static routes are `/`, `/overview`, `/providers`, + `/model-mappings`, `/routing-rules`, `/forwarding`, `/activity`, `/system`, + and `/setup`. +- Route pages are thin adapters. Business views under `src/views/` keep their current named + exports and prop contracts while their visual implementation is migrated. + +## Shared UI + +- Pages may continue importing `Button`, `IconButton`, `Panel`, `PanelHeader`, + `PageHeader`, `Notice`, `ErrorNotice`, `StatusBadge`, `Modal`, + `DefinitionList`, `EmptyState`, form fields, and `cx` from + `src/components/Primitives.tsx`. +- New reusable primitives live in `src/components/ui/` and use shadcn source + patterns backed by Base UI. Icons come from Lucide. +- Theme colors, type, spacing, borders, and focus treatment come from + `app/globals.css`; pages must use the shared tokens instead of inventing a + second palette. +- The visual direction is the approved light, compact, green CRP console in the + Overview preview: quiet sidebar, dense operational cards, and restrained + status color. + +## Strict CSP + +- The root client providers include ``. +- Server-rendered markup must contain no `style` attribute, `