commit 91be9ba320b7e77e1d3ab91bd5e7b210aafdd1d3 Author: Kaihua Date: Thu Jul 23 09:31:05 2026 +0800 up diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a35c6c8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + pull_request: + push: + branches-ignore: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm test + - run: npm run typecheck + - run: npm run build diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..e736fb4 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,98 @@ +name: Refresh prices and deploy Pages + +on: + push: + branches: [main] + workflow_dispatch: + schedule: + - cron: "17 3 * * *" + timezone: "Asia/Shanghai" + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: github-pages + cancel-in-progress: true + +jobs: + schedule-gate: + runs-on: ubuntu-latest + outputs: + run: ${{ steps.gate.outputs.run }} + steps: + - id: gate + name: Run pushes and manual dispatches immediately; schedules every two days + shell: bash + run: | + if [[ "${{ github.event_name }}" != "schedule" ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + epoch_day=$(( $(date -u +%s) / 86400 )) + if (( epoch_day % 2 == 0 )); then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + fi + + build: + needs: schedule-gate + if: needs.schedule-gate.outputs.run == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Locate the previous deployed snapshot + shell: bash + run: | + owner="${GITHUB_REPOSITORY%%/*}" + repo="${GITHUB_REPOSITORY##*/}" + if [[ "${repo,,}" == "${owner,,}.github.io" ]]; then + url="https://${owner}.github.io/data/prices.json" + else + url="https://${owner}.github.io/${repo}/data/prices.json" + fi + echo "PREVIOUS_SNAPSHOT_URL=$url" >> "$GITHUB_ENV" + + - name: Collect and validate official monthly prices + env: + MIN_FIRST_RUN_ROWS: 20 + run: npm run collect + + - name: Type-check and build + run: npm run build + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b862e9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +public/data/prices.json +coverage/ +*.local +.DS_Store +npm-debug.log* +*.tsbuildinfo diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a5d20fa --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Business Price Radar contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..99ea5c9 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,11 @@ +# Notices + +## OpenAI and ChatGPT + +OpenAI and ChatGPT are trademarks of their respective owners. This independent project is not affiliated with, endorsed by, or sponsored by OpenAI. Pricing data is retrieved from a publicly reachable configuration endpoint on `chatgpt.com`; availability and schema are not guaranteed. + +## PriceAI + +The idea of presenting regional subscription prices as a searchable comparison table was informed by publicly visible PriceAI documentation and user interface concepts. No PriceAI source code, branding, visual assets, production datasets, channel data, price snapshots, or written guide content are included in this repository. + +PriceAI retains all rights granted or reserved under its own license, data license, and trademark policy. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0102c0 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# Business Price Radar + +一个独立的 ChatGPT Business 全球月付地区价格查询工具。项目从 `chatgpt.com` 的公开结账价格配置中读取 `currency_config.business.month`,保留官方原币价格,并按公开汇率换算为人民币或美元。 + +> 本项目不是 OpenAI 官方产品,与 OpenAI 没有隶属、合作或背书关系。实际价格、税费、付款资格和地区可用性以结账页为准。 + +## 功能 + +- 探测完整的 ISO 3166-1 alpha-2 国家和地区代码。 +- 仅展示标准 ChatGPT Business 月付价格,不包含年付或非营利套餐。 +- 显示官方原币价格、人民币/美元估算、含税或未含税口径。 +- 支持搜索、筛选、排序以及桌面表格和移动端卡片。 +- GitHub Actions 每两天刷新,结构异常或覆盖率骤降时停止部署。 +- 单个地区暂时失败时,最多沿用 14 天的上次成功结果并标记为“数据暂旧”。 + +## 本地开发 + +需要 Node.js 22 或更高版本,推荐 Node.js 24 LTS。 + +```bash +npm install +npm run dev +``` + +本地首次打开会使用 `public/data/sample-prices.json` 演示数据。获取真实数据: + +```bash +npm run collect +npm run dev +``` + +生成的 `public/data/prices.json` 已加入 `.gitignore`,不会提交到仓库。 + +常用检查: + +```bash +npm test +npm run typecheck +npm run build +``` + +## 数据采集规则 + +采集器位于 `scripts/collect-prices.mjs`: + +1. 遍历 249 个 ISO 国家/地区代码。 +2. 请求 `https://chatgpt.com/backend-anon/checkout_pricing_config/configs/{CODE}`。 +3. 校验返回地区、币种和 `business.month.amount/tax`。 +4. 以 Frankfurter 为主要汇率源;未覆盖币种由 `open.er-api.com` 补齐。 +5. 通过美元中间价换算:`USD = 本币金额 ÷ USD→本币汇率`,`CNY = USD × USD→CNY汇率`。 +6. 不自行加税,只展示接口提供的税务口径。 + +采集请求不使用 Cookie、设备 ID、会话 ID、登录凭据或浏览器指纹头。 + +### 发布保护 + +- 首次快照必须包含新鲜的 US、至少一个非 USD 地区、CNY 汇率和至少 20 个有效地区。 +- 后续快照总行数不得低于上一线上快照的 90%。 +- 任何保护条件失败时,Action 直接失败,不会覆盖当前线上版本。 + +可通过环境变量调整: + +- `PREVIOUS_SNAPSHOT_URL`:上一线上快照地址。 +- `PREVIOUS_SNAPSHOT_PATH`:本地上一快照路径。 +- `MIN_FIRST_RUN_ROWS`:首次部署最少行数,默认 `20`。 +- `OUTPUT_PATH`:输出文件,默认 `public/data/prices.json`。 + +## GitHub Pages 发布 + +1. 新建 GitHub 仓库并推送本目录内容,默认分支命名为 `main`。 +2. 打开仓库 **Settings → Pages**。 +3. 在 **Build and deployment → Source** 中选择 **GitHub Actions**。 +4. 打开 **Actions → Refresh prices and deploy Pages → Run workflow**,执行首次手动刷新。 +5. 首次成功后,网站会发布到仓库对应的 GitHub Pages 地址。 + +工作流还会在北京时间每天 03:17 唤醒,通过日期门控每两天真正刷新一次;主分支推送和手动运行会立即刷新。 + +GitHub 可能在公开仓库连续 60 天没有活动后停用定时工作流。本项目按约定不创建保活提交;如被停用,请在 Actions 页面手动重新启用并运行一次。 + +## 计费与免责声明 + +ChatGPT Business 按标准席位计费,通常至少需要两个标准席位。本站表格显示的是接口返回的**每用户月价**,不代表两席位总账单。企业税号、VAT/GST、反向征税、发卡行汇率和手续费都可能改变最终金额。 + +- [OpenAI Business 官方价格页](https://openai.com/business/pricing/) +- [OpenAI Business 账单与席位说明](https://help.openai.com/en/articles/8792536) + +## 授权与归属 + +代码采用 [MIT License](LICENSE)。 + +ChatGPT 与 OpenAI 是其各自权利人的商标。本项目仅作描述性使用。项目受到 PriceAI 信息架构的启发,但没有复制 PriceAI 的代码、品牌、素材、价格快照或线上数据;详见 [NOTICE.md](NOTICE.md)。 diff --git a/index.html b/index.html new file mode 100644 index 0000000..31a022b --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + Business Price Radar|全球月付价格 + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fdb47b3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3143 @@ +{ + "name": "chatgpt-business-price-radar", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chatgpt-business-price-radar", + "version": "1.0.0", + "dependencies": { + "lucide-react": "^0.468.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.0", + "@types/node": "^24.0.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.0", + "jsdom": "^27.0.0", + "typescript": "^5.9.0", + "vite": "^7.0.0", + "vitest": "^4.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "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/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "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.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.10.0.tgz", + "integrity": "sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "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": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", + "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.395", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", + "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "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" + } + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/typescript": { + "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", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9685085 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "chatgpt-business-price-radar", + "private": true, + "version": "1.0.0", + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "dev": "vite", + "collect": "node scripts/collect-prices.mjs", + "build": "tsc -b && vite build", + "typecheck": "tsc -b --pretty false", + "test": "vitest run", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^0.468.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.0", + "@types/node": "^24.0.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.0", + "jsdom": "^27.0.0", + "typescript": "^5.9.0", + "vite": "^7.0.0", + "vitest": "^4.0.0" + } +} diff --git a/public/.nojekyll b/public/.nojekyll new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/public/.nojekyll @@ -0,0 +1 @@ + diff --git a/public/data/sample-prices.json b/public/data/sample-prices.json new file mode 100644 index 0000000..9784b28 --- /dev/null +++ b/public/data/sample-prices.json @@ -0,0 +1,129 @@ +{ + "version": 1, + "mode": "sample", + "generatedAt": "2026-07-22T15:00:00.000Z", + "pricingSource": { + "name": "OpenAI ChatGPT checkout pricing config", + "urlTemplate": "https://chatgpt.com/backend-anon/checkout_pricing_config/configs/{country_code}" + }, + "fx": { + "baseCurrency": "USD", + "date": "2026-06-29", + "source": "示例汇率", + "sourceUrls": [], + "rates": { + "USD": 1, + "CNY": 6.803731, + "SGD": 1.294141, + "INR": 94.490194, + "JPY": 161.766947, + "EUR": 0.878247, + "BRL": 5.173169 + } + }, + "coverage": { + "requested": 249, + "success": 6, + "unsupported": 0, + "failed": 0, + "stale": 0 + }, + "rows": [ + { + "countryCode": "JP", + "countryName": "日本", + "currencyCode": "JPY", + "symbol": "¥", + "minorUnitExponent": 0, + "localAmount": 3850, + "usdAmount": 23.8, + "cnyAmount": 161.93, + "taxTreatment": "exclusive", + "taxType": "jct", + "taxPercent": 10, + "status": "fresh", + "fetchedAt": "2026-07-22T15:00:00.000Z", + "sourceUrl": "https://chatgpt.com/backend-anon/checkout_pricing_config/configs/JP" + }, + { + "countryCode": "IN", + "countryName": "印度", + "currencyCode": "INR", + "symbol": "₹", + "minorUnitExponent": 2, + "localAmount": 2250, + "usdAmount": 23.81, + "cnyAmount": 162.01, + "taxTreatment": "exclusive", + "taxType": "gst", + "taxPercent": 18, + "status": "fresh", + "fetchedAt": "2026-07-22T15:00:00.000Z", + "sourceUrl": "https://chatgpt.com/backend-anon/checkout_pricing_config/configs/IN" + }, + { + "countryCode": "SG", + "countryName": "新加坡", + "currencyCode": "SGD", + "symbol": "S$", + "minorUnitExponent": 2, + "localAmount": 32, + "usdAmount": 24.73, + "cnyAmount": 168.23, + "taxTreatment": "exclusive", + "taxType": "gst", + "taxPercent": 9, + "status": "fresh", + "fetchedAt": "2026-07-22T15:00:00.000Z", + "sourceUrl": "https://chatgpt.com/backend-anon/checkout_pricing_config/configs/SG" + }, + { + "countryCode": "US", + "countryName": "美国", + "currencyCode": "USD", + "symbol": "$", + "minorUnitExponent": 2, + "localAmount": 25, + "usdAmount": 25, + "cnyAmount": 170.09, + "taxTreatment": "exclusive", + "taxType": "other", + "taxPercent": null, + "status": "fresh", + "fetchedAt": "2026-07-22T15:00:00.000Z", + "sourceUrl": "https://chatgpt.com/backend-anon/checkout_pricing_config/configs/US" + }, + { + "countryCode": "BR", + "countryName": "巴西", + "currencyCode": "BRL", + "symbol": "R$", + "minorUnitExponent": 2, + "localAmount": 130, + "usdAmount": 25.13, + "cnyAmount": 170.98, + "taxTreatment": "exclusive", + "taxType": "vat", + "taxPercent": 0, + "status": "fresh", + "fetchedAt": "2026-07-22T15:00:00.000Z", + "sourceUrl": "https://chatgpt.com/backend-anon/checkout_pricing_config/configs/BR" + }, + { + "countryCode": "DE", + "countryName": "德国", + "currencyCode": "EUR", + "symbol": "€", + "minorUnitExponent": 2, + "localAmount": 26, + "usdAmount": 29.6, + "cnyAmount": 201.42, + "taxTreatment": "inclusive", + "taxType": "vat", + "taxPercent": 19, + "status": "fresh", + "fetchedAt": "2026-07-22T15:00:00.000Z", + "sourceUrl": "https://chatgpt.com/backend-anon/checkout_pricing_config/configs/DE" + } + ] +} diff --git a/scripts/collect-prices.mjs b/scripts/collect-prices.mjs new file mode 100644 index 0000000..9ea3251 --- /dev/null +++ b/scripts/collect-prices.mjs @@ -0,0 +1,365 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { z } from "zod"; + +export const ISO_COUNTRY_CODES = ` +AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ +BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ +CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ +DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR +GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY +HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP +KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY +MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ +NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY +QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ +TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ +VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW +`.trim().split(/\s+/).sort(); + +export const PRICING_URL_TEMPLATE = "https://chatgpt.com/backend-anon/checkout_pricing_config/configs/{country_code}"; +const FRANKFURTER_URL = "https://api.frankfurter.dev/v1/latest"; +const OPEN_EXCHANGE_URL = "https://open.er-api.com/v6/latest/USD"; +const DEFAULT_TIMEOUT_MS = 12_000; +const DEFAULT_RETRIES = 3; +const DEFAULT_CONCURRENCY = 6; +const STALE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000; + +const upstreamSchema = z.object({ + country_code: z.string().length(2), + currency_config: z.object({ + business: z.object({ + month: z.object({ + amount: z.number().positive().finite(), + tax: z.enum(["inclusive", "exclusive"]), + }), + }), + }), + symbol_code: z.string().length(3), + symbol: z.string().min(1), + minor_unit_exponent: z.number().int().min(0).max(4).optional().default(2), + tax_type: z.string().min(1).nullable().optional(), + tax_percent: z.number().finite().nullable().optional(), +}).passthrough(); + +const snapshotSchema = z.object({ + version: z.literal(1), + generatedAt: z.string(), + rows: z.array(z.object({ + countryCode: z.string().length(2), + countryName: z.string(), + currencyCode: z.string().length(3), + symbol: z.string(), + minorUnitExponent: z.number().int(), + localAmount: z.number(), + usdAmount: z.number(), + cnyAmount: z.number(), + taxTreatment: z.enum(["inclusive", "exclusive"]), + taxType: z.string().nullable(), + taxPercent: z.number().nullable(), + status: z.enum(["fresh", "stale"]), + fetchedAt: z.string(), + sourceUrl: z.string().url(), + })), +}).passthrough(); + +const countryNames = new Intl.DisplayNames(["zh-CN"], { type: "region" }); + +export function sourceUrl(countryCode) { + return PRICING_URL_TEMPLATE.replace("{country_code}", countryCode.toUpperCase()); +} + +export function parsePriceResponse(raw, requestedCode, fetchedAt = new Date().toISOString()) { + const data = upstreamSchema.parse(raw); + const countryCode = requestedCode.toUpperCase(); + if (data.country_code.toUpperCase() !== countryCode) { + throw new Error(`Country mismatch: requested ${countryCode}, received ${data.country_code}`); + } + + return { + countryCode, + countryName: countryNames.of(countryCode) || countryCode, + currencyCode: data.symbol_code.toUpperCase(), + symbol: data.symbol, + minorUnitExponent: data.minor_unit_exponent, + localAmount: data.currency_config.business.month.amount, + taxTreatment: data.currency_config.business.month.tax, + taxType: data.tax_type ?? null, + taxPercent: data.tax_percent ?? null, + status: "fresh", + fetchedAt, + sourceUrl: sourceUrl(countryCode), + }; +} + +export function convertAmount(localAmount, currencyCode, rates) { + const localRate = currencyCode === "USD" ? 1 : Number(rates[currencyCode]); + const cnyRate = Number(rates.CNY); + if (!Number.isFinite(localRate) || localRate <= 0) throw new Error(`Missing FX rate for ${currencyCode}`); + if (!Number.isFinite(cnyRate) || cnyRate <= 0) throw new Error("Missing FX rate for CNY"); + const rawUsd = localAmount / localRate; + return { + usdAmount: roundMoney(rawUsd), + cnyAmount: roundMoney(rawUsd * cnyRate), + }; +} + +export function withConvertedAmounts(row, rates) { + return { ...row, ...convertAmount(row.localAmount, row.currencyCode, rates) }; +} + +export function mergeStaleRows(freshRows, failedCodes, previousRows, rates, now = new Date()) { + const freshCodes = new Set(freshRows.map((row) => row.countryCode)); + const failed = new Set(failedCodes); + const staleRows = previousRows + .filter((row) => failed.has(row.countryCode) && !freshCodes.has(row.countryCode)) + .filter((row) => now.getTime() - new Date(row.fetchedAt).getTime() <= STALE_MAX_AGE_MS) + .map((row) => withConvertedAmounts({ ...row, status: "stale" }, rates)); + return [...freshRows, ...staleRows]; +} + +export function validateCoverage(snapshot, previousSnapshot = null, minimumFirstRunRows = 20) { + const freshUs = snapshot.rows.find((row) => row.countryCode === "US" && row.status === "fresh"); + if (!freshUs) throw new Error("Coverage guard failed: a fresh US Business price is required."); + const freshNonUsd = snapshot.rows.find((row) => row.currencyCode !== "USD" && row.status === "fresh"); + if (!freshNonUsd) throw new Error("Coverage guard failed: a fresh non-USD Business price is required."); + if (!Number.isFinite(Number(snapshot.fx.rates.CNY))) throw new Error("Coverage guard failed: CNY FX rate is required."); + + if (previousSnapshot?.rows?.length) { + const minimum = Math.ceil(previousSnapshot.rows.length * 0.9); + if (snapshot.rows.length < minimum) { + throw new Error(`Coverage guard failed: ${snapshot.rows.length} rows is below 90% of previous ${previousSnapshot.rows.length}.`); + } + } else if (snapshot.rows.length < minimumFirstRunRows) { + throw new Error(`Coverage guard failed: first live snapshot requires at least ${minimumFirstRunRows} rows.`); + } +} + +async function fetchWithTimeout(url, options = {}) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS); + try { + return await fetch(url, { + headers: { + accept: "application/json", + "user-agent": "BusinessPriceRadar/1.0 (public pricing reference)", + ...options.headers, + }, + signal: controller.signal, + }); + } finally { + clearTimeout(timer); + } +} + +export async function fetchJsonWithRetry(url, options = {}) { + const retries = options.retries ?? DEFAULT_RETRIES; + let lastError; + for (let attempt = 0; attempt < retries; attempt += 1) { + try { + const response = await fetchWithTimeout(url, options); + if (response.status === 404) return { kind: "unsupported", status: 404 }; + if (!response.ok) { + const error = new Error(`HTTP ${response.status} for ${url}`); + if (response.status < 500 && response.status !== 429) throw Object.assign(error, { nonRetryable: true }); + throw error; + } + return { kind: "success", data: await response.json(), status: response.status }; + } catch (error) { + lastError = error; + if (error?.nonRetryable || attempt === retries - 1) break; + await sleep((attempt + 1) * 850 + Math.floor(Math.random() * 250)); + } + } + throw lastError; +} + +async function collectCountry(countryCode, fetchedAt) { + const result = await fetchJsonWithRetry(sourceUrl(countryCode)); + if (result.kind === "unsupported") return { kind: "unsupported", countryCode }; + return { kind: "success", countryCode, row: parsePriceResponse(result.data, countryCode, fetchedAt) }; +} + +async function collectAllCountries(countryCodes, fetchedAt, concurrency = DEFAULT_CONCURRENCY) { + const results = await mapLimit(countryCodes, concurrency, async (countryCode) => { + try { + return await collectCountry(countryCode, fetchedAt); + } catch (error) { + return { + kind: "failed", + countryCode, + error: error instanceof Error ? error.message : String(error), + }; + } + }); + + return { + rows: results.filter((item) => item.kind === "success").map((item) => item.row), + unsupportedCodes: results.filter((item) => item.kind === "unsupported").map((item) => item.countryCode), + failedItems: results.filter((item) => item.kind === "failed"), + }; +} + +export async function fetchFxSnapshot(currencies) { + const wanted = [...new Set(["CNY", ...currencies.filter((item) => item !== "USD")])].sort(); + const rates = { USD: 1 }; + const sourceUrls = []; + let primaryDate = null; + let primaryWorked = false; + + const primaryUrl = `${FRANKFURTER_URL}?base=USD&symbols=${encodeURIComponent(wanted.join(","))}`; + sourceUrls.push(primaryUrl); + try { + const result = await fetchJsonWithRetry(primaryUrl, { retries: 2 }); + if (result.kind === "success") { + Object.assign(rates, numericRates(result.data.rates)); + primaryDate = result.data.date || null; + primaryWorked = true; + } + } catch (error) { + console.warn(`Frankfurter unavailable: ${error instanceof Error ? error.message : error}`); + } + + const missing = wanted.filter((currency) => !Number.isFinite(Number(rates[currency]))); + let fallbackDate = null; + if (missing.length) { + sourceUrls.push(OPEN_EXCHANGE_URL); + const fallback = await fetchJsonWithRetry(OPEN_EXCHANGE_URL); + if (fallback.kind !== "success") throw new Error("FX fallback unexpectedly returned no data."); + const fallbackRates = numericRates(fallback.data.rates); + for (const currency of missing) { + if (Number.isFinite(fallbackRates[currency])) rates[currency] = fallbackRates[currency]; + } + fallbackDate = dateFromOpenExchange(fallback.data.time_last_update_utc); + } + + const stillMissing = wanted.filter((currency) => !Number.isFinite(Number(rates[currency]))); + if (stillMissing.length) throw new Error(`Missing FX rates for ${stillMissing.join(", ")}`); + + return { + baseCurrency: "USD", + date: primaryDate || fallbackDate || new Date().toISOString().slice(0, 10), + source: missing.length ? (primaryWorked ? "Frankfurter + open.er-api.com" : "open.er-api.com") : "Frankfurter", + sourceUrls, + rates, + }; +} + +function numericRates(value) { + if (!value || typeof value !== "object") return {}; + return Object.fromEntries( + Object.entries(value) + .map(([currency, rate]) => [currency, Number(rate)]) + .filter(([, rate]) => Number.isFinite(rate) && rate > 0), + ); +} + +function dateFromOpenExchange(value) { + const date = new Date(value || ""); + return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10); +} + +async function readPreviousSnapshot(urlOrPath) { + if (!urlOrPath) return null; + try { + let raw; + if (/^https?:\/\//i.test(urlOrPath)) { + const result = await fetchJsonWithRetry(urlOrPath, { retries: 1, timeoutMs: 8_000 }); + if (result.kind !== "success") return null; + raw = result.data; + } else { + raw = JSON.parse(await readFile(urlOrPath, "utf8")); + } + return snapshotSchema.parse(raw); + } catch (error) { + console.warn(`Previous snapshot unavailable: ${error instanceof Error ? error.message : error}`); + return null; + } +} + +export async function collectSnapshot(options = {}) { + const generatedAt = options.generatedAt || new Date().toISOString(); + const countryCodes = options.countryCodes || ISO_COUNTRY_CODES; + const previousSnapshot = options.previousSnapshot || null; + const collected = await collectAllCountries(countryCodes, generatedAt, options.concurrency); + const currencies = collected.rows.map((row) => row.currencyCode); + for (const failed of collected.failedItems) { + const previous = previousSnapshot?.rows?.find((row) => row.countryCode === failed.countryCode); + if (previous) currencies.push(previous.currencyCode); + } + const fx = await fetchFxSnapshot(currencies); + const freshRows = collected.rows.map((row) => withConvertedAmounts(row, fx.rates)); + const rows = mergeStaleRows( + freshRows, + collected.failedItems.map((item) => item.countryCode), + previousSnapshot?.rows || [], + fx.rates, + new Date(generatedAt), + ).sort((a, b) => a.cnyAmount - b.cnyAmount); + + return { + version: 1, + mode: "live", + generatedAt, + pricingSource: { + name: "OpenAI ChatGPT checkout pricing config", + urlTemplate: PRICING_URL_TEMPLATE, + }, + fx, + coverage: { + requested: countryCodes.length, + success: freshRows.length, + unsupported: collected.unsupportedCodes.length, + failed: collected.failedItems.length, + stale: rows.filter((row) => row.status === "stale").length, + }, + rows, + }; +} + +async function runCli() { + const outputPath = cliValue("--output") || process.env.OUTPUT_PATH || "public/data/prices.json"; + const previousSource = process.env.PREVIOUS_SNAPSHOT_URL || process.env.PREVIOUS_SNAPSHOT_PATH || null; + const previousSnapshot = await readPreviousSnapshot(previousSource); + const snapshot = await collectSnapshot({ previousSnapshot }); + validateCoverage(snapshot, previousSnapshot, Number(process.env.MIN_FIRST_RUN_ROWS || 20)); + await mkdir(path.dirname(outputPath), { recursive: true }); + await writeFile(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8"); + console.log(JSON.stringify({ outputPath, generatedAt: snapshot.generatedAt, coverage: snapshot.coverage, fx: snapshot.fx.source }, null, 2)); +} + +function cliValue(name) { + const index = process.argv.indexOf(name); + return index >= 0 ? process.argv[index + 1] : null; +} + +async function mapLimit(items, limit, mapper) { + const output = new Array(items.length); + let next = 0; + async function worker() { + while (next < items.length) { + const index = next; + next += 1; + output[index] = await mapper(items[index], index); + } + } + await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker())); + return output; +} + +function roundMoney(value) { + return Math.round((value + Number.EPSILON) * 100) / 100; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +const isMain = process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href; +if (isMain) { + runCli().catch((error) => { + console.error(error instanceof Error ? error.stack || error.message : error); + process.exitCode = 1; + }); +} diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 0000000..5c022f0 --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,45 @@ +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import sample from "../public/data/sample-prices.json"; +import App from "./App"; + +describe("pricing explorer", () => { + beforeEach(() => { + window.localStorage.clear(); + vi.stubGlobal("fetch", vi.fn() + .mockResolvedValueOnce({ ok: false, status: 404 }) + .mockResolvedValueOnce({ ok: true, json: async () => sample })); + }); + + afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + }); + + it("loads the snapshot, preserves local prices and switches display currency locally", async () => { + const user = userEvent.setup(); + render(); + + expect((await screen.findAllByText("S$32.00")).length).toBeGreaterThan(0); + expect(screen.getByRole("button", { name: "$ USD" })).toHaveAttribute("aria-pressed", "false"); + await user.click(screen.getByRole("button", { name: "$ USD" })); + expect(screen.getByRole("button", { name: "$ USD" })).toHaveAttribute("aria-pressed", "true"); + expect((await screen.findAllByText("$23.80")).length).toBeGreaterThan(0); + expect((screen.getAllByText("S$32.00")).length).toBeGreaterThan(0); + expect(fetch).toHaveBeenCalledTimes(2); + }); + + it("filters by country and tax treatment", async () => { + const user = userEvent.setup(); + render(); + await screen.findAllByText("美国"); + await user.type(screen.getByPlaceholderText("搜索国家、代码或币种"), "新加坡"); + await waitFor(() => expect(screen.queryByText("德国")).not.toBeInTheDocument()); + expect(screen.getAllByText("新加坡").length).toBeGreaterThan(0); + await user.clear(screen.getByPlaceholderText("搜索国家、代码或币种")); + await user.selectOptions(screen.getByLabelText("税费"), "inclusive"); + expect(screen.getAllByText("德国").length).toBeGreaterThan(0); + expect(screen.queryByText("新加坡")).not.toBeInTheDocument(); + }); +}); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..2d4dca0 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,420 @@ +import { + ArrowDownUp, + ArrowUpRight, + CalendarClock, + CheckCircle2, + CircleDollarSign, + Database, + Info, + Search, + ShieldCheck, + Users, +} from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { filterAndSortRows, flagEmoji, formatConverted, formatLocal, relativeTime, taxLabel } from "./lib"; +import type { + DisplayCurrency, + PriceRow, + PricingSnapshot, + SortMode, + StatusFilter, + TaxFilter, +} from "./types"; + +const currencyStorageKey = "business-price-radar:currency"; + +async function loadSnapshot(): Promise { + const liveUrl = new URL("data/prices.json", document.baseURI); + const sampleUrl = new URL("data/sample-prices.json", document.baseURI); + + const liveResponse = await fetch(liveUrl, { cache: "no-store" }); + if (liveResponse.ok) { + const contentType = liveResponse.headers?.get?.("content-type") || "application/json"; + if (contentType.includes("json")) { + try { + return await liveResponse.json() as PricingSnapshot; + } catch { + // Vite and some static hosts may return index.html for a missing JSON file. + } + } + } + + const sampleResponse = await fetch(sampleUrl, { cache: "no-store" }); + if (!sampleResponse.ok) throw new Error("价格快照暂时不可用"); + return sampleResponse.json() as Promise; +} + +function initialCurrency(): DisplayCurrency { + const stored = window.localStorage.getItem(currencyStorageKey); + return stored === "USD" ? "USD" : "CNY"; +} + +export default function App() { + const [snapshot, setSnapshot] = useState(null); + const [loadingError, setLoadingError] = useState(null); + const [currency, setCurrency] = useState(initialCurrency); + const [query, setQuery] = useState(""); + const [tax, setTax] = useState("all"); + const [status, setStatus] = useState("all"); + const [sort, setSort] = useState("converted"); + + useEffect(() => { + let active = true; + loadSnapshot() + .then((data) => { + if (active) setSnapshot(data); + }) + .catch((error: unknown) => { + if (active) setLoadingError(error instanceof Error ? error.message : "价格快照暂时不可用"); + }); + return () => { + active = false; + }; + }, []); + + useEffect(() => { + window.localStorage.setItem(currencyStorageKey, currency); + }, [currency]); + + const rows = useMemo( + () => + filterAndSortRows(snapshot?.rows ?? [], { + query, + tax, + status, + sort, + currency, + }), + [snapshot, query, tax, status, sort, currency], + ); + + const lowest = useMemo(() => { + if (!snapshot?.rows.length) return null; + return [...snapshot.rows].sort((a, b) => { + const aValue = currency === "CNY" ? a.cnyAmount : a.usdAmount; + const bValue = currency === "CNY" ? b.cnyAmount : b.usdAmount; + return aValue - bValue; + })[0]; + }, [snapshot, currency]); + + const setDisplayCurrency = (value: DisplayCurrency) => { + setCurrency(value); + window.localStorage.setItem(currencyStorageKey, value); + }; + + return ( +
+
+ + +
+ + OpenAI 公开配置 + + +
+
+ +
+
+
+ CHATGPT BUSINESS · MONTHLY +

一眼看懂,
全球月付差多少。

+

+ 汇总不同国家和地区的 ChatGPT Business 官方月付配置,保留原币价格,统一换算成人民币或美元。 +

+
+ 每用户 / 月 + 原价税务口径 + 每两天更新 +
+
+ +
+
+ 当前最低月付 + 全球 #1 +
+ {lowest ? ( + <> +
+ {flagEmoji(lowest.countryCode)} +
+ {lowest.countryName} + {lowest.countryCode} · {lowest.currencyCode} +
+
+
+ {formatConverted(currency === "CNY" ? lowest.cnyAmount : lowest.usdAmount, currency)} + / 用户 / 月 +
+
+ 官方原价 {formatLocal(lowest)} +
+ + ) : ( +
+ )} +
+ 价格按接口返回口径比较,未自行加税 +
+
+
+ + {snapshot?.mode === "sample" ? ( +
+ 当前为本地演示快照;GitHub Actions 首次成功部署后将自动替换为全量实时数据。 +
+ ) : null} + + {loadingError ? ( +
+ +
暂时无法读取价格快照{loadingError}
+
+ ) : null} + +
+ } + label="有效地区" + value={snapshot ? String(snapshot.coverage.success + snapshot.coverage.stale) : "—"} + detail={snapshot ? `探测 ${snapshot.coverage.requested} 个 ISO 地区` : "正在读取快照"} + /> + } + label="显示币种" + value={currency} + detail={currency === "CNY" ? "按美元中间价折算人民币" : "统一折算为美元"} + /> + } + label="数据更新时间" + value={snapshot ? relativeTime(snapshot.generatedAt) : "—"} + detail={snapshot ? `汇率日期 ${snapshot.fx.date}` : "等待数据"} + /> + } + label="暂旧数据" + value={snapshot ? String(snapshot.coverage.stale) : "—"} + detail="最多沿用 14 天" + /> +
+ +
+
+
+ REGIONAL PRICING +

地区月付价格

+
+

共显示 {rows.length} 个地区

+
+ +
+ + + + +
+ + {!snapshot && !loadingError ? : null} + {snapshot && rows.length ? ( + <> + + + + ) : null} + {snapshot && !rows.length ? ( +
没有符合当前筛选条件的地区
+ ) : null} +
+ +
+
+ 01 +

价格从哪里来?

+

报价来自 chatgpt.com 的公开结账价格配置接口。本站只读取 Business 月付字段,不参与购买。

+
+
+ 02 +

为什么金额会不同?

+

OpenAI 会按地区设置本地币种和价格。汇率换算只用于横向比较,不等于发卡行最终结算价。

+
+
+ 03 +

税费如何处理?

+

不自行加税,直接展示接口的含税或未含税标记。企业税号、VAT 与反向征税可能改变账单。

+
+
+ +
+ +

+ 独立信息工具,非 OpenAI 官方产品。 + 实际价格、税费、付款资格与地区可用性以结账页为准。ChatGPT Business 按用户计费,标准席位至少购买 2 个。 + 查看官方账单说明 +

+
+
+ +
+
+
+

ChatGPT 与 OpenAI 为其各自权利人的商标。本站与 OpenAI 无隶属或背书关系。

+ MIT License · Data refreshed by GitHub Actions +
+
+ ); +} + +function CurrencyToggle({ value, onChange }: { value: DisplayCurrency; onChange: (value: DisplayCurrency) => void }) { + return ( +
+ {(["CNY", "USD"] as const).map((currency) => ( + + ))} +
+ ); +} + +function Metric({ icon, label, value, detail }: { icon: React.ReactNode; label: string; value: string; detail: string }) { + return ( +
+
{icon}
+ {label} + {value} + {detail} +
+ ); +} + +function ConvertedPrice({ row, currency }: { row: PriceRow; currency: DisplayCurrency }) { + const amount = currency === "CNY" ? row.cnyAmount : row.usdAmount; + return <>{formatConverted(amount, currency)}; +} + +function StatusBadge({ status }: { status: PriceRow["status"] }) { + return status === "fresh" ? ( + 最新 + ) : ( + 数据暂旧 + ); +} + +function PriceTable({ rows, currency }: { rows: PriceRow[]; currency: DisplayCurrency }) { + return ( +
+ + + + + + + + + + + + + {rows.map((row, index) => ( + + + + + + + + + ))} + +
地区官方原币月价折合 {currency}税费口径更新时间来源
+
+ {String(index + 1).padStart(2, "0")} + {flagEmoji(row.countryCode)} +
{row.countryName}{row.countryCode} · {row.currencyCode}
+
+
{formatLocal(row)}/ 用户 / 月{taxLabel(row)}{relativeTime(row.fetchedAt)} + + + +
+
+ ); +} + +function PriceCards({ rows, currency }: { rows: PriceRow[]; currency: DisplayCurrency }) { + return ( +
+ {rows.map((row, index) => ( +
+
+ #{index + 1} + +
+
+ {flagEmoji(row.countryCode)} +
{row.countryName}{row.countryCode} · {row.currencyCode}
+
+
/ 用户 / 月
+
+ 官方原价 {formatLocal(row)} + {taxLabel(row)} +
+ 查看官方价格源 +
+ ))} +
+ ); +} + +function LoadingRows() { + return ( +
+ {Array.from({ length: 5 }, (_, index) => )} +
+ ); +} diff --git a/src/lib.ts b/src/lib.ts new file mode 100644 index 0000000..5d29b77 --- /dev/null +++ b/src/lib.ts @@ -0,0 +1,80 @@ +import type { + DisplayCurrency, + PriceRow, + SortMode, + StatusFilter, + TaxFilter, +} from "./types"; + +export function formatConverted(amount: number, currency: DisplayCurrency): string { + const value = new Intl.NumberFormat("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); + return `${currency === "CNY" ? "¥" : "$"}${value}`; +} + +export function formatLocal(row: PriceRow): string { + const value = new Intl.NumberFormat("en-US", { + minimumFractionDigits: row.minorUnitExponent, + maximumFractionDigits: row.minorUnitExponent, + }).format(row.localAmount); + return `${row.symbol}${value}`; +} + +export function flagEmoji(countryCode: string): string { + return [...countryCode.toUpperCase()] + .map((char) => String.fromCodePoint(127397 + char.charCodeAt(0))) + .join(""); +} + +export function relativeTime(iso: string): string { + const delta = Date.now() - new Date(iso).getTime(); + if (!Number.isFinite(delta) || delta < 0) return "刚刚"; + const minutes = Math.floor(delta / 60_000); + if (minutes < 1) return "刚刚"; + if (minutes < 60) return `${minutes} 分钟前`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours} 小时前`; + const days = Math.floor(hours / 24); + return `${days} 天前`; +} + +export function taxLabel(row: PriceRow): string { + const treatment = row.taxTreatment === "inclusive" ? "含税" : "未含税"; + const detail = [row.taxType?.toUpperCase(), row.taxPercent == null ? null : `${row.taxPercent}%`] + .filter(Boolean) + .join(" · "); + return detail ? `${treatment} · ${detail}` : treatment; +} + +export function filterAndSortRows( + rows: PriceRow[], + options: { + query: string; + tax: TaxFilter; + status: StatusFilter; + sort: SortMode; + currency: DisplayCurrency; + }, +): PriceRow[] { + const query = options.query.trim().toLocaleLowerCase("zh-CN"); + const filtered = rows.filter((row) => { + if (options.tax !== "all" && row.taxTreatment !== options.tax) return false; + if (options.status !== "all" && row.status !== options.status) return false; + if (!query) return true; + return [row.countryName, row.countryCode, row.currencyCode] + .join(" ") + .toLocaleLowerCase("zh-CN") + .includes(query); + }); + + return filtered.sort((a, b) => { + if (options.sort === "country") return a.countryName.localeCompare(b.countryName, "zh-CN"); + if (options.sort === "local") return a.localAmount - b.localAmount; + if (options.sort === "updated") return b.fetchedAt.localeCompare(a.fetchedAt); + const aValue = options.currency === "CNY" ? a.cnyAmount : a.usdAmount; + const bValue = options.currency === "CNY" ? b.cnyAmount : b.usdAmount; + return aValue - bValue; + }); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..693ac2e --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./styles.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..00af988 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,204 @@ +:root { + font-family: Inter, "SF Pro Display", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; + color: #17201d; + background: #f5f7f3; + font-synthesis: none; + text-rendering: optimizeLegibility; + --ink: #17201d; + --muted: #65706c; + --line: #dfe5df; + --surface: #ffffff; + --soft: #eef3ed; + --green: #176b4d; + --green-dark: #0f4e39; + --lime: #d8f28d; + --amber: #9a6415; +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { margin: 0; min-width: 320px; min-height: 100vh; background: #f5f7f3; } +button, input, select { font: inherit; } +a { color: inherit; } +button:focus-visible, input:focus-visible, select:focus-visible, a:focus-visible { outline: 3px solid rgba(23, 107, 77, .28); outline-offset: 2px; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } +.app-shell { min-height: 100vh; overflow: hidden; } +.topbar { width: min(1440px, calc(100% - 64px)); height: 88px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); } +.brand { display: inline-flex; align-items: center; gap: 12px; text-decoration: none; } +.brand > span:last-child { display: flex; flex-direction: column; gap: 2px; } +.brand strong { font-size: 15px; letter-spacing: -.01em; } +.brand small { color: var(--muted); font-size: 10px; letter-spacing: .13em; text-transform: uppercase; } +.brand-mark { width: 38px; height: 38px; border-radius: 50%; background: var(--green); display: grid; place-items: center; box-shadow: inset 0 0 0 7px rgba(255,255,255,.09); } +.brand-mark::before { content: ""; width: 18px; height: 18px; border: 1px solid rgba(255,255,255,.75); border-radius: 50%; } +.brand-mark span { position: absolute; width: 5px; height: 5px; background: var(--lime); border-radius: 50%; transform: translate(5px, -5px); } +.topbar-actions { display: flex; align-items: center; gap: 18px; } +.source-pill { display: inline-flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12px; font-weight: 600; } +.live-dot { width: 7px; height: 7px; border-radius: 50%; background: #2ca675; box-shadow: 0 0 0 4px rgba(44,166,117,.12); } +.currency-toggle { display: inline-flex; padding: 4px; border: 1px solid var(--line); border-radius: 12px; background: rgba(255,255,255,.75); } +.currency-toggle button { border: 0; background: transparent; color: var(--muted); padding: 8px 11px; border-radius: 8px; font-size: 12px; font-weight: 750; cursor: pointer; } +.currency-toggle button.active { background: var(--ink); color: white; box-shadow: 0 3px 10px rgba(20,30,27,.14); } +main { width: min(1440px, calc(100% - 64px)); margin: 0 auto; } +.hero { min-height: 570px; display: grid; grid-template-columns: 1.2fr .8fr; gap: 80px; align-items: center; position: relative; } +.hero::before { content: ""; position: absolute; width: 540px; height: 540px; right: -210px; top: -110px; border: 1px solid rgba(23,107,77,.08); border-radius: 50%; box-shadow: 0 0 0 90px rgba(23,107,77,.025), 0 0 0 180px rgba(23,107,77,.018); pointer-events: none; } +.hero-copy { max-width: 780px; position: relative; z-index: 1; } +.eyebrow, .section-kicker { color: var(--green); font-size: 11px; line-height: 1; font-weight: 800; letter-spacing: .18em; } +.eyebrow { display: inline-flex; align-items: center; gap: 7px; border: 1px solid #cbdacf; background: rgba(255,255,255,.58); padding: 9px 12px; border-radius: 999px; } +.hero h1 { margin: 26px 0 20px; font-family: Georgia, "Noto Serif SC", serif; font-size: clamp(52px, 6vw, 88px); line-height: .98; letter-spacing: -.055em; font-weight: 500; } +.hero h1 em { color: var(--green); font-style: normal; } +.hero-copy > p { color: var(--muted); max-width: 650px; margin: 0; font-size: 16px; line-height: 1.9; } +.hero-notes { display: flex; flex-wrap: wrap; gap: 22px; margin-top: 30px; } +.hero-notes span { display: inline-flex; align-items: center; gap: 7px; color: #4f5c57; font-size: 12px; font-weight: 650; } +.hero-card { position: relative; z-index: 1; background: var(--ink); color: white; border-radius: 28px; padding: 30px; min-height: 390px; box-shadow: 0 28px 70px rgba(18,36,29,.18); overflow: hidden; } +.hero-card::after { content: ""; position: absolute; width: 240px; height: 240px; right: -85px; bottom: -110px; border: 1px solid rgba(216,242,141,.18); border-radius: 50%; box-shadow: 0 0 0 45px rgba(216,242,141,.035), 0 0 0 90px rgba(216,242,141,.02); } +.hero-card-top { display: flex; justify-content: space-between; align-items: center; color: #acb8b3; font-size: 12px; } +.rank-badge { color: var(--ink); background: var(--lime); padding: 6px 9px; border-radius: 999px; font-weight: 800; } +.lowest-country { display: flex; align-items: center; gap: 14px; margin-top: 50px; } +.large-flag { font-size: 38px; filter: drop-shadow(0 5px 10px rgba(0,0,0,.18)); } +.lowest-country div { display: flex; flex-direction: column; gap: 4px; } +.lowest-country strong { font-size: 20px; } +.lowest-country span:last-child { color: #94a29d; font-size: 12px; } +.lowest-price { margin-top: 28px; color: var(--lime); font-family: Georgia, serif; font-size: clamp(42px, 5vw, 66px); letter-spacing: -.04em; line-height: 1; } +.lowest-price small { display: block; margin-top: 12px; font: 500 11px/1 Inter, sans-serif; letter-spacing: .1em; color: #899892; } +.local-price-line { width: fit-content; margin-top: 24px; padding: 9px 12px; border: 1px solid rgba(255,255,255,.12); border-radius: 10px; color: #93a09b; font-size: 12px; } +.local-price-line strong { color: white; margin-left: 5px; } +.hero-card-foot { position: absolute; left: 30px; bottom: 24px; display: flex; align-items: center; gap: 7px; color: #8e9d97; font-size: 10px; z-index: 1; } +.hero-skeleton { height: 150px; margin-top: 58px; border-radius: 18px; background: linear-gradient(90deg, rgba(255,255,255,.04), rgba(255,255,255,.1), rgba(255,255,255,.04)); background-size: 200% 100%; animation: shimmer 1.4s infinite; } +.sample-banner { display: flex; align-items: center; gap: 9px; margin: -4px 0 24px; padding: 13px 16px; border-radius: 12px; background: #fff7e7; border: 1px solid #eddcb9; color: #805a19; font-size: 12px; } +.error-state { display: flex; align-items: center; gap: 16px; padding: 22px; border: 1px solid #e6c9c6; background: #fff3f2; border-radius: 16px; color: #8c3b34; } +.error-state div { display: flex; flex-direction: column; gap: 4px; } +.error-state span { font-size: 12px; opacity: .8; } +.metrics { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); border-radius: 20px; background: rgba(255,255,255,.72); overflow: hidden; } +.metric-card { min-height: 150px; padding: 24px; border-right: 1px solid var(--line); display: grid; grid-template-columns: 36px 1fr; grid-template-rows: auto auto auto; align-content: center; column-gap: 12px; } +.metric-card:last-child { border-right: 0; } +.metric-icon { grid-row: 1 / 4; width: 36px; height: 36px; display: grid; place-items: center; color: var(--green); background: var(--soft); border-radius: 10px; align-self: start; } +.metric-card > span { color: var(--muted); font-size: 11px; } +.metric-card > strong { margin: 5px 0 3px; font-family: Georgia, serif; font-size: 24px; font-weight: 600; } +.metric-card > small { color: #8b9591; font-size: 10px; } +.explorer { margin-top: 90px; } +.section-heading { display: flex; justify-content: space-between; align-items: end; margin-bottom: 24px; } +.section-heading h2 { margin: 8px 0 0; font-family: Georgia, "Noto Serif SC", serif; font-size: 38px; font-weight: 500; letter-spacing: -.035em; } +.section-heading p { color: var(--muted); font-size: 12px; } +.section-heading p strong { color: var(--ink); } +.controls { display: grid; grid-template-columns: minmax(260px, 1fr) auto auto auto; gap: 10px; margin-bottom: 16px; } +.search-control, .select-control { min-height: 48px; display: flex; align-items: center; gap: 10px; padding: 0 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); color: var(--muted); } +.search-control input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--ink); font-size: 13px; } +.search-control input::placeholder { color: #9ca6a2; } +.select-control > span { font-size: 10px; font-weight: 800; white-space: nowrap; color: #87928e; } +.select-control select { border: 0; outline: 0; background: transparent; color: var(--ink); font-size: 12px; font-weight: 650; cursor: pointer; } +.sort-control { background: var(--ink); border-color: var(--ink); color: white; } +.sort-control select { color: white; } +.sort-control option { color: var(--ink); } +.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); box-shadow: 0 14px 45px rgba(25,48,39,.04); } +table { width: 100%; min-width: 1060px; border-collapse: collapse; text-align: left; } +th { padding: 15px 20px; color: #7a8581; background: #f0f3ef; border-bottom: 1px solid var(--line); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; } +td { padding: 18px 20px; border-bottom: 1px solid #edf0ed; font-size: 12px; vertical-align: middle; } +tbody tr:last-child td { border-bottom: 0; } +tbody tr { transition: background .18s ease; } +tbody tr:hover { background: #f9fbf8; } +.country-cell { display: flex; align-items: center; gap: 12px; } +.rank { width: 24px; color: #a4ada9; font: 600 10px/1 monospace; } +.flag { font-size: 25px; line-height: 1; } +.country-cell div, .price-card-country div { display: flex; flex-direction: column; gap: 4px; } +.country-cell strong, .price-card-country strong { font-size: 13px; } +.country-cell div span, .price-card-country div span { color: #8a9490; font-size: 10px; } +.local-amount { display: block; font-size: 14px; } +.per-user { display: block; margin-top: 4px; color: #929c98; font-size: 9px; } +.converted-amount { color: var(--green); font: 600 20px/1 Georgia, serif; } +.tax-badge, .status-badge { display: inline-flex; align-items: center; gap: 6px; width: fit-content; padding: 6px 8px; border-radius: 7px; font-size: 9px; font-weight: 750; white-space: nowrap; } +.tax-badge.inclusive { color: #286c53; background: #e8f3ed; } +.tax-badge.exclusive { color: #85601f; background: #fff4dd; } +.status-badge.fresh { color: #356c57; background: #eef6f1; } +.status-badge.stale { color: #98631d; background: #fff5e4; } +.status-badge > span { width: 5px; height: 5px; border-radius: 50%; background: currentColor; } +.updated-time { display: block; margin-top: 5px; color: #9aa39f; font-size: 9px; } +.source-link { width: 32px; height: 32px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 9px; color: var(--green); transition: .18s ease; } +.source-link:hover { background: var(--green); border-color: var(--green); color: white; transform: translateY(-1px); } +.price-cards { display: none; } +.loading-rows { border: 1px solid var(--line); border-radius: 18px; background: white; overflow: hidden; } +.loading-rows span { display: block; height: 76px; border-bottom: 1px solid var(--line); background: linear-gradient(90deg, #fff, #f1f4f0, #fff); background-size: 200% 100%; animation: shimmer 1.4s infinite; } +.loading-rows span:last-child { border-bottom: 0; } +.empty-state { min-height: 220px; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 12px; border: 1px dashed #cdd5d0; border-radius: 18px; color: #8b9691; font-size: 12px; } +.explainers { margin: 90px 0 32px; display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.explainers article { padding: 36px 34px 40px 0; border-right: 1px solid var(--line); } +.explainers article + article { padding-left: 34px; } +.explainers article:last-child { border-right: 0; } +.explainer-number { color: var(--green); font: 700 10px/1 monospace; } +.explainers h3 { margin: 18px 0 10px; font-family: Georgia, "Noto Serif SC", serif; font-size: 20px; font-weight: 600; } +.explainers p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.8; } +.disclaimer { display: flex; align-items: flex-start; gap: 14px; padding: 20px 22px; border-radius: 16px; background: #ebefea; color: #66716c; } +.disclaimer > svg { flex: 0 0 auto; margin-top: 2px; } +.disclaimer p { margin: 0; font-size: 11px; line-height: 1.8; } +.disclaimer strong { color: var(--ink); } +.disclaimer a { display: inline-flex; align-items: center; gap: 3px; margin-left: 5px; color: var(--green); font-weight: 700; text-decoration: none; } +footer { width: min(1440px, calc(100% - 64px)); margin: 80px auto 0; padding: 32px 0 40px; border-top: 1px solid var(--line); display: grid; grid-template-columns: 1fr 1.5fr 1fr; align-items: center; gap: 30px; color: #88928e; font-size: 9px; } +.footer-brand .brand-mark { width: 30px; height: 30px; } +.footer-brand .brand-mark::before { width: 14px; height: 14px; } +footer p { text-align: center; line-height: 1.7; } +footer > span:last-child { text-align: right; } +@keyframes shimmer { to { background-position: -200% 0; } } + +@media (max-width: 1040px) { + .hero { grid-template-columns: 1fr .8fr; gap: 36px; } + .metrics { grid-template-columns: repeat(2, 1fr); } + .metric-card:nth-child(2) { border-right: 0; } + .metric-card:nth-child(-n+2) { border-bottom: 1px solid var(--line); } + .controls { grid-template-columns: 1fr 1fr 1fr; } + .search-control { grid-column: 1 / -1; } +} + +@media (max-width: 760px) { + .topbar, main, footer { width: min(100% - 32px, 680px); } + .topbar { height: 74px; } + .source-pill { display: none; } + .brand small { display: none; } + .currency-toggle button { padding: 7px 9px; font-size: 10px; } + .hero { min-height: auto; grid-template-columns: 1fr; gap: 34px; padding: 64px 0 46px; } + .hero::before { width: 360px; height: 360px; right: -250px; } + .hero h1 { margin-top: 22px; font-size: clamp(45px, 14vw, 68px); } + .hero-copy > p { font-size: 14px; line-height: 1.8; } + .hero-notes { gap: 12px 18px; margin-top: 24px; } + .hero-card { min-height: 365px; padding: 26px; } + .hero-card-foot { left: 26px; right: 20px; } + .metrics { grid-template-columns: 1fr 1fr; } + .metric-card { min-height: 132px; padding: 18px; grid-template-columns: 30px 1fr; column-gap: 9px; } + .metric-icon { width: 30px; height: 30px; } + .metric-card > strong { font-size: 19px; } + .explorer { margin-top: 64px; } + .section-heading { align-items: flex-end; } + .section-heading h2 { font-size: 30px; } + .controls { grid-template-columns: 1fr 1fr; } + .search-control { grid-column: 1 / -1; } + .sort-control { grid-column: 1 / -1; justify-content: center; } + .table-wrap { display: none; } + .price-cards { display: grid; grid-template-columns: 1fr; gap: 12px; } + .price-card { padding: 20px; border: 1px solid var(--line); border-radius: 17px; background: white; box-shadow: 0 10px 28px rgba(28,50,42,.04); } + .price-card-head { display: flex; justify-content: space-between; align-items: center; } + .mobile-rank { color: var(--green); font: 700 10px/1 monospace; } + .price-card-country { display: flex; align-items: center; gap: 12px; margin-top: 19px; } + .price-card-country .flag { font-size: 30px; } + .mobile-price { margin-top: 20px; color: var(--green); font: 600 34px/1 Georgia, serif; } + .mobile-price small { display: block; margin-top: 7px; color: #95a09b; font: 500 9px/1 Inter, sans-serif; } + .mobile-details { display: flex; align-items: center; justify-content: space-between; margin-top: 18px; padding-top: 14px; border-top: 1px solid #edf0ed; color: var(--muted); font-size: 10px; } + .mobile-details strong { color: var(--ink); margin-left: 4px; } + .price-card > a { display: inline-flex; align-items: center; gap: 4px; margin-top: 18px; color: var(--green); font-size: 10px; font-weight: 750; text-decoration: none; } + .explainers { grid-template-columns: 1fr; margin-top: 64px; } + .explainers article, .explainers article + article { padding: 28px 0; border-right: 0; border-bottom: 1px solid var(--line); } + .explainers article:last-child { border-bottom: 0; } + footer { margin-top: 56px; grid-template-columns: 1fr; text-align: center; } + .footer-brand { justify-self: center; } + footer p, footer > span:last-child { text-align: center; margin: 0; } +} + +@media (max-width: 430px) { + .topbar, main, footer { width: calc(100% - 24px); } + .brand strong { font-size: 12px; } + .brand-mark { width: 32px; height: 32px; } + .hero { padding-top: 48px; } + .hero-notes span:last-child { display: none; } + .metrics { grid-template-columns: 1fr; } + .metric-card { border-right: 0; border-bottom: 1px solid var(--line); } + .metric-card:last-child { border-bottom: 0; } + .controls { grid-template-columns: 1fr; } + .search-control, .sort-control { grid-column: auto; } + .section-heading p { display: none; } +} diff --git a/src/test-setup.ts b/src/test-setup.ts new file mode 100644 index 0000000..a86a67e --- /dev/null +++ b/src/test-setup.ts @@ -0,0 +1,2 @@ +import "@testing-library/jest-dom/vitest"; + diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..c907c40 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,49 @@ +export type DisplayCurrency = "CNY" | "USD"; +export type TaxTreatment = "inclusive" | "exclusive"; +export type RowStatus = "fresh" | "stale"; + +export type PriceRow = { + countryCode: string; + countryName: string; + currencyCode: string; + symbol: string; + minorUnitExponent: number; + localAmount: number; + usdAmount: number; + cnyAmount: number; + taxTreatment: TaxTreatment; + taxType: string | null; + taxPercent: number | null; + status: RowStatus; + fetchedAt: string; + sourceUrl: string; +}; + +export type PricingSnapshot = { + version: 1; + mode: "live" | "sample"; + generatedAt: string; + pricingSource: { + name: string; + urlTemplate: string; + }; + fx: { + baseCurrency: "USD"; + date: string; + source: string; + sourceUrls: string[]; + rates: Record; + }; + coverage: { + requested: number; + success: number; + unsupported: number; + failed: number; + stale: number; + }; + rows: PriceRow[]; +}; + +export type SortMode = "converted" | "country" | "local" | "updated"; +export type TaxFilter = "all" | TaxTreatment; +export type StatusFilter = "all" | RowStatus; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tests/collector.test.mjs b/tests/collector.test.mjs new file mode 100644 index 0000000..9dd5583 --- /dev/null +++ b/tests/collector.test.mjs @@ -0,0 +1,135 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + ISO_COUNTRY_CODES, + collectSnapshot, + convertAmount, + fetchJsonWithRetry, + mergeStaleRows, + parsePriceResponse, + validateCoverage, + withConvertedAmounts, +} from "../scripts/collect-prices.mjs"; + +const fixtures = path.resolve(process.cwd(), "tests/fixtures"); + +async function fixture(name) { + return JSON.parse(await readFile(path.join(fixtures, name), "utf8")); +} + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("official Business monthly parser", () => { + it("contains the full ISO alpha-2 probe list", () => { + expect(ISO_COUNTRY_CODES).toHaveLength(249); + expect(new Set(ISO_COUNTRY_CODES).size).toBe(249); + }); + + it.each([ + ["us.json", "US", 25, "USD", 2, "exclusive"], + ["sg.json", "SG", 32, "SGD", 2, "exclusive"], + ["in.json", "IN", 2250, "INR", 2, "exclusive"], + ["jp.json", "JP", 3850, "JPY", 0, "exclusive"], + ["de.json", "DE", 26, "EUR", 2, "inclusive"], + ["br.json", "BR", 130, "BRL", 2, "exclusive"], + ])("parses %s", async (name, code, amount, currency, exponent, tax) => { + const row = parsePriceResponse(await fixture(name), code, "2026-07-22T00:00:00.000Z"); + expect(row).toMatchObject({ + countryCode: code, + localAmount: amount, + currencyCode: currency, + minorUnitExponent: exponent, + taxTreatment: tax, + status: "fresh", + }); + }); + + it("rejects mismatched country responses", async () => { + const raw = await fixture("us.json"); + expect(() => parsePriceResponse(raw, "SG")).toThrow(/Country mismatch/); + }); + + it("classifies a 404 response as unsupported", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 404 })); + await expect(fetchJsonWithRetry("https://example.com/ZZ", { retries: 1 })).resolves.toEqual({ + kind: "unsupported", + status: 404, + }); + }); +}); + +describe("conversion and snapshot guards", () => { + const rates = { USD: 1, CNY: 6.8, SGD: 1.25 }; + + it("converts local currency through USD", () => { + expect(convertAmount(25, "USD", rates)).toEqual({ usdAmount: 25, cnyAmount: 170 }); + expect(convertAmount(32, "SGD", rates)).toEqual({ usdAmount: 25.6, cnyAmount: 174.08 }); + }); + + it("rejects missing rates", () => { + expect(() => convertAmount(100, "JPY", rates)).toThrow(/Missing FX rate/); + }); + + it("carries a failed row for at most fourteen days and refreshes its conversion", () => { + const previous = { + countryCode: "SG", + countryName: "新加坡", + currencyCode: "SGD", + symbol: "S$", + minorUnitExponent: 2, + localAmount: 32, + usdAmount: 99, + cnyAmount: 99, + taxTreatment: "exclusive", + taxType: "gst", + taxPercent: 9, + status: "fresh", + fetchedAt: "2026-07-10T00:00:00.000Z", + sourceUrl: "https://chatgpt.com/backend-anon/checkout_pricing_config/configs/SG", + }; + const rows = mergeStaleRows([], ["SG"], [previous], rates, new Date("2026-07-22T00:00:00.000Z")); + expect(rows[0]).toMatchObject({ status: "stale", usdAmount: 25.6, cnyAmount: 174.08 }); + expect(mergeStaleRows([], ["SG"], [previous], rates, new Date("2026-07-26T00:00:00.000Z"))).toEqual([]); + }); + + it("enforces baseline and 90 percent coverage", () => { + const us = withConvertedAmounts({ + countryCode: "US", countryName: "美国", currencyCode: "USD", symbol: "$", minorUnitExponent: 2, + localAmount: 25, taxTreatment: "exclusive", taxType: "other", taxPercent: null, + status: "fresh", fetchedAt: "2026-07-22T00:00:00.000Z", sourceUrl: "https://example.com/US", + }, rates); + const sg = withConvertedAmounts({ ...us, countryCode: "SG", countryName: "新加坡", currencyCode: "SGD", symbol: "S$", localAmount: 32 }, rates); + const snapshot = { fx: { rates }, rows: [us, sg] }; + expect(() => validateCoverage(snapshot, null, 2)).not.toThrow(); + expect(() => validateCoverage(snapshot, { rows: Array.from({ length: 3 }) }, 2)).toThrow(/90%/); + }); + + it("builds a validated snapshot from pricing and FX responses", async () => { + const usFixture = await fixture("us.json"); + const sgFixture = await fixture("sg.json"); + vi.stubGlobal("fetch", vi.fn(async (input) => { + const url = String(input); + if (url.endsWith("/US")) return { ok: true, status: 200, json: async () => usFixture }; + if (url.endsWith("/SG")) return { ok: true, status: 200, json: async () => sgFixture }; + if (url.endsWith("/ZZ")) return { ok: false, status: 404 }; + if (url.startsWith("https://api.frankfurter.dev/")) { + return { ok: true, status: 200, json: async () => ({ base: "USD", date: "2026-07-22", rates: { CNY: 6.8, SGD: 1.25 } }) }; + } + throw new Error(`Unexpected URL: ${url}`); + })); + + const snapshot = await collectSnapshot({ + countryCodes: ["US", "SG", "ZZ"], + generatedAt: "2026-07-22T00:00:00.000Z", + concurrency: 2, + }); + + expect(snapshot.coverage).toEqual({ requested: 3, success: 2, unsupported: 1, failed: 0, stale: 0 }); + expect(snapshot.rows.map((row) => row.countryCode)).toEqual(["US", "SG"]); + expect(snapshot.rows.find((row) => row.countryCode === "SG")).toMatchObject({ usdAmount: 25.6, cnyAmount: 174.08 }); + expect(() => validateCoverage(snapshot, null, 2)).not.toThrow(); + }); +}); diff --git a/tests/fixtures/br.json b/tests/fixtures/br.json new file mode 100644 index 0000000..596714f --- /dev/null +++ b/tests/fixtures/br.json @@ -0,0 +1 @@ +{"country_code":"BR","currency_config":{"business":{"month":{"amount":130,"tax":"exclusive"}}},"tax_type":"vat","tax_percent":0,"symbol_code":"BRL","symbol":"R$","minor_unit_exponent":2} diff --git a/tests/fixtures/de.json b/tests/fixtures/de.json new file mode 100644 index 0000000..3e100a2 --- /dev/null +++ b/tests/fixtures/de.json @@ -0,0 +1 @@ +{"country_code":"DE","currency_config":{"business":{"month":{"amount":26,"tax":"inclusive"}}},"tax_type":"vat","tax_percent":19,"symbol_code":"EUR","symbol":"€","minor_unit_exponent":2} diff --git a/tests/fixtures/in.json b/tests/fixtures/in.json new file mode 100644 index 0000000..010fb11 --- /dev/null +++ b/tests/fixtures/in.json @@ -0,0 +1 @@ +{"country_code":"IN","currency_config":{"business":{"month":{"amount":2250,"tax":"exclusive"}}},"tax_type":"gst","tax_percent":18,"symbol_code":"INR","symbol":"₹","minor_unit_exponent":2} diff --git a/tests/fixtures/jp.json b/tests/fixtures/jp.json new file mode 100644 index 0000000..bbe9e0f --- /dev/null +++ b/tests/fixtures/jp.json @@ -0,0 +1 @@ +{"country_code":"JP","currency_config":{"business":{"month":{"amount":3850,"tax":"exclusive"}}},"tax_type":"jct","tax_percent":10,"symbol_code":"JPY","symbol":"¥","minor_unit_exponent":0} diff --git a/tests/fixtures/sg.json b/tests/fixtures/sg.json new file mode 100644 index 0000000..bfdff5c --- /dev/null +++ b/tests/fixtures/sg.json @@ -0,0 +1 @@ +{"country_code":"SG","currency_config":{"business":{"month":{"amount":32,"tax":"exclusive"}}},"tax_type":"gst","tax_percent":9,"symbol_code":"SGD","symbol":"S$","minor_unit_exponent":2} diff --git a/tests/fixtures/unsupported.json b/tests/fixtures/unsupported.json new file mode 100644 index 0000000..1ed9d8d --- /dev/null +++ b/tests/fixtures/unsupported.json @@ -0,0 +1 @@ +{"detail":"Not found"} diff --git a/tests/fixtures/us.json b/tests/fixtures/us.json new file mode 100644 index 0000000..a68860e --- /dev/null +++ b/tests/fixtures/us.json @@ -0,0 +1 @@ +{"country_code":"US","currency_config":{"business":{"month":{"amount":25,"tax":"exclusive"}}},"tax_type":"other","symbol_code":"USD","symbol":"$","minor_unit_exponent":2} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..25fb0ef --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vitest/globals", "@testing-library/jest-dom", "node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..3f31b33 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,16 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + base: "./", + plugins: [react()], + build: { + target: "es2022", + sourcemap: true, + }, + test: { + environment: "jsdom", + setupFiles: "./src/test-setup.ts", + css: true, + }, +});