commit 1dc582ba07774dc0673ca8f23f0190bff9e85133 Author: abhishek Date: Mon Aug 24 20:35:18 2026 +0530 Initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dd616c6 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# The Fiesta API host used by the production build. +# In development this is left unset and Vite proxies /fiesta -> fiesta.nearle.app +# (see vite.config.ts), which keeps the network tab consistent and avoids +# preflight surprises. +VITE_API_BASE="https://fiesta.nearle.app" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11c21a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +.env +.env.local +*.local +.DS_Store +.vscode diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..c0c80ba --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=false diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d5222e --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# Nearle Console + +The new Nearle Daily retail operations console. Built against the existing Fiesta +backend — the API is a fixed constraint, not something this repo changes. + +## Running it + +```bash +npm install +npm run dev # http://localhost:3100 +npm run typecheck # tsc --noEmit +npm run build # typecheck + production build +``` + +> **Port 3100, not 3000.** The old console (`daily_merchant_web`) runs its dev +> server on 3000. `strictPort` is on, so if 3100 is taken this fails loudly +> rather than silently moving — which is the failure that makes you think your +> changes did not land when you are actually looking at a different app. + +In development Vite proxies `/fiesta` → `https://fiesta.nearle.app`. For a +deployed build set `VITE_API_BASE` (see `.env.example`). + +## Stack + +| Layer | Choice | +|---|---| +| Build | Vite 8 | +| Language | **TypeScript 7**, strict, `noUncheckedIndexedAccess`. No `.js`/`.jsx` anywhere. | +| UI | React 19 + **@astryxdesign/core** | +| Theme | `src/theme/nearle.ts`, compiled to `nearle.css` | +| Routing | React Router 7, split per page | +| Server state | TanStack Query 5 | +| Icons | lucide-react | +| Spreadsheets | `xlsx`, loaded on demand | + +## What's built + +Only the **Nearle Admin** workspace so far: + +- `/nearle/stores` — every tenant, branch counts, per-tenant performance +- `/nearle/stores/:tenantId` — one tenant's branches, orders and revenue +- `/nearle/onboard/tenant` — provision a merchant group +- `/nearle/onboard/branch` — commission an outlet with its delivery thresholds +- `/nearle/catalogue` — the global catalogue, plus both product-import paths + +`/admin/*` (Store Admin) and `/store/*` (Store Manager) render a named +placeholder rather than a 404, so those roles land somewhere that explains +itself. + +## Things about the backend that shape this code + +These are not bugs in this repo. They are the API's behaviour, and each one is +worked around deliberately. + +1. **No web authentication.** The login endpoints return the user record and no + token; only `/v1/pos/*` has middleware. So the session *is* that record, held + in `sessionStorage`. `src/auth/session.ts` is the only file that changes when + tokens arrive. +2. **Every list call needs a scoping id or it 400s.** The IDOR pass added + controller-level guards. Query hooks are therefore `enabled`-gated on the id + and the id is part of the query key. +3. **Roles are derived, not asserted.** `issuperadmin` is checked before + `roleid`, because the flag is server-derived and a roleid is not. Roleids 7 + and 8 are till roles and never reach an admin workspace. +4. **`POST /products/create` takes one product and returns no id.** The + controller passes the struct to the service by value, so GORM writes the + generated id into a copy that is then discarded. The sheet importer therefore + creates, then re-queries by SKU to resolve ids, then batches the location and + stock writes. See `importSheetProducts` in `src/api/products.ts`. +5. **The two import paths are not symmetric.** Catalogue import is one + idempotent batch call. Sheet import is N creates with no dedupe on SKU, so + re-uploading a file duplicates its products — the UI says so before the + button. +6. **The global catalogue has a price *range*, not a price**, and no mapping to + a tenant's own categories. Category, subcategory, retail price, cost and tax + are collected before an import can be enabled. +7. **POS endpoints take one required `locationid`.** There is no tenant-wide + counter-sales call, so a multi-branch POS view fans out per branch. + +## The 30-second cadence + +Online orders land when an order is placed; counter sales reach the console on a +30-second refetch; anything still sitting on an offline till has not arrived at +all. So **any figure blending the two is eventually consistent**, and the +`` component exists to say when a number was last true and how many +bills are still stranded. Polling is applied per query in `src/queries/hooks.ts` +rather than globally — a provisioning form has no business re-polling. + +## Design + +The visual system is ported from KROW (which is built on Astryx too, so the +tokens are role-for-role comparable), with Nearle purple `#662582` as the accent. +The ambient canvas in `index.css` — a fixed, viewport-wide horizontal gradient at +`z-index: -1` — is the one piece of KROW that carries most of the character. + +Radius, type scale and control heights are matched to KROW in +`src/theme/nearle.ts`, with the reasoning kept in comments. + +**Open:** the dark-mode accent `#B57FD0` was chosen for 5.41:1 contrast on +Astryx's dark surface and still needs brand-owner sign-off before dark mode +ships. diff --git a/index.html b/index.html new file mode 100644 index 0000000..5c6b8b7 --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + Nearle Console + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3a7e6a9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3998 @@ +{ + "name": "nearle-console", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nearle-console", + "version": "0.1.0", + "dependencies": { + "@astryxdesign/core": "^0.4.5", + "@stylexjs/stylex": "^0.19.0", + "@tanstack/react-query": "^5.101.4", + "lucide-react": "^1.33.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.2", + "recharts": "^3.10.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@astryxdesign/cli": "^0.4.5", + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^5.0.4", + "tailwindcss": "^4.3.3", + "typescript": "^7.0.2", + "vite": "^8.2.2" + } + }, + "node_modules/@astryxdesign/cli": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@astryxdesign/cli/-/cli-0.4.5.tgz", + "integrity": "sha512-3kUYK5K5eO77O4trXmzG/GfwMBiFI2Y7MOfsVK3euHZk98zQX/Z04MXag+S89v5qeObS3S2LI27QitkIq7hoOQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "jiti": "^2.7.0", + "jscodeshift": "^17.3.0", + "zod": "^4.4.3" + }, + "bin": { + "astryx": "clients/cli/bin/astryx.mjs", + "cli": "clients/cli/bin/astryx.mjs" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@astryxdesign/charts": "*", + "@astryxdesign/core": "*", + "@astryxdesign/lab": "*", + "@astryxdesign/theme-neutral": "*", + "gpt-tokenizer": "^3.4.0" + }, + "peerDependenciesMeta": { + "@astryxdesign/charts": { + "optional": true + }, + "@astryxdesign/core": { + "optional": true + }, + "@astryxdesign/lab": { + "optional": true + }, + "@astryxdesign/theme-neutral": { + "optional": true + } + } + }, + "node_modules/@astryxdesign/core": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@astryxdesign/core/-/core-0.4.5.tgz", + "integrity": "sha512-rbGaoAGZq1QImY2VWeWQNAh1ZqQa/KLWnmoOdy13lSjzMcAPDS6WrVv9fZfsNDx+DIG2oySLtMikwZk5WFl1Uw==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "intl-messageformat": "^11.2.9" + }, + "peerDependencies": { + "@stylexjs/stylex": "^0.19.0", + "react": ">=19.0.0", + "react-dom": ">=19.0.0" + } + }, + "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", + "peer": true, + "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.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@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-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "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-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "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-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-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.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-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "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.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", + "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-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "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-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "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-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "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-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@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-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/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.29.7.tgz", + "integrity": "sha512-KYIRV0BuaN68CDdsqFkAD7MU7yipUqQNuNElwATdxaIdpTjhvtY82QvkBJs7zV3Evxj2jFAAZ1iO8nyy0nhjqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-flow-strip-types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.7.tgz", + "integrity": "sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "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/@formatjs/fast-memoize": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", + "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.17.tgz", + "integrity": "sha512-cN9jhVqT7u0K9tix43fhjoUwL0nazyW6zsNIXs2QdPADr+nurPfYyssUiMqcSCGlPcCiqnYVxgSn7zBSuI+5Bg==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.11" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", + "license": "MIT" + }, + "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/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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/@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==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@stylexjs/stylex": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@stylexjs/stylex/-/stylex-0.19.0.tgz", + "integrity": "sha512-CnUFp7YMaDLDeemsWOfJgoC/gKM5P/yBNMcpJaE6ChJmXr7s0DJwSeGTTlHJcqqwN9OW1qGtmARWLFhGZN1pTA==", + "license": "MIT", + "peer": true, + "dependencies": { + "css-mediaquery": "^0.1.2", + "invariant": "^2.2.4", + "styleq": "0.2.1" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "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/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.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/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "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/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "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/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", + "license": "BSD" + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.411", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz", + "integrity": "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-toolkit": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz", + "integrity": "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "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/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flow-estree": { + "version": "0.328.0", + "resolved": "https://registry.npmjs.org/flow-estree/-/flow-estree-0.328.0.tgz", + "integrity": "sha512-uMB3dC4nfZYn+dd7/PYkyAK1mqBR4TJ4TWRdOjubpb/ObrLvPFdFSmMTq55fqttCCTygQ4NtD1Zdd+wcjC5t0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/flow-parser": { + "version": "0.328.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.328.0.tgz", + "integrity": "sha512-F+Ik2Of7ndl2FyPmZ77NrhbZp9lYjIsreDXeq4fo0sjxa3SIFv48QAszBqvPeMMzmw7JzqySJUxGR8CSD7EdXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flow-estree": "0.328.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "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/gpt-tokenizer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz", + "integrity": "sha512-wxFLnhIXTDjYebd9A9pGl3e31ZpSypbpIJSOswbgop5jLte/AsZVDvjlbEuVFlsqZixVKqbcoNmRlFDf6pz/UQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/intl-messageformat": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.14.tgz", + "integrity": "sha512-9f2VD1HFuxUvMw0RxsaP8WmMns6JRTnsNB/zghTFrp11ZktiXWwVDeZBPQchBKEmo+Gx/ZhxI7Qht7YglFD4PA==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.7", + "@formatjs/icu-messageformat-parser": "3.5.17" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jscodeshift": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-17.4.0.tgz", + "integrity": "sha512-i3ESKiiTsGynxzTg5BhsZViD0ai72/6SsI1efDZxG6/5KCoElsmquxtyhXK5lpEgoO7MTNpYkjFEdEL97SkBNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/preset-flow": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@babel/register": "^7.24.6", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "neo-async": "^2.5.0", + "picocolors": "^1.0.1", + "picomatch": "^4.0.2", + "recast": "^0.23.11", + "tmp": "^0.2.3", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "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/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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": "1.33.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.33.0.tgz", + "integrity": "sha512-MTRwMy0ZlL8Ur/vOAiJ9XGHE+kFPC7brq6MxAm0GiGXEBj0qy0jA/pG4N675oSzciO/UCdX8T+5yUQdmDeTLxg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "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.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "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/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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", + "peer": true, + "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", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": 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/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recast": { + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/styleq": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.2.1.tgz", + "integrity": "sha512-L0TR0NQb+X4/ktDEKmjWyp27gla+LUYi/by5k5SjKXf6/pvZP7wbwEB5J+tqxdFVPgzbsuz+d4RTScO/QZquBw==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "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/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7899743 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "nearle-console", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --port 3100 --host 0.0.0.0", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --port 3100", + "typecheck": "tsc --noEmit", + "test": "tsx --test \"src/**/*.test.ts\"" + }, + "dependencies": { + "@astryxdesign/core": "^0.4.5", + "@stylexjs/stylex": "^0.19.0", + "@tanstack/react-query": "^5.101.4", + "lucide-react": "^1.33.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.2", + "recharts": "^3.10.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@astryxdesign/cli": "^0.4.5", + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^5.0.4", + "tailwindcss": "^4.3.3", + "typescript": "^7.0.2", + "vite": "^8.2.2" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..0239e38 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..5060a96 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/icon-16.png b/public/icon-16.png new file mode 100644 index 0000000..db77a1e Binary files /dev/null and b/public/icon-16.png differ diff --git a/public/icon-180.png b/public/icon-180.png new file mode 100644 index 0000000..70b4c7c Binary files /dev/null and b/public/icon-180.png differ diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..b04d5fa Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-32.png b/public/icon-32.png new file mode 100644 index 0000000..5060a96 Binary files /dev/null and b/public/icon-32.png differ diff --git a/public/icon-48.png b/public/icon-48.png new file mode 100644 index 0000000..56590cf Binary files /dev/null and b/public/icon-48.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..bd0d622 Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/logo-512.png b/public/logo-512.png new file mode 100644 index 0000000..bd0d622 Binary files /dev/null and b/public/logo-512.png differ diff --git a/public/logo-wordmark-light.png b/public/logo-wordmark-light.png new file mode 100644 index 0000000..3da4859 Binary files /dev/null and b/public/logo-wordmark-light.png differ diff --git a/public/logo-wordmark.png b/public/logo-wordmark.png new file mode 100644 index 0000000..8c470b0 Binary files /dev/null and b/public/logo-wordmark.png differ diff --git a/replace_required.js b/replace_required.js new file mode 100644 index 0000000..92f5d3a --- /dev/null +++ b/replace_required.js @@ -0,0 +1,41 @@ +const fs = require('fs'); + +const req = (label) => `label={${label} *}`; + +function processFile(file) { + let content = fs.readFileSync(file, 'utf8'); + + // Find all components with isRequired + // We need to match: label="Something" ... isRequired + + // First, find all lines with label="..." + // If a block has isRequired, replace the label and remove isRequired. + + const tags = ['TextInput', 'Selector', 'NumberInput', 'TimeInput']; + + // A simple regex to find JSX tags and their props + const regex = /<(TextInput|Selector|NumberInput|TimeInput)([^>]+)isRequired([^>]*)>/g; + + content = content.replace(regex, (match, tag, before, after) => { + // extract label + const labelMatch = before.match(/label="([^"]+)"/); + if (!labelMatch) { + // maybe label is after isRequired + const labelMatchAfter = after.match(/label="([^"]+)"/); + if (!labelMatchAfter) return match; // fallback + + const newLabel = req(labelMatchAfter[1]); + return `<${tag}${before}${after.replace(labelMatchAfter[0], newLabel)}>`; + } + + const newLabel = req(labelMatch[1]); + return `<${tag}${before.replace(labelMatch[0], newLabel)}${after}>`; + }); + + fs.writeFileSync(file, content); +} + +processFile('src/features/nearle-admin/pages/OnboardTenantPage.tsx'); +processFile('src/features/nearle-admin/pages/OnboardBranchPage.tsx'); + +console.log("Done"); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..40ffc3f --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,152 @@ +import { lazy, Suspense, type ComponentType } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { Spinner } from '@astryxdesign/core/Spinner'; +import { RequireRole, useAuth } from '@/auth/AuthContext'; +import { HOME_ROUTE } from '@/auth/roles'; +import { LoginPage } from '@/features/auth/LoginPage'; +import { NearleAdminShell } from '@/features/nearle-admin/NearleAdminShell'; +import { StoreAdminShell } from '@/features/store-admin/StoreAdminShell'; + +/** + * Routes are split per page. + * + * The reference console ships as one 1.8MB chunk with no route splitting, which + * means every visitor downloads the spreadsheet importer to look at a dashboard. + * That is the one thing from it worth deliberately not copying. + */ +const named = (key: T, loader: () => Promise>) => + lazy(() => loader().then((module) => ({ default: module[key] }))); + +const StoresPage = named('StoresPage', () => import('@/features/nearle-admin/pages/StoresPage')); +const StoreDetailPage = named('StoreDetailPage', () => import('@/features/nearle-admin/pages/StoreDetailPage')); +const OnboardTenantPage = named('OnboardTenantPage', () => import('@/features/nearle-admin/pages/OnboardTenantPage')); +const GlobalCataloguePage = named('GlobalCataloguePage', () => import('@/features/nearle-admin/pages/GlobalCataloguePage')); + +const ConsolePage = named('ConsolePage', () => import('@/features/store-admin/pages/ConsolePage')); +const SalesPage = named('SalesPage', () => import('@/features/store-admin/pages/SalesPage')); +const InventoryPage = named('InventoryPage', () => import('@/features/store-admin/pages/InventoryPage')); +const ReportsPage = named('ReportsPage', () => import('@/features/store-admin/pages/ReportsPage')); +const OnboardBranchPage = named('OnboardBranchPage', () => import('@/features/store-admin/pages/OnboardBranchPage')); +const UsersPage = named('UsersPage', () => import('@/features/store-admin/pages/UsersPage')); +const TerminalsPage = named('TerminalsPage', () => import('@/features/store-admin/pages/TerminalsPage')); + +function RouteFallback() { + return ( +
+ +
+ ); +} + +/** + * Routes. + * + * Three workspaces, hard-partitioned by role and by URL prefix, the same way the + * three logins imply. Only the Nearle Admin workspace is built so far; the other + * two resolve to a placeholder rather than a 404, so a Store Admin signing in + * today lands somewhere that explains itself. + */ +export function App() { + const { user } = useAuth(); + + return ( + + } /> + + {/* Nearle Admin — the platform workspace */} + + }> + + + + } + > + } /> + } /> + } /> + } /> + {/* Branch onboarding moved to the Store Admin workspace — the merchant + opens their own outlets. Kept as a redirect rather than deleted so a + bookmark or a stale link lands on the directory instead of a 404. */} + } /> + } /> + {/* Absorbed here rather than by the global `*`, so a wrong sub-path can + never bounce out to a HOME_ROUTE that points back into this + workspace and loop. */} + } /> + + + {/* Store Admin — the merchant workspace, scoped to one tenant's branches */} + + }> + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + {/* Reached from the account menu, not the nav — setup is not a place + anyone works from day to day. See `AppShellProps.manageItems`. */} + } /> + } /> + {/* Catches `/admin/dashboard` and anything else that does not resolve. + Without this, an unknown sub-path escapes to the global `*`, which + redirects to this role's HOME_ROUTE — and if that is itself an + unknown path, the two bounce forever and the page renders blank. */} + } /> + + + {/* Not built yet. Named rather than missing, so the redirect has somewhere to land. */} + } /> + + } + /> + + ); +} + +function NotBuiltYet({ workspace }: { workspace: string }) { + const { signOut } = useAuth(); + return ( +
+
+

+ The {workspace} workspace is not built yet +

+

+ This console currently covers the Nearle Admin workspace. Your account signs in + correctly — there is simply nothing here for it to open. +

+ +
+
+ ); +} diff --git a/src/api/catalogue.ts b/src/api/catalogue.ts new file mode 100644 index 0000000..5826383 --- /dev/null +++ b/src/api/catalogue.ts @@ -0,0 +1,53 @@ +/** + * The global FMCG catalogue — a separate pgvector database, bridged to tenant + * data by the composite key `(brand, catalogueid)`. + * + * A catalogue row's bare `id` is only unique WITHIN its own brand table: + * `brand_dabur.id = 1` and `brand_nestle.id = 1` are different products. Every + * call that references a catalogue product sends both. + */ + +import { api, WEB } from './client'; +import type { CatalogueBrand, CatalogueProduct, CatalogueRef } from './types'; + +export interface CatalogueQuery { + /** Omit to search every brand merged — that is the "show everything" entry point. */ + brand?: string; + category?: string; + keyword?: string; + pageno?: number; + pagesize?: number; +} + +export const catalogueApi = { + /** + * Browse the global catalogue. Called with no `brand` this returns the full + * merged, paginated list — the list is never gated behind a brand selector. + */ + products: (query: CatalogueQuery = {}) => + api.get(`${WEB}/catalogue/getproducts`, { + brand: query.brand, + category: query.category, + keyword: query.keyword, + pageno: query.pageno ?? 0, + pagesize: query.pagesize ?? 48, + }), + + /** Brands with product counts, for the filter chip row. Never hardcode this list. */ + brands: () => api.get(`${WEB}/catalogue/getbrands`), + + categories: () => api.get(`${WEB}/catalogue/getcategories`), + + /** + * The `(brand, catalogueid)` pairs this tenant has already imported, for + * badging "Imported" in the browser. Called without `brand` because the list + * mixes brands. + */ + importedRefs: (tenantid: number) => + api.get(`${WEB}/products/getimportedcatalogueproducts`, { tenantid }), +}; + +/** Key for the imported-refs lookup. Both halves, always. */ +export function catalogueKey(ref: CatalogueRef | CatalogueProduct): string { + return 'catalogueid' in ref ? `${ref.brand}:${ref.catalogueid}` : `${ref.brand}:${ref.id}`; +} diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..3da4d6c --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,186 @@ +/** + * The Fiesta HTTP client. + * + * Everything the console knows about talking to the backend lives here, so the + * day the backend starts issuing a session token, this is the only file that + * changes. Nothing else in the app calls `fetch`. + */ + +import { demoResolve, isDemoActive, MISS } from '@/demo'; +import type { FiestaEnvelope } from './types'; + +/** + * In dev, Vite proxies `/fiesta` -> https://fiesta.nearle.app (see + * vite.config.ts), which keeps the network tab honest and sidesteps preflight + * surprises. In production the deployed host is set by VITE_API_BASE. + */ +const API_BASE = import.meta.env['VITE_API_BASE'] ?? '/fiesta'; + +/** Every console route lives under this prefix. `/mob/*` and `/pos/*` differ. */ +export const WEB = '/live/api/v1/web'; +export const POS = '/live/api/v1/pos'; + +/** + * A failed call, carrying the backend's own message. + * + * Fiesta answers HTTP 200 with `status: false` in several places, so the HTTP + * status alone is not enough to tell success from failure — both are checked. + */ +export class FiestaError extends Error { + readonly code: number; + readonly endpoint: string; + + constructor(message: string, code: number, endpoint: string) { + super(message); + this.name = 'FiestaError'; + this.code = code; + this.endpoint = endpoint; + } + + /** + * True when the backend rejected the call for want of a scoping id. + * + * The IDOR pass added controller-level guards: an unscoped list call 400s + * rather than returning every tenant's rows. That is a bug in the caller, + * not a server fault, and it should surface as one. + */ + get isMissingScope(): boolean { + return this.code === 400 && /required/i.test(this.message); + } +} + +export type QueryValue = string | number | boolean | null | undefined; + +/** Drops empty params rather than sending `?tenantid=` and getting a 400 back. */ +function toQueryString(params: Record | undefined): string { + if (!params) return ''; + const search = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null || value === '') continue; + search.set(key, String(value)); + } + const qs = search.toString(); + return qs ? `?${qs}` : ''; +} + +interface RequestOptions { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + params?: Record; + body?: unknown; + signal?: AbortSignal; +} + +async function request(path: string, options: RequestOptions = {}): Promise { + const { method = 'GET', params, body, signal } = options; + + // Demo mode short-circuits before any network call. In a production build + // `isDemoActive` is a constant `false`, so the bundler removes this branch + // and the fixtures with it. + if (import.meta.env.DEV && isDemoActive()) { + const fixture = await demoResolve(path, params as Record | undefined); + if (fixture !== MISS) { + // A beat of latency, so loading states are visible while working on them. + await new Promise((resolve) => setTimeout(resolve, 180)); + return fixture as T; + } + } + + const url = `${API_BASE}${path}${toQueryString(params)}`; + + const init: RequestInit = { + method, + headers: { Accept: 'application/json' }, + signal: signal ?? null, + }; + + if (body !== undefined) { + init.headers = { ...init.headers, 'Content-Type': 'application/json' }; + init.body = JSON.stringify(body); + } + + let response: Response; + try { + response = await fetch(url, init); + } catch (cause) { + // A network failure and a 500 read very differently to a user; keep them + // distinguishable rather than collapsing both into "something went wrong". + throw new FiestaError( + cause instanceof DOMException && cause.name === 'AbortError' + ? 'Request cancelled' + : 'Could not reach the server', + 0, + path, + ); + } + + let envelope: FiestaEnvelope; + try { + envelope = (await response.json()) as FiestaEnvelope; + } catch { + throw new FiestaError(`Malformed response (HTTP ${response.status})`, response.status, path); + } + + if (!response.ok || envelope.status === false) { + throw new FiestaError( + envelope.message ?? `Request failed (HTTP ${response.status})`, + envelope.code ?? response.status, + path, + ); + } + + return envelope.details as T; +} + +/** + * The whole envelope, for the handful of callers that need `message` or + * `tenantform` on success — login being the one that matters. + */ +async function requestEnvelope( + path: string, + options: RequestOptions = {}, +): Promise> { + const { method = 'GET', params, body } = options; + const url = `${API_BASE}${path}${toQueryString(params)}`; + + const init: RequestInit = { method, headers: { Accept: 'application/json' } }; + if (body !== undefined) { + init.headers = { ...init.headers, 'Content-Type': 'application/json' }; + init.body = JSON.stringify(body); + } + + let response: Response; + try { + response = await fetch(url, init); + } catch { + throw new FiestaError('Could not reach the server', 0, path); + } + + try { + return (await response.json()) as FiestaEnvelope; + } catch { + throw new FiestaError(`Malformed response (HTTP ${response.status})`, response.status, path); + } +} + +export const api = { + get: (path: string, params?: Record, signal?: AbortSignal) => + request(path, { method: 'GET', params, signal }), + + post: (path: string, body?: unknown, params?: Record) => + request(path, { method: 'POST', body, params }), + + put: (path: string, body?: unknown, params?: Record) => + request(path, { method: 'PUT', body, params }), + + del: (path: string, body?: unknown, params?: Record) => + request(path, { method: 'DELETE', body, params }), + + envelope: requestEnvelope, +}; + +/** Normalises anything thrown into a message worth showing a person. */ +export function errorMessage(error: unknown): string { + if (error instanceof FiestaError) return error.message; + if (error instanceof Error) return error.message; + return 'Something went wrong'; +} diff --git a/src/api/insights.ts b/src/api/insights.ts new file mode 100644 index 0000000..0b45473 --- /dev/null +++ b/src/api/insights.ts @@ -0,0 +1,159 @@ +/** + * Order, delivery and POS reads — the numbers behind store performance. + * + * Note the split: online orders come from `/web/orders/*` and counter sales + * from `/pos/*`, which is the ONLY part of the API carrying auth middleware. + * Any figure that blends the two is eventually consistent by construction, so + * screens that show one must also show its freshness. + */ + +import { api, POS, WEB } from './client'; +import type { + DeliveryRow, + DeliverySummary, + LocationOrderSummary, + OrderRow, + OrderSummary, + PosLocationHealth, + PosSalesPage, + PosSalesSummary, +} from './types'; + +export interface DateRange { + fromdate?: string; + todate?: string; +} + +export interface OrderQuery extends DateRange { + tenantid: number; + /** Omit for every branch of the tenant. */ + locationid?: number; + status?: string; + keyword?: string; + pageno?: number; + pagesize?: number; +} + +export const insightsApi = { + /** + * The order rows themselves. + * + * `/orders/tenant/getorders` rather than the bare `/orders/getorders`: the + * controller routes on which ids are present, and passing a tenant with no + * partner, customer or app-user reaches `GetTenantOrders`. Passing a + * `locationid` as well reaches `GetTenantLocationOrders`, which is the + * branch-scoped read — so one call covers both "all branches" and "one + * branch" by presence alone. + * + * `pageno` is 1-based here. The controller floors anything <= 0 to 1, so + * sending 0 silently gives page one rather than an error. + */ + orders: (query: OrderQuery) => + api.get(`${WEB}/orders/tenant/getorders`, { + tenantid: query.tenantid, + locationid: query.locationid, + status: query.status, + keyword: query.keyword, + fromdate: query.fromdate, + todate: query.todate, + pageno: query.pageno ?? 1, + pagesize: query.pagesize ?? 50, + }), + + /** + * The delivery jobs. + * + * A separate read from the orders list, NOT a filter over it. The rows are a + * different struct with different fields — rider name, planned vs actual + * distance, rider charge vs job value, notes — and a different status ladder. + * Deriving deliveries from orders, which is what this page did first, loses + * every one of those. + * + * The controller 400s unless one of tenantid/partnerid/customerid/ + * applocationid/userid/appuserid is present, so `tenantid` is required here. + */ + deliveries: (query: OrderQuery) => + api.get(`${WEB}/deliveries/getdeliveries`, { + tenantid: query.tenantid, + locationid: query.locationid, + status: query.status, + keyword: query.keyword, + fromdate: query.fromdate, + todate: query.todate, + pageno: query.pageno ?? 1, + pagesize: query.pagesize ?? 50, + }), + + orderSummary: (tenantid: number, range: DateRange = {}) => + api.get(`${WEB}/orders/getordersummary`, { tenantid, ...range }), + + /** Per-branch order totals for one tenant. `tenantid` is required. */ + locationSummary: (tenantid: number, range: DateRange = {}) => + api.get(`${WEB}/orders/getlocationsummary`, { tenantid, ...range }), + + revenueSummary: (tenantid: number, range: DateRange = {}) => + api.get(`${WEB}/orders/getrevenuesummary`, { tenantid, ...range }), + + timeSeries: (tenantid: number, range: DateRange = {}) => + api.get[]>(`${WEB}/orders/gettimeseries`, { tenantid, ...range }), + + deliverySummary: (tenantid: number, range: DateRange = {}) => + api.get(`${WEB}/deliveries/deliverysummary`, { tenantid, ...range }), + + /** + * Counter sales for ONE outlet. + * + * `locationid` is required and singular — there is no tenant-wide POS call, + * so a multi-branch view fans out one request per branch. + */ + posSales: ( + locationid: number, + params: DateRange & { + pageno?: number; + pagesize?: number; + /** The three server-side filters the old console's bills tab offers. */ + terminalid?: string; + cashiername?: string; + paymentmode?: string; + } = {}, + ) => + api.get(`${POS}/sales`, { + locationid, + pageno: params.pageno ?? 0, + pagesize: params.pagesize ?? 50, + fromdate: params.fromdate, + todate: params.todate, + terminalid: params.terminalid, + cashiername: params.cashiername, + paymentmode: params.paymentmode, + }), + + /** + * One counter bill in full, with its lines. + * + * `reference` accepts the till's order UUID, the invoice number, or this + * backend's posorderid — a support call starts from whichever the person is + * looking at, so the endpoint takes all three. + */ + posSaleDetail: (locationid: number, reference: string) => + api.get(`${POS}/sales/detail`, { locationid, reference }), + + /** + * Counter-sales totals for ONE outlet. + * + * The richest read in the API: bill count, gross, tax, discount, roundoff and + * average bill, already broken down by payment mode, by day and by terminal. + * Reports gets its offline half from this and nothing else. + * + * Kept apart from the order summaries above on purpose. These are `posorders` + * rows; those are `orders` rows. The two are never added together — see + * `store-admin-backend-gap.md` §3.1 for why that would double-count a branch + * that both runs a till and uploads a spreadsheet. + */ + posSalesSummary: (locationid: number, range: DateRange = {}) => + api.get(`${POS}/sales/summary`, { locationid, ...range }), + + /** Till presence for one outlet — how many are online, how many bills are stranded. */ + posHealth: (locationid: number) => + api.get(`${POS}/health/location`, { location_id: locationid }), +}; diff --git a/src/api/people.ts b/src/api/people.ts new file mode 100644 index 0000000..c28ec01 --- /dev/null +++ b/src/api/people.ts @@ -0,0 +1,160 @@ +/** + * People — back-office staff and till accounts. + * + * Two account systems that happen to share one table. A till account is NOT a + * Nearle Daily user: the backend excludes roles 7 and 8 from every application + * lookup inside the query itself, deliberately, so a cashier is "not found" + * rather than "refused". They are read and written through different endpoints + * with different conventions, and this file keeps them apart. + * + * Three things this module will not do, each for a reason recorded in + * `store-admin-user-menu-plan.md`: + * + * - **No delete.** `DELETE /users/delete` and `DELETE /deleteposuser` are hard + * deletes with no cascade. Deactivating via `status` is the safe equivalent + * and is what both list screens offer. + * - **No password management.** `PUT /users/update` doubles as the + * password-reset call and passwords are stored in clear. Creating an account + * is in scope; issuing its password is not, until hashing exists. + * - **Never send `roleid: -1`.** The old console clears a role that way; + * `UpdateStaff` does not special-case it, so `-1` lands in the column and the + * account ends up holding a role that matches nothing. + */ + +import { api, POS, WEB } from './client'; +import type { PosRole, PosUser, StaffInfo, StaffShift } from './types'; + +/* ── Back-office staff ───────────────────────────────────────────────────── */ + +export interface CreateStaffRequest { + tenantid: number; + locationid: number; + firstname: string; + lastname?: string; + email: string; + contactno: string; + roleid: number; + status?: string; +} + +export interface UpdateStaffRequest { + userid: number; + firstname?: string; + lastname?: string; + email?: string; + contactno?: string; + roleid?: number; + locationid?: number; + status?: string; +} + +export const staffApi = { + /** + * The tenant's back-office directory. + * + * `getstaffs`, NOT `getallusers`. This one resolves `rolename` server-side, + * and the backend says why that matters: "`app_roles` holds six rows for four + * back-office roles and most accounts carry an id absent from it, so any + * mapping written client-side is wrong." + */ + list: (tenantid: number) => + api.get(`${WEB}/tenants/getstaffs`, { tenantid }), + + create: (body: CreateStaffRequest) => api.post(`${WEB}/users/create`, body), + + /** + * Update a person. + * + * GORM's `Updates` with a struct skips zero values, so an omitted field is + * left alone rather than blanked — which is why every field here is optional + * and why clearing something is not possible through this call. + */ + update: (body: UpdateStaffRequest) => api.put(`${WEB}/users/update`, body), +}; + +/* ── Till accounts ───────────────────────────────────────────────────────── */ + +export interface CreatePosUserRequest { + tenantid: number; + locationid: number; + full_name: string; + /** + * The role NAME, lowercase — "supervisor" or "cashier". + * + * Not the id. `PosRoleFromName` reads the name off the request and returns 0 + * for anything it does not recognise, which every caller treats as a refusal + * rather than as a default. Sending 7 or 8 here does nothing. + */ + role: string; + /** Ten digits. The till matches on it exactly — see the note in `normaliseMobile`. */ + contactno: string; + pin?: string; + /** A `staffshifts.staff_shift_id`. Zero leaves it unset. */ + shift_id?: number; + status?: string; +} + +export interface UpdatePosUserRequest { + tenantid: number; + locationid: number; + user_id: number; + full_name?: string; + role?: string; + contactno?: string; + shift_id?: number; + status?: string; +} + +export const posUsersApi = { + /** Till accounts at one outlet. `locationid` is required and singular. */ + list: (tenantid: number, locationid: number) => + api.get(`${POS}/getposusers`, { tenantid, locationid }), + + /** + * The role picker's source. + * + * Read rather than hardcoded. Supervisor is 7 and Cashier is 8 today, but the + * endpoint also carries the label and the description a person needs to + * choose between them — and a third role would appear here first. + */ + roles: () => api.get(`${POS}/posroles`), + + /** + * Create a till account. + * + * The response carries the PIN or password ONCE. The backend is explicit that + * a listing never returns it: "An admin who loses it reissues rather than + * looks it up." So it is shown at creation and never read back. + */ + create: (body: CreatePosUserRequest) => api.post(`${POS}/createposuser`, body), + + update: (body: UpdatePosUserRequest) => api.put(`${POS}/updateposuser`, body), + + /** Shift windows a till account can be put on. */ + shifts: (tenantid: number, locationid: number) => + api.get(`${POS}/getstaffshifts`, { tenantid, locationid }), +}; + +/** + * Ten digits, or nothing. + * + * The till matches the mobile number EXACTLY, so `+91 98765 43210` typed back + * as `9876543210` would not find the row. Stripping to the last ten digits at + * the edge means an admin can paste whatever their contact list gave them. + */ +export function normaliseMobile(input: string): string { + const digits = input.replace(/\D/g, ''); + return digits.length > 10 ? digits.slice(-10) : digits; +} + +/** Mon-first mask → "Mon–Fri", "Every day". `weekdays` empty means every day. */ +export function weekdayLabel(mask: string | undefined): string { + if (!mask || !/^[01]{7}$/.test(mask)) return 'Every day'; + const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + const on = [...mask].map((bit, index) => (bit === '1' ? days[index] : null)).filter(Boolean); + if (on.length === 7) return 'Every day'; + if (on.length === 0) return '—'; + if (mask === '1111100') return 'Mon–Fri'; + if (mask === '0000011') return 'Weekends'; + return on.join(', '); +} diff --git a/src/api/products.ts b/src/api/products.ts new file mode 100644 index 0000000..2563660 --- /dev/null +++ b/src/api/products.ts @@ -0,0 +1,213 @@ +/** + * Product, stock and import endpoints. + * + * Two import paths exist and they are NOT symmetric — the asymmetry is the + * backend's, not a choice made here: + * + * Catalogue path : one batch call, idempotent. Re-importing the same + * (tenantid, brand, catalogueid) tops up stock and + * overwrites price instead of duplicating. + * + * Sheet path : `create` accepts ONE product, not an array, and does not + * return the generated productid — the controller passes the + * struct to the service by value, so GORM writes the id into + * a copy that is then discarded, and the response echoes what + * was sent. So a sheet import is N creates, then a lookup by + * SKU to resolve ids, then one batched location call and one + * batched stock call. + * + * `importSheetProducts` below encapsulates that whole dance so no screen has to + * know about it. + */ + +import { api, WEB } from './client'; +import type { + ImportCatalogueProductRequest, + Product, + ProductCategory, + ProductLocationRequest, + ProductStockRequest, + ProductSubCategory, +} from './types'; + +export interface LocationProductQuery { + tenantid: number; + locationid: number; + pageno?: number; + pagesize?: number; +} + +export const productsApi = { + /** A store's own catalogue — what is actually imported, with live stock. */ + locationProducts: (query: LocationProductQuery) => + api.get(`${WEB}/products/getlocationproducts`, { + tenantid: query.tenantid, + locationid: query.locationid, + pageno: query.pageno ?? 0, + pagesize: query.pagesize ?? 50, + }), + + allProducts: (tenantid: number) => + api.get(`${WEB}/products/getallproducts`, { tenantid }), + + count: (tenantid: number) => + api.get<{ count?: number }>(`${WEB}/products/getproductscount`, { tenantid }), + + categories: (tenantid: number) => + api.get(`${WEB}/products/getproductcategories`, { tenantid }), + + subCategories: (tenantid: number, categoryid: number) => + api.get(`${WEB}/products/getproductsubcategories`, { + tenantid, + categoryid, + }), + + /** Batch, idempotent. Send the whole selection in one call. */ + importFromCatalogue: (rows: ImportCatalogueProductRequest[]) => + api.post(`${WEB}/products/importcatalogueproduct`, rows), + + /** Single product only — the backend parses one object, not an array. */ + createProduct: (product: Partial) => + api.post(`${WEB}/products/create`, product), + + /** Array. Upserts on (tenantid, locationid, productid). */ + createProductLocations: (rows: ProductLocationRequest[]) => + api.post(`${WEB}/products/createproductlocation`, rows), + + /** Array. Appends to the stock ledger. */ + createProductStock: (rows: ProductStockRequest[]) => + api.post(`${WEB}/products/createproductstock`, rows), + + publish: (body: { tenantid: number; locationid: number; productid: number }) => + api.post(`${WEB}/products/publishproduct`, body), + + /** Unlinks from the store. The product row and its order history survive. */ + removeFromStore: (body: { tenantid: number; locationid: number; productid: number }) => + api.del(`${WEB}/products/deleteproductlocation`, body), +}; + +/* ──────────────────────────────────────────────────────────────────────────── + The sheet-import dance + ──────────────────────────────────────────────────────────────────────────── */ + +/** One validated row from the uploaded workbook. */ +export interface SheetProductRow { + productname: string; + productsku: string; + categoryid: number; + subcategoryid: number; + retailprice: number; + productcost: number; + taxpercent: number; + quantity: number; + productunit?: string; + unitvalue?: string; + productbrand?: string; + productdesc?: string; +} + +export interface SheetImportResult { + created: number; + linked: number; + stocked: number; + /** Rows the backend rejected, paired with the reason, so they can be retried. */ + failures: { row: SheetProductRow; reason: string }[]; +} + +export interface SheetImportOptions { + tenantid: number; + locationid: number; + rows: SheetProductRow[]; + onProgress?: (done: number, total: number) => void; +} + +/** + * Imports a parsed sheet. + * + * NOT idempotent, and it cannot be made so from this side: nothing in the API + * dedupes on SKU, so uploading the same workbook twice creates the products + * twice. The importer UI is responsible for warning before a re-upload. + * + * Creates run sequentially rather than in parallel on purpose. There is no + * batch create, and firing 500 concurrent writes at a single-instance Go + * service to save a few seconds is a poor trade against a half-imported tenant. + */ +export async function importSheetProducts( + options: SheetImportOptions, +): Promise { + const { tenantid, locationid, rows, onProgress } = options; + const failures: SheetImportResult['failures'] = []; + const createdSkus: string[] = []; + + for (const [index, row] of rows.entries()) { + try { + await productsApi.createProduct({ + tenantid, + productname: row.productname, + productsku: row.productsku, + categoryid: row.categoryid, + subcategoryid: row.subcategoryid, + retailprice: row.retailprice, + productcost: row.productcost, + taxpercent: row.taxpercent, + productunit: row.productunit, + unitvalue: row.unitvalue, + productbrand: row.productbrand, + productdesc: row.productdesc, + productstatus: 'Active', + }); + createdSkus.push(row.productsku); + } catch (error) { + failures.push({ row, reason: error instanceof Error ? error.message : 'Create failed' }); + } + onProgress?.(index + 1, rows.length); + } + + if (createdSkus.length === 0) { + return { created: 0, linked: 0, stocked: 0, failures }; + } + + // Resolve the ids the create endpoint refused to hand back. + const all = await productsApi.allProducts(tenantid); + const bySku = new Map(); + for (const product of all) { + if (product.productsku) bySku.set(product.productsku, product); + } + + const locationRows: ProductLocationRequest[] = []; + const stockRows: ProductStockRequest[] = []; + + for (const row of rows) { + if (!createdSkus.includes(row.productsku)) continue; + const product = bySku.get(row.productsku); + if (!product) { + failures.push({ row, reason: 'Created, but could not be found again by SKU' }); + continue; + } + locationRows.push({ + tenantid, + locationid, + productid: product.productid, + price: row.retailprice, + status: 'available', + }); + stockRows.push({ + tenantid, + locationid, + productid: product.productid, + quantity: row.quantity, + stocktype: 'in', + status: 'Active', + }); + } + + if (locationRows.length > 0) await productsApi.createProductLocations(locationRows); + if (stockRows.length > 0) await productsApi.createProductStock(stockRows); + + return { + created: createdSkus.length, + linked: locationRows.length, + stocked: stockRows.length, + failures, + }; +} diff --git a/src/api/stock.ts b/src/api/stock.ts new file mode 100644 index 0000000..5dc0aef --- /dev/null +++ b/src/api/stock.ts @@ -0,0 +1,98 @@ +/** + * Stock requests and stock movement — the Store Admin's approval surface. + * + * Read `store-admin-backend-gap.md` §2.2 before extending this file. The + * approval workflow the spec describes does not exist in Fiesta: the request + * table has no reason, requester, approved quantity, approver or remarks, and + * `UpdateStockRequest(requestID, status)` takes a bare string. Exactly one + * value does anything — "Received" — and it posts a stock movement for the FULL + * requested quantity, guarded against double-receiving. + * + * So "approve for a different quantity" is not implemented here because it + * cannot be implemented here. It is a backend change, not a frontend one. + */ + +import { api, WEB } from './client'; +import type { StockRequest, StockStatementRow } from './types'; + +/** + * The status values the backend actually distinguishes. + * + * `Received` is the only one with behaviour. The others are stored verbatim and + * read back, which is enough to drive a queue but is not a state machine — the + * backend will accept any string at all. + */ +export const STOCK_REQUEST_STATUS = { + pending: 'Pending', + received: 'Received', + rejected: 'Rejected', +} as const; + +export type StockRequestStatus = (typeof STOCK_REQUEST_STATUS)[keyof typeof STOCK_REQUEST_STATUS]; + +export interface StockRequestQuery { + tenantid: number; + /** Omit for every branch. */ + locationid?: number; + status?: string; + date?: string; + pageno?: number; + pagesize?: number; +} + +export const stockApi = { + requests: (query: StockRequestQuery) => + api.get(`${WEB}/products/getstockrequests`, { + tenantid: query.tenantid, + locationid: query.locationid, + status: query.status, + date: query.date, + pageno: query.pageno ?? 0, + pagesize: query.pagesize ?? 50, + }), + + /** + * Approve a request — which means marking it Received. + * + * This adds `request.qty` to the branch's stock. There is no way to approve a + * different amount: the service reads the quantity off the request row, not + * off this call. + */ + approve: (requestid: number) => + api.put(`${WEB}/products/updatestockrequest`, { + requestid, + status: STOCK_REQUEST_STATUS.received, + }), + + /** Reject — a status write and nothing else. No stock moves, no reason stored. */ + reject: (requestid: number) => + api.put(`${WEB}/products/updatestockrequest`, { + requestid, + status: STOCK_REQUEST_STATUS.rejected, + }), + + /** + * Stock movement for one branch. + * + * Opening, credit, debit, closing — and that is the whole vocabulary. + * `productstocks.stocktype` is `in`/`out`, so a sale, a transfer and a + * correction are indistinguishable once written. The spec's seven movement + * types (§3.8) cannot be sourced; see gap §2.5. + */ + statement: (params: { + tenantid: number; + locationid: number; + subcategoryid?: number; + keyword?: string; + pageno?: number; + pagesize?: number; + }) => + api.get(`${WEB}/products/getstockstatement`, { + tenantid: params.tenantid, + locationid: params.locationid, + subcategoryid: params.subcategoryid, + keyword: params.keyword, + pageno: params.pageno ?? 0, + pagesize: params.pagesize ?? 100, + }), +}; diff --git a/src/api/tenants.ts b/src/api/tenants.ts new file mode 100644 index 0000000..095e132 --- /dev/null +++ b/src/api/tenants.ts @@ -0,0 +1,71 @@ +/** Tenant and branch endpoints — the Nearle Admin's provisioning surface. */ + +import { api, WEB } from './client'; +import type { TenantInfo, TenantLocation } from './types'; + +/** Everything the tenant-onboarding form collects. */ +export interface CreateTenantRequest { + tenantname: string; + companyname: string; + primarycontact: string; + primaryemail: string; + locationname: string; + categoryid: number; + subcategoryid?: number; + address: string; + suburb?: string; + city: string; + state: string; + postcode: string; + latitude?: string; + longitude?: string; + moduleid?: number; + status?: string; +} + +/** Everything the branch-onboarding form collects. */ +export interface CreateBranchRequest { + tenantid: number; + locationname: string; + email?: string; + contactno?: string; + address: string; + suburb?: string; + city: string; + state: string; + postcode: string; + latitude?: string; + longitude?: string; + opentime?: string; + closetime?: string; + deliveryradius?: number; + deliverymins?: number; + status?: string; +} + +export const tenantsApi = { + /** + * Every tenant on the platform. Deliberately unscoped — this is the + * Nearle Admin's list, and the backend treats it as the platform-operator + * endpoint rather than a tenant-scoped one. + */ + listAll: () => api.get(`${WEB}/tenants/getalltenants`), + + /** Branches under one tenant. `tenantid` is required — omit it and it 400s. */ + locations: (tenantid: number) => + api.get(`${WEB}/tenants/gettenantlocations`, { tenantid }), + + search: (keyword: string) => + api.get(`${WEB}/tenants/searchbykeyword`, { keyword }), + + /** Provisions the enterprise and spawns its primary Administrator account. */ + createTenant: (body: CreateTenantRequest) => + api.post(`${WEB}/tenants/createtenantlocation`, body), + + /** Commissions a branch and spawns a placeholder branch-manager account. */ + createBranch: (body: CreateBranchRequest) => + api.post(`${WEB}/tenants/createlocation`, body), + + updateBranch: (body: Partial & { locationid: number }) => + api.put(`${WEB}/tenants/updatelocation`, body), +}; diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..7399c66 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,697 @@ +/** + * Types for the Fiesta REST API. + * + * Hand-written from the Go structs in `backend_fiesta/models` — there is no + * OpenAPI document to generate from. Field names mirror the `json:` tags + * EXACTLY, including the ones that are misspelled on the wire + * (`applolcationid`, `Subcategoryname`, `Catlougeid`, `Accountname`). Fixing + * them here would only mean the client silently reads undefined. + */ + +/* ──────────────────────────────────────────────────────────────────────────── + Envelope + ──────────────────────────────────────────────────────────────────────────── */ + +/** + * Every Fiesta handler answers in this shape. `details` is the payload and its + * type varies per endpoint — sometimes an object, sometimes an array, and on + * the create endpoints an echo of what was sent. + * + * `status` is the success flag and `code` repeats the HTTP status in the body. + * Both are checked: several handlers return HTTP 200 with `status: false`. + */ +export interface FiestaEnvelope { + status: boolean; + code: number; + message?: string; + details?: T; + /** Present on the tenant login endpoints. */ + tenantform?: boolean; +} + +/* ──────────────────────────────────────────────────────────────────────────── + Users — models/users.go + ──────────────────────────────────────────────────────────────────────────── */ + +export interface FiestaUser { + userid: number; + authname: string; + configid: number; + authmode: number; + roleid: number; + firstname: string; + lastname: string; + fullname?: string; + email: string; + contactno: string; + address?: string; + suburb?: string; + city?: string; + state?: string; + postcode?: string; + shiftid?: number; + shiftname?: string; + partnerid?: number; + tenantid: number; + locationid: number; + applocationid?: number; + applocation?: string; + status: string; + /** Server-derived platform-operator flag. Never inferred client-side. */ + issuperadmin?: boolean; +} + +/* ──────────────────────────────────────────────────────────────────────────── + Tenants & locations — models/tenant.go + ──────────────────────────────────────────────────────────────────────────── */ + +export interface TenantInfo { + tenantid: number; + locationid: number; + tenantname: string; + locationname: string; + tenanttype?: string; + registrationno?: string; + companyname?: string; + primaryemail?: string; + primarycontact?: string; + locationcontact?: string; + categoryid?: number; + subcategoryid?: number; + address?: string; + suburb?: string; + city?: string; + state?: string; + postcode?: string; + latitude?: string; + longitude?: string; + tenantimage?: string; + tenantinfo?: string; + partnerid?: number; + minorder?: number; + /** Misspelled on the wire — `applolcationid`, not `applocationid`. */ + applolcationid?: number; + applocation?: string; + approved?: number; + moduleid?: number; + subcategoryname?: string; + firstname?: string; + lastname?: string; + /** Capitalised on the wire. */ + Accountname?: string; + status: string; +} + +export interface TenantLocation { + locationid: number; + tenantid: number; + applocationid?: number; + moduleid?: number; + roleid?: number; + locationname: string; + email?: string; + contactno?: string; + latitude?: string; + longitude?: string; + address?: string; + suburb?: string; + city?: string; + state?: string; + postcode?: string; + opentime?: string; + closetime?: string; + partnerid?: number; + deliveryradius?: number; + deliverymins?: number; + cancelsecs?: number; + status: string; +} + +/* ──────────────────────────────────────────────────────────────────────────── + Global catalogue — models/catalogue.go (the separate pgvector database) + ──────────────────────────────────────────────────────────────────────────── */ + +export interface CatalogueProduct { + id: number; + brand: string; + product_name: string; + title?: string; + description?: string; + category?: string; + image_id?: string; + images?: string[]; + size?: string; + variant_key?: string; + product_sku?: string; + sku_source?: string; + /** + * A DISPLAY STRING, not a number — e.g. "₹33-37". The catalogue has no exact + * price; `retailprice` always comes from the store owner at import time. + */ + price_range?: string; + providers?: string[]; + fssai_license?: string; + highlights?: string[]; + nutrients?: string[]; +} + +export interface CatalogueBrand { + brand: string; + product_count: number; +} + +/** The composite key. A bare `id` repeats across brand tables — never use it alone. */ +export interface CatalogueRef { + brand: string; + catalogueid: number; +} + +/* ──────────────────────────────────────────────────────────────────────────── + Products & stock — models/product.go + ──────────────────────────────────────────────────────────────────────────── */ + +export interface Product { + productid: number; + applocationid?: number; + productlocationid?: number; + tenantid?: number; + categoryid?: number; + categoryname?: string; + subcategoryid?: number; + /** Capitalised on the wire. */ + Subcategoryname?: string; + catalogueid?: number; + productname?: string; + productimage?: string; + /** A JSON-encoded array of URLs, held as a string. Parse before use. */ + productimages?: string; + productdesc?: string; + productsku?: string; + brandid?: number; + productbrand?: string; + productunit?: string; + unitvalue?: string; + productcost?: number; + taxamount?: number; + taxpercent?: number; + productstock?: number; + quantity?: number; + /** Effective selling price at the scoped location. Read-only, computed. */ + price?: number; + retailprice?: number; + productstatus?: string; + locationstatus?: string; +} + +export interface ProductCategory { + categoryid: number; + categoryname: string; +} + +export interface ProductSubCategory { + subcategoryid: number; + subcategoryname: string; + categoryid?: number; +} + +/** Body of POST /web/products/importcatalogueproduct — send an ARRAY of these. */ +export interface ImportCatalogueProductRequest { + tenantid: number; + locationid: number; + brand: string; + catalogueid: number; + categoryid: number; + subcategoryid: number; + quantity: number; + stocktype: string; + status: string; + retailprice: number; + productcost: number; + taxpercent: number; +} + +/** Body of POST /web/products/createproductlocation — send an ARRAY. Upserts. */ +export interface ProductLocationRequest { + tenantid: number; + locationid: number; + productid: number; + /** Misspelled on the wire — `catlougeid`. */ + catlougeid?: number; + minquantity?: number; + maxquantity?: number; + price?: number; + status?: string; +} + +/** Body of POST /web/products/createproductstock — send an ARRAY. */ +export interface ProductStockRequest { + tenantid: number; + locationid: number; + productid: number; + quantity: number; + stocktype: string; + status: string; +} + +/* ──────────────────────────────────────────────────────────────────────────── + Orders & deliveries — the summary shapes the console reads + ──────────────────────────────────────────────────────────────────────────── */ + +export interface OrderSummary { + totalorders?: number; + delivered?: number; + pending?: number; + cancelled?: number; + revenue?: number; + [key: string]: unknown; +} + +export interface LocationOrderSummary { + locationid?: number; + locationname?: string; + totalorders?: number; + delivered?: number; + cancelled?: number; + revenue?: number; + [key: string]: unknown; +} + +export interface DeliverySummary { + totaldeliveries?: number; + dispatched?: number; + delivered?: number; + [key: string]: unknown; +} + +/* ──────────────────────────────────────────────────────────────────────────── + POS — /v1/pos/* + ──────────────────────────────────────────────────────────────────────────── */ + +/** `GET /pos/sales` — verified against `models/pos.go` `PosSalesPage`. */ +export interface PosSalesPage { + total?: number; + pageno?: number; + pagesize?: number; + bills?: PosSale[]; +} + +/** + * One counter bill, from `models/posorder.go` `PosOrders`. + * + * Two identifiers, and they mean different things. `terminalorderid` is a UUID + * minted at the till, globally unique, and the only thing that safely keys a + * row — delivery is at-least-once, so the same bill legitimately arrives more + * than once. `invoicenumber` is the human-facing one and is unique only per + * terminal: a replaced till restarts its series, so duplicates across terminals + * are expected and gaps are normal. + * + * `billedat` is when the sale was rung, NOT when it reached us. A till that was + * offline all day uploads bills dated yesterday, so every daily figure must use + * this rather than `receivedat` — otherwise a sync outage looks like a bumper + * trading day the moment it clears. + */ +export interface PosSale { + posorderid?: number; + terminalorderid?: string; + invoicenumber?: string; + tenantid?: number; + locationid?: number; + terminalid?: string; + cashiername?: string; + customerid?: number; + customername?: string; + customermobile?: string; + billedat?: string; + businessdate?: string; + subtotal?: number; + discount?: number; + taxamount?: number; + roundoff?: number; + /** What the shopper actually paid — the figure every revenue report sums. */ + total?: number; + itemcount?: number; + /** The largest tender. `paymentsjson` carries the full split. */ + paymentmode?: string; + paymentsjson?: string; + /** Which upload batch carried this bill, for tracing a till's complaint. */ + batchid?: string; + receivedat?: string; +} + +/** + * One order row, from `models/order.go` `OrderInfo`. + * + * A subset — the struct has ~70 fields including pickup/drop geometry and push + * tokens that no list needs. Note that the delivery half lives here too: rider, + * KMS and every timestamp of the journey are columns on the order, which is why + * the old console served orders and deliveries from one read. + * + * `orderheaderid` is the key, NOT `orderid`. The old console's own comment + * records why: "orderid is not unique". + */ +export interface OrderRow { + orderheaderid: number; + orderid?: string; + tenantid?: number; + locationid?: number; + locationname?: string; + orderdate?: string; + deliverydate?: string; + orderstatus?: string; + deliverystatus?: string; + itemcount?: number; + /** The old console reads `quantity` first; not every writer sets itemcount. */ + quantity?: number; + /** + * The order's money, and it moves. + * + * The old console reads `ordervalue || orderamount || deliveryamt` — a + * fallback chain, not indecision. `ordervalue` is on the fuller Orders struct + * (amount + tax + charges − promo), `orderamount` is the goods alone, and + * `deliveryamt` is what a delivery job carries. Which one is populated + * depends on the endpoint the row came through, so all three are typed. + */ + ordervalue?: number; + orderamount?: number; + /** Cash to collect on delivery. Shown only when > 0. */ + collectionamt?: number; + deliverycharge?: number; + deliveryamt?: number; + paymenttype?: number; + paymentstatus?: number; + taxamount?: number; + ordernotes?: string; + /** The delivery REGION — a coarser grouping than the branch. */ + applocation?: string; + /** Set when the shop finished packing. Sits between confirm and pickup. */ + packtime?: string; + starttime?: string; + arrivaltime?: string; + /** Who placed it, and where it is going. */ + deliverycustomer?: string; + deliverycontactno?: string; + deliveryaddress?: string; + deliverysuburb?: string; + pickupcustomer?: string; + pickupcontactno?: string; + pickupaddress?: string; + pickupsuburb?: string; + /** The delivery half. Blank on an order nobody has been assigned to. */ + deliveryid?: number; + rider?: string; + ridercontactno?: string; + riderkms?: string | number; + assigntime?: string; + pickuptime?: string; + deliverytime?: string; + canceltime?: string; +} + +/** + * One till, as it last reported itself. + * + * Every value is a STRING. This is a Redis hash under a TTL, not a table: + * `services/posService.go` types it `[]map[string]string` and hands the hash + * straight back, so numbers arrive as `"14"` and booleans as `"true"`. Typing + * them as numbers here would compile and then silently produce `NaN`, so the + * shape stays honest and the parsing is explicit at the call site. + * + * A terminal that stops refreshing simply disappears from the array — there is + * no reaper job and no stale row claiming "online" after closing time. + */ +export interface PosTerminalHealth { + terminal_id?: string; + location_id?: string; + store_name?: string; + app_version?: string; + /** "online" while refreshing. The MQTT Last Will writes "offline" on power loss. */ + status?: string; + /** Queue depth. The number that matters most — see `models/poshealth.go`. */ + pending_bills?: string; + pending_registrations?: string; + oldest_pending_at?: string; + today_bills?: string; + today_amount?: string; + last_bill_at?: string; + battery_level?: string; + battery_charging?: string; + storage_free_mb?: string; + printer_reachable?: string; + drawer_status?: string; + /** When the till says it wrote this hash — the till's own clock. */ + reported_at?: string; + /** + * When the server received it — the server's clock. + * + * The two disagreeing is itself the signal: a till whose clock is wrong + * writes bills under the wrong business date. See `models/poshealth.go`. + */ + received_at?: string; + /** + * Why the till went offline, present only on an offline record. + * + * On the expired-key stub this is the ONLY field carrying information — + * `{terminal_id, location_id, status, reason}` and nothing else. That + * shape is how a vanished till is told apart from one that declared itself + * offline on its way out; see `isExpiredStub` in `posStatus.ts`. + */ + reason?: string; + [key: string]: string | undefined; +} + +/** `GET /pos/health/location` returns the tills at one outlet, newest state first. */ +export type PosLocationHealth = PosTerminalHealth[]; + +/** `GET /pos/sales/summary` — verified against `models/pos.go` `PosSalesSummary`. */ +export interface PosSalesSummary { + locationid?: number; + fromdate?: string; + todate?: string; + billcount?: number; + itemcount?: number; + grosssales?: number; + taxcollected?: number; + discountgiven?: number; + roundoff?: number; + averagebill?: number; + bypaymentmode?: { paymentmode?: string; amount?: number; billcount?: number }[]; + byday?: { day?: string; amount?: number; billcount?: number }[]; + /** + * Which tills contributed. Note the spelling. + * + * `models/posorder.go` declares `PosTerminalTotal.Terminalid` with the tag + * `json:"terminalid"` — no underscore — while the presence payload uses + * `terminal_id`. The two surfaces disagree, and reading this one with the + * presence spelling silently yields `undefined` for every row, which is + * exactly the bug that made the billed-but-absent reconciliation a no-op. + * + * `terminalid` is also `COALESCE`d to `''` in the GROUP BY, so bills with no + * code collapse into a single empty-id row — see `readTerminalId`. + */ + byterminal?: { terminalid?: string; amount?: number; billcount?: number }[]; +} + +/** + * A store's request for stock, from `models/stockrequest.go`. + * + * Note what is NOT here: reason, requestedby, approvedqty, approvedby, + * approvaldate, remarks. The spec asks for all six; the table has none of them. + * `status` is a free string whose only meaningful value is "Received", which + * posts a stock movement for the FULL `qty` and cannot be overridden. + */ +export interface StockRequest { + requestid: number; + tenantid: number; + tenantname?: string; + locationid: number; + locationname?: string; + productid: number; + productname?: string; + productimage?: string; + qty: number; + status: string; + created?: string; + updated?: string; +} + +/** From `models/product.go` `Productstockstatement` — opening/in/out/closing, nothing finer. */ +export interface StockStatementRow { + productid: number; + productname?: string; + productimage?: string; + productunit?: string; + unitvalue?: string; + retailprice?: number; + locationid?: number; + opening?: number; + credit?: number; + debit?: number; + closing?: number; +} + +/** + * One delivery job, from `models/deliveries.go` `Deliveryinfo`. + * + * A SEPARATE read from the orders list, not a projection of it. The two agree + * on the order id and diverge everywhere else: a delivery carries `ridername` + * (the orders list calls it `rider`), a planned `kms` alongside an `actualkms`, + * and `deliverycharges` — what the rider is paid — alongside `deliveryamt`, + * what the job is worth. None of that exists on the order. + * + * `orderstatus` here walks the DELIVERY ladder, not the order one: + * pending → accepted → arrived → picked → active → delivered, with skipped and + * cancelled off to the side. Colouring it with the order map would be wrong. + * + * Note `Pickupaddress` is capitalised on the wire. That is the Go tag, not a + * typo here — the old console falls back through four fields for the same + * reason, because app-created jobs leave the customer blank but fill the + * address. + */ +export interface DeliveryRow { + deliveryid: number; + orderheaderid?: number; + orderid?: string; + tenantid?: number; + locationid?: number; + locationname?: string; + locationsuburb?: string; + tenantname?: string; + tenantcity?: string; + orderstatus?: string; + deliverydate?: string; + assigntime?: string; + starttime?: string; + arrivaltime?: string; + pickuptime?: string; + deliverytime?: string; + canceltime?: string; + expecteddeliverytime?: string; + itemcount?: number; + orderamount?: number; + pickupcustomer?: string; + pickupcontactno?: string; + /** Capitalised on the wire — see the note above. */ + Pickupaddress?: string; + pickupaddress?: string; + pickuplocation?: string; + pickupsuburb?: string; + deliverycustomer?: string; + deliverycontactno?: string; + deliveryaddress?: string; + deliverylocation?: string; + deliverysuburb?: string; + /** What the rider is paid. Distinct from `deliveryamt`, the job's value. */ + deliverycharges?: number; + deliveryamt?: number; + deliverytype?: string; + notes?: string; + ordernotes?: string; + ridername?: string; + ridercontact?: string; + /** Planned distance. `actualkms`/`riderkms` is what was really ridden. */ + kms?: string; + actualkms?: string; + riderkms?: string; + transitminutes?: number; +} + + +/* ──────────────────────────────────────────────────────────────────────────── + People — back-office staff and till accounts + ──────────────────────────────────────────────────────────────────────────── */ + +/** + * One back-office person, from `GET /tenants/getstaffs` (`models.StaffInfo`). + * + * `password` is DELIBERATELY ABSENT from this type. The endpoint returns it — + * passwords are stored and compared in clear — so declaring it here is all it + * would take for a careless cell to render one. Leaving it off the type means + * the compiler refuses. + * + * `rolename` is resolved server-side and is the only trustworthy label. The + * backend's own comment: "`app_roles` holds six rows for four back-office roles + * and most accounts carry an id absent from it, so any mapping written + * client-side is wrong." + */ +export interface StaffInfo { + userid: number; + rolename?: string; + roleid?: number; + authname?: string; + firstname?: string; + lastname?: string; + fullname?: string; + email?: string; + contactno?: string; + address?: string; + suburb?: string; + city?: string; + state?: string; + postcode?: string; + tenantid?: number; + locationid?: number; + locationname?: string; + status?: string; +} + +/** + * One till account, from `GET /getposusers` (`models.PosUser`). + * + * Note the wire names are snake_case here and camel-ish everywhere else in this + * API — the POS half was written later and to its own convention. + * + * `password` is returned ONLY in the answer to a creation or a reset, never by + * a listing, which is the backend's deliberate shape: "An admin who loses it + * reissues rather than looks it up." We show it once at creation and never + * store or re-read it. `pin` is treated the same way. + */ +export interface PosUser { + user_id: number; + full_name?: string; + first_name?: string; + last_name?: string; + authname?: string; + contactno?: string; + role_id?: number; + role?: string; + has_password?: boolean; + shift_id?: number; + shift_name?: string; + shift_start?: string; + shift_end?: string; + location_id?: number; + status?: string; + /** Present only on a create/reset response. Never on a listing. */ + password?: string; + pin?: string; +} + +/** `GET /posroles` — the source of the role picker. Never hardcode 7 and 8. */ +export interface PosRole { + role_id: number; + /** The lowercase name the create endpoint expects — "supervisor", "cashier". */ + role: string; + label: string; + description?: string; +} + +/** + * A shift window, from `models.StaffShifts`. + * + * `start_time` / `end_time` are wall-clock `HH:MM` text, not instants — they + * repeat, and a date component on them invites exactly the timezone confusion + * that once filed a day of POS bills under the wrong business date. + * + * `weekdays` is a 7-character mask starting Monday: "1111100" is Mon–Fri. + * Empty means every day. + */ +export interface StaffShift { + staff_shift_id: number; + tenantid?: number; + locationid?: number; + name?: string; + start_time?: string; + end_time?: string; + weekdays?: string; + status?: string; +} diff --git a/src/auth/AuthContext.tsx b/src/auth/AuthContext.tsx new file mode 100644 index 0000000..3e6b650 --- /dev/null +++ b/src/auth/AuthContext.tsx @@ -0,0 +1,73 @@ +import { createContext, use, useCallback, useMemo, useState, type ReactNode } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { HOME_ROUTE, type ConsoleRole, type SessionUser } from './roles'; +import { clear, login as loginRequest, persist, restore } from './session'; +import { disableDemo, enableDemo } from '@/demo'; + +interface AuthContextValue { + user: SessionUser | null; + signIn: (email: string, password: string) => Promise; + /** Development only — see `src/demo`. Absent from a production build. */ + signInAsDemo: (session: SessionUser) => void; + signOut: () => void; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(() => restore()); + + const signIn = useCallback(async (email: string, password: string) => { + const session = await loginRequest(email, password); + setUser(session); + return session; + }, []); + + /** + * Seeds a session without touching the API. Demo mode is switched on at the + * same moment, so the fixture backend and the fixture session can never be + * out of step — a demo user paired with live data would be the worst of both. + */ + const signInAsDemo = useCallback((session: SessionUser) => { + if (!import.meta.env.DEV) return; + enableDemo(); + persist(session); + setUser(session); + }, []); + + const signOut = useCallback(() => { + if (import.meta.env.DEV) disableDemo(); + clear(); + setUser(null); + }, []); + + const value = useMemo( + () => ({ user, signIn, signInAsDemo, signOut }), + [user, signIn, signInAsDemo, signOut], + ); + + return {children}; +} + +export function useAuth(): AuthContextValue { + const context = use(AuthContext); + if (!context) throw new Error('useAuth must be used inside '); + return context; +} + +/** + * Route guard. + * + * A role that reaches a workspace it does not own is redirected to its own home + * rather than shown an error — the same partition the old console enforces, and + * the same one the three logins imply. + */ +export function RequireRole({ role, children }: { role: ConsoleRole; children: ReactNode }) { + const { user } = useAuth(); + const location = useLocation(); + + if (!user) return ; + if (user.role !== role) return ; + + return <>{children}; +} diff --git a/src/auth/roles.ts b/src/auth/roles.ts new file mode 100644 index 0000000..11dcd4c --- /dev/null +++ b/src/auth/roles.ts @@ -0,0 +1,88 @@ +/** + * Role resolution. + * + * The backend issues no session token: the login endpoints look the user up and + * return the record. So "signed in" here means "we hold a verified user object", + * and the role is DERIVED from that record rather than asserted by the client. + * + * When the backend does start issuing tokens, this file and `session.ts` are + * the only two that should need to change. + */ + +import type { FiestaUser } from '@/api/types'; + +export type ConsoleRole = 'nearle-admin' | 'store-admin' | 'store-manager'; + +/** + * Roleids that reach the Store Admin workspace. + * + * 7 (Supervisor) and 8 (Cashier) must NEVER appear here: they are till roles, + * and a cashier landing in the tenant console is a privilege escalation, not a + * cosmetic bug. + */ +const STORE_ADMIN_ROLE_IDS: ReadonlySet = new Set([1, 3]); + +/** Till-only roles, listed so the exclusion is explicit rather than implied. */ +export const TILL_ROLE_IDS: ReadonlySet = new Set([7, 8]); + +export interface SessionUser { + userid: number; + role: ConsoleRole; + name: string; + email: string; + roleid: number; + tenantid: number; + locationid: number; + issuperadmin: boolean; +} + +/** + * `issuperadmin` is checked FIRST because it is server-derived. A roleid cannot + * be trusted to imply platform access, so the flag wins over the numeric split. + */ +export function resolveRole(user: Pick): ConsoleRole { + if (user.issuperadmin === true) return 'nearle-admin'; + if (STORE_ADMIN_ROLE_IDS.has(user.roleid)) return 'store-admin'; + return 'store-manager'; +} + +export function toSessionUser(user: FiestaUser): SessionUser { + const name = [user.firstname, user.lastname].filter(Boolean).join(' ').trim(); + return { + userid: user.userid, + role: resolveRole(user), + name: name || user.fullname || user.authname || user.email, + email: user.email, + roleid: user.roleid, + tenantid: user.tenantid, + locationid: user.locationid, + issuperadmin: user.issuperadmin === true, + }; +} + +/** Where each role lands when it has nowhere more specific to go. */ +/** + * Where each role lands. + * + * These MUST be paths that actually resolve. The global `*` route redirects + * here, so a HOME_ROUTE pointing at a path with no matching route sends the + * router straight back to `*`, which sends it here again: an infinite redirect + * that React Router resolves by rendering nothing at all. It fails as a blank + * page with no console error, which is the worst way for a routing bug to + * present. `/admin/dashboard` did exactly that — it was the old console's name + * for the page this one calls Console. + * + * Each workspace also carries its own catch-all in `App.tsx`, so a wrong + * sub-path is absorbed there and never reaches the global one. + */ +export const HOME_ROUTE: Record = { + 'nearle-admin': '/nearle/stores', + 'store-admin': '/admin/console', + 'store-manager': '/store/console', +}; + +export const ROLE_LABEL: Record = { + 'nearle-admin': 'Nearle Admin', + 'store-admin': 'Store Admin', + 'store-manager': 'Store Manager', +}; diff --git a/src/auth/session.ts b/src/auth/session.ts new file mode 100644 index 0000000..4c7e033 --- /dev/null +++ b/src/auth/session.ts @@ -0,0 +1,81 @@ +/** + * Sign-in and session persistence. + * + * There is no token to hold. `TenantWebLogin` returns the user record and + * nothing else, so the session IS that record. It is kept in sessionStorage + * rather than localStorage: a shared back-office machine should not stay signed + * in after the browser closes, and there is no server-side session to revoke. + */ + +import { api, WEB } from '@/api/client'; +import type { FiestaUser } from '@/api/types'; +import { toSessionUser, type SessionUser } from './roles'; + +const STORAGE_KEY = 'nearle.session.v1'; + +/** Thrown when the account exists but has never had a password set. */ +export class PasswordSetupRequiredError extends Error { + readonly userid: number; + constructor(userid: number) { + super('This account needs a password before it can sign in.'); + this.name = 'PasswordSetupRequiredError'; + this.userid = userid; + } +} + +interface LoginBody { + authname: string; + password: string; + roleid?: number; + configid?: number; +} + +/** + * Signs in against the web login endpoint. + * + * The handler answers HTTP 200 with `status: false` for a wrong password, and + * with `code: 409` plus `details.setup` when no password has been set, so the + * envelope is inspected rather than trusting the HTTP status. + */ +export async function login(email: string, password: string): Promise { + const body: LoginBody = { authname: email.trim(), password }; + + const envelope = await api.envelope( + `${WEB}/users/tenant/weblogin`, + { method: 'POST', body }, + ); + + if (envelope.code === 409 && envelope.details?.setup === true) { + throw new PasswordSetupRequiredError(envelope.details.userid ?? 0); + } + + if (envelope.status !== true || !envelope.details) { + throw new Error(envelope.message ?? 'Sign-in failed'); + } + + const session = toSessionUser(envelope.details); + persist(session); + return session; +} + +export function persist(session: SessionUser): void { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(session)); +} + +export function restore(): SessionUser | null { + const raw = sessionStorage.getItem(STORAGE_KEY); + if (!raw) return null; + try { + const parsed = JSON.parse(raw) as SessionUser; + // A stored blob is only as trustworthy as the tab it came from; a shape + // check keeps a corrupted value from crashing the shell on boot. + if (typeof parsed?.userid !== 'number' || typeof parsed?.role !== 'string') return null; + return parsed; + } catch { + return null; + } +} + +export function clear(): void { + sessionStorage.removeItem(STORAGE_KEY); +} diff --git a/src/components/ConsoleSearchBar.tsx b/src/components/ConsoleSearchBar.tsx new file mode 100644 index 0000000..f63a939 --- /dev/null +++ b/src/components/ConsoleSearchBar.tsx @@ -0,0 +1,67 @@ +import { useState } from 'react'; +import { Search } from 'lucide-react'; + +export interface ConsoleSearchBarProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; + style?: React.CSSProperties; +} + +export function ConsoleSearchBar({ + value, + onChange, + placeholder = 'Search', + className, + style, +}: ConsoleSearchBarProps) { + const [isFocused, setIsFocused] = useState(false); + + return ( +
event.preventDefault()} + style={{ + position: 'relative', + display: 'flex', + alignItems: 'center', + width: '100%', + ...style, + }} + > + + onChange(event.target.value)} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + placeholder={placeholder} + aria-label={placeholder} + style={{ + height: 32, + width: '100%', + borderRadius: 12, + border: `1px solid ${isFocused ? 'var(--color-brand)' : 'var(--color-line)'}`, + background: isFocused ? 'var(--color-surface)' : 'var(--color-surface-subtle)', + paddingLeft: 32, + paddingRight: 12, // Removed space for kbd since it's not present here + fontSize: 13, + color: 'var(--color-ink-1)', + outline: 'none', + transition: 'border-color .2s, background .2s', + boxShadow: 'none', + }} + /> + + ); +} diff --git a/src/components/DataState.tsx b/src/components/DataState.tsx new file mode 100644 index 0000000..3cd7dd9 --- /dev/null +++ b/src/components/DataState.tsx @@ -0,0 +1,65 @@ +import type { ReactNode } from 'react'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; +import { Spinner } from '@astryxdesign/core/Spinner'; +import { HStack } from '@astryxdesign/core/HStack'; +import { AlertTriangle, Inbox } from 'lucide-react'; +import { errorMessage } from '@/api/client'; + +export interface DataStateProps { + isLoading: boolean; + error: unknown; + isEmpty: boolean; + /** What is missing, in the user's words — "No tenants yet". */ + emptyTitle: string; + emptyDescription?: string; + emptyAction?: ReactNode; + children: ReactNode; +} + +/** + * The four states every list has, in one place. + * + * Written as a wrapper rather than repeated per screen because the failure mode + * it prevents is a real one: pages that handle loading and success, and render + * a blank rectangle for empty and error. + */ +export function DataState({ + isLoading, + error, + isEmpty, + emptyTitle, + emptyDescription, + emptyAction, + children, +}: DataStateProps) { + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + } + /> + ); + } + + if (isEmpty) { + return ( + } + actions={emptyAction} + /> + ); + } + + return <>{children}; +} diff --git a/src/components/Freshness.tsx b/src/components/Freshness.tsx new file mode 100644 index 0000000..f6c7ef6 --- /dev/null +++ b/src/components/Freshness.tsx @@ -0,0 +1,53 @@ +import { Text } from '@astryxdesign/core/Text'; +import { HStack } from '@astryxdesign/core/HStack'; +import { RefreshCw } from 'lucide-react'; + +export interface FreshnessProps { + /** `dataUpdatedAt` from the query that fed the figure. */ + updatedAt: number | undefined; + isFetching?: boolean; + /** Bills sitting on a till that has not synced. Hidden when zero. */ + unsynced?: number; +} + +function clock(ms: number): string { + return new Date(ms).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); +} + +/** + * How old the number above this line is. + * + * Online orders land as they are placed; counter sales arrive on the console's + * 30-second refetch, and whatever is still sitting on an offline till has not + * arrived at all. So any figure blending the two is eventually consistent, and + * printing one confident number without saying when it was true is how a screen + * loses the room's trust. + */ +export function Freshness({ updatedAt, isFetching, unsynced }: FreshnessProps) { + return ( + + + + + {updatedAt ? `as of ${clock(updatedAt)}` : 'not loaded yet'} + + + + {typeof unsynced === 'number' && unsynced > 0 ? ( + + · {unsynced} bill{unsynced === 1 ? '' : 's'} not yet synced + + ) : null} + + ); +} diff --git a/src/components/KpiCard.tsx b/src/components/KpiCard.tsx new file mode 100644 index 0000000..f3b29c5 --- /dev/null +++ b/src/components/KpiCard.tsx @@ -0,0 +1,86 @@ +import type { ReactNode } from 'react'; +import { Card } from '@astryxdesign/core/Card'; +import { HStack } from '@astryxdesign/core/HStack'; +import { Text } from '@astryxdesign/core/Text'; +import { VStack } from '@astryxdesign/core/VStack'; + +export type KpiTone = 'accent' | 'success' | 'warning' | 'error' | 'neutral'; + +const TONE_COLOR: Record = { + accent: 'var(--color-brand)', + success: 'var(--color-brand)', + warning: 'var(--color-brand)', + error: 'var(--color-brand)', + neutral: 'var(--color-brand)', +}; + +export interface KpiCardProps { + /** Small-caps label. Say what it is, not what it means. */ + label: string; + value: string; + /** + * The sub-pill. This is where the number gets its meaning — "1 of 84 orders" + * says something "1.2%" does not. + */ + note?: string; + tone?: KpiTone; + icon?: ReactNode; +} + +/** + * A KPI tile. + * + * Label and icon, a large tabular value, a muted sub-pill carrying the + * interpretation, and a 2px underline in the tile's tone. Sized to sit 4–6 + * across rather than 4 — a console is glanced at all day, not read once. + */ +export function KpiCard({ label, value, note, tone = 'neutral', icon }: KpiCardProps) { + const color = TONE_COLOR[tone]; + + return ( + + + + + {label} + + {icon ? {icon} : null} + + + + + {value} + + + + {note ? ( + + {note} + + ) : null} + + + + ); +} diff --git a/src/components/PageBody.tsx b/src/components/PageBody.tsx new file mode 100644 index 0000000..f5f65e5 --- /dev/null +++ b/src/components/PageBody.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from 'react'; +import { VStack } from '@astryxdesign/core/VStack'; + +/** + * Two page measures, and the rule for choosing between them. + * + * KROW declares `--container-max: 80rem` for a page and a wider shell measure + * for the app frame, and the distinction matters: a form stretched across + * 1600px puts five inputs in a row and makes the eye travel further than the + * reading task deserves, while a table at 80rem wastes half the screen. + * + * 'reading' — forms, single flows, anything read top to bottom + * 'data' — tables, dashboards, catalogue grids + */ +export type PageMeasure = 'reading' | 'data'; + +const MAX_WIDTH: Record = { + reading: 'var(--container-page, 80rem)', + data: 'var(--container-admin, 102rem)', +}; + +export function PageBody({ + measure = 'data', + children, +}: { + measure?: PageMeasure; + children: ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/components/PageHeader.tsx b/src/components/PageHeader.tsx new file mode 100644 index 0000000..86332c8 --- /dev/null +++ b/src/components/PageHeader.tsx @@ -0,0 +1,109 @@ +import type { ReactNode } from 'react'; + +export interface PageHeaderProps { + title: string; + /** A real count beside the name — "8 on record". Tabular, meta-coloured. */ + count?: string; + /** One line of context. */ + description?: string; + /** Right-aligned actions, wrapping. */ + actions?: ReactNode; + /** An optional tabs row directly under the header rule. */ + tabs?: ReactNode; + isLive?: boolean; +} + +/** + * The page frame header, built to KROW's `AdminPage` spec. + * + * Title + count + a live pill on one line, a one-line subtitle beneath, actions + * right-aligned and wrapping, and — the part that makes pages feel like one + * product — a hairline rule beneath the whole thing at half-opacity, with 16px + * of padding above it. Everything the page stacks below sits on a 24px rhythm. + */ +export function PageHeader({ title, count, description, actions, tabs, isLive }: PageHeaderProps) { + return ( + <> +
+
+
+

+ {title} +

+ + {count ? ( + + {count} + + ) : null} + + {isLive ? ( + + + Live + + ) : null} +
+ + {description ? ( +

+ {description} +

+ ) : null} +
+ + {actions ? ( +
+ {actions} +
+ ) : null} +
+ {tabs} + + ); +} diff --git a/src/components/SectionHeader.tsx b/src/components/SectionHeader.tsx new file mode 100644 index 0000000..b44ea05 --- /dev/null +++ b/src/components/SectionHeader.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from 'react'; +import { Heading } from '@astryxdesign/core/Heading'; +import { Text } from '@astryxdesign/core/Text'; +import { HStack } from '@astryxdesign/core/HStack'; + +export interface SectionHeaderProps { + title: string; + /** + * Sits inline beside the title, not beneath it. + * + * Required rather than optional on purpose: a bare heading tells the reader + * what a block is called but not what it is showing them, and every section + * in this console has something worth saying there. + */ + note: string; + /** A control or a "View all" link, right-aligned. */ + action?: ReactNode; +} + +export function SectionHeader({ title, note, action }: SectionHeaderProps) { + return ( + + + + {title} + + + {note} + + + {action} + + ); +} diff --git a/src/components/shell/AppShell.tsx b/src/components/shell/AppShell.tsx new file mode 100644 index 0000000..ae83b74 --- /dev/null +++ b/src/components/shell/AppShell.tsx @@ -0,0 +1,787 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react'; +import { Link, NavLink, Outlet, useLocation } from 'react-router-dom'; +import { Bell, ChevronDown, ChevronLeft, LogOut, Menu, Search, Sparkles, X } from 'lucide-react'; +import { useIsMobile } from '@/hooks/useIsMobile'; +import { useAuth } from '@/auth/AuthContext'; +import { ROLE_LABEL } from '@/auth/roles'; +import { AssistantPanel } from './AssistantPanel'; + +export interface NavEntry { + to: string; + label: string; +} + +/** One entry in the account menu's MANAGE group. */ +export interface MenuEntry { + to: string; + label: string; + icon: ReactNode; + /** One line under the label, for an entry whose scope is not obvious. */ + note?: string; +} + +export interface AppShellProps { + /** Destinations for the header tabs and the mobile sheet. */ + nav: readonly NavEntry[]; + /** Where the logo links to — the workspace's own landing page. */ + home: string; + /** Accessible name for the tab list, e.g. "Store Admin". */ + navLabel: string; + /** + * An optional control between the logo and the tabs — the Store Admin's + * branch selector lives here. It sits inside the header rather than on each + * page because it scopes every page, and a control that moves between pages + * reads as a different control each time. + */ + scopeControl?: ReactNode; + /** + * Setup destinations, listed inside the account menu rather than in the nav. + * + * This follows the old console's judgement, and its reasoning holds: setup is + * configuration, not a place anyone works from day to day, and a nav slot + * spent on it is a slot taken from a section that IS worked from. The account + * menu is where "my workspace's setup" belongs, beside the account and + * sign-out. + * + * Listed as individual destinations rather than one "Settings" entry for the + * old console's other reason: a single entry lands everyone on the first + * section and makes them click again, and the sections are what people come + * here for. + */ + manageItems?: readonly MenuEntry[]; +} + +/** + * The application chrome, shared by every workspace. + * + * Built to KROW's `AdminLayout` spec. Deliberately NOT a coloured slab: the + * header is a 56px white-at-85% bar with a backdrop blur and a hairline bottom + * border, carrying text tabs whose active state is accent-coloured text plus a + * 2px accent bar sitting on that border. The repo's own reasoning for text tabs + * over pills: several destinations means several competing shapes if each one + * is a pill, and a console header should recede rather than compete with the + * page. + * + * One component rather than one per workspace. The workspaces differ in exactly + * three things — their destinations, their home, and whether they have a scope + * control — so those are props. Copying four hundred lines of chrome per role + * is how two headers drift apart and stop looking like one product. + */ +export function AppShell({ nav, home, navLabel, scopeControl, manageItems }: AppShellProps) { + const { user, signOut } = useAuth(); + const { pathname } = useLocation(); + + const [query, setQuery] = useState(''); + const [isSearchFocused, setIsSearchFocused] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isAssistantOpen, setIsAssistantOpen] = useState(true); + const [isNavOpen, setIsNavOpen] = useState(false); + const isMobile = useIsMobile(); + const searchRef = useRef(null); + const menuRef = useRef(null); + + // ⌘K / Ctrl-K focuses search, the same global shortcut the reference uses. + useEffect(() => { + function onKeyDown(event: KeyboardEvent) { + if (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + searchRef.current?.focus(); + } + } + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, []); + + const isSearchWide = isSearchFocused || query !== ''; + + // Escape closes the account menu. A menu that can only be dismissed by + // finding the trigger again is a trap for anyone on a keyboard, and this one + // sits over the page rather than beside it. + useEffect(() => { + if (!isMenuOpen) return; + function onKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') setIsMenuOpen(false); + } + function onMouseDown(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsMenuOpen(false); + } + } + window.addEventListener('keydown', onKeyDown); + document.addEventListener('mousedown', onMouseDown); + return () => { + window.removeEventListener('keydown', onKeyDown); + document.removeEventListener('mousedown', onMouseDown); + }; + }, [isMenuOpen]); + + return ( +
+ {/* Two elements, on purpose. The bar is full-bleed — the glass, the blur + and the hairline run the whole width of the screen, because a header + that stops short of the edge reads as a floating card, not as chrome. + The ROW inside it is capped by `.app-gutter`, so the logo and nav sit + on exactly the same left edge as the page title below them at every + width, including a 2560px monitor where the body is centred. */} +
+
+ + {/* 24px tall, width auto — the reference pins the logo's height and + lets the wordmark set its own width. */} + Nearle + + + {/* Below md the scope control moves into the navigation sheet. On a + 390px phone the logo, the selector and the right-hand cluster add + up to 503px and push the header 113px past the viewport, so the + page scrolls sideways. The sheet gives the branch names room to be + read in full, and every page states its own scope in the header + line beneath, so the phone never leaves the operator guessing. */} + {scopeControl ? ( +
+ {scopeControl} +
+ ) : null} + + {/* Below lg the tabs are gone, so a flexible spacer keeps the right + cluster against the edge instead of bunched beside the logo. */} +
+ + {/* Text tabs. Active = accent text + a 2px accent bar on the header rule. */} + + +
+ {/* Search: 176px at rest, 256px once focused or filled. */} +
event.preventDefault()} + style={{ + position: 'relative', + alignItems: 'center', + width: isSearchWide ? 256 : 176, + transition: 'width .2s cubic-bezier(.16,1,.3,1)', + }} + > + + setQuery(event.target.value)} + onFocus={() => setIsSearchFocused(true)} + onBlur={() => setIsSearchFocused(false)} + placeholder="Search" + aria-label="Search the console" + style={{ + height: 32, + width: '100%', + borderRadius: 12, + border: `1px solid ${isSearchFocused ? 'var(--color-brand)' : 'var(--color-line)'}`, + background: isSearchFocused + ? 'var(--color-surface)' + : 'var(--color-surface-subtle)', + paddingLeft: 32, + paddingRight: 40, + fontSize: 13, + color: 'var(--color-ink-1)', + outline: 'none', + transition: 'border-color .2s, background .2s', + boxShadow: isSearchFocused ? '0 0 0 2px rgba(102,37,130,.15)' : 'none', + }} + /> + + ⌘K + + + + + + + + {/* Assistant button moved to floating pill */} + + {/* Avatar trigger + chevron, with the account menu below it. */} +
+ + + {isMenuOpen ? ( +
+
+
+ {user?.name} +
+
{user?.email}
+
+ + {user ? ROLE_LABEL[user.role] : ''} + +
+
+ {manageItems && manageItems.length > 0 ? ( + <> + +

+ Manage +

+ {manageItems.map((entry) => ( + setIsMenuOpen(false)} + /> + ))} + + ) : null} + + + + + {/* When this code was built. If it does not match what you + were told was delivered, you are looking at an old copy — + which is not something any screen otherwise reveals. */} +

+ Build {__BUILD_STAMP__} +

+
+ ) : null} +
+ + {/* Wrapped rather than styled inline: an inline `display` would win + over the media query that hides this above the tabs breakpoint. */} + + setIsNavOpen(true)}> + + + +
+
+
+ + {/* Body: a column on a phone so the assistant stacks under the page, a + row from md where it becomes a side column. */} +
+
+ +
+ + {isAssistantOpen || isMobile ? ( + setIsAssistantOpen(false)} isStacked={isMobile} /> + ) : null} +
+ + {/* Floating Assistant Button */} + {!isMobile && !isAssistantOpen ? ( + + ) : null} + + {isNavOpen ? ( + setIsNavOpen(false)} + pathname={pathname} + /> + ) : null} +
+ ); +} + +/** + * The navigation sheet, below lg. + * + * 288px from the right, opaque rather than glass — a translucent sheet over a + * page of tables is unreadable — with the active item carried by a tinted fill + * instead of the 2px underline, which has nothing to sit on here. + */ +function MobileNav({ + nav, + scopeControl, + onClose, + pathname, +}: { + nav: readonly NavEntry[]; + scopeControl?: ReactNode; + onClose: () => void; + pathname: string; +}) { + return ( +
+
+ ); +} + +/** + * One destination in the account menu. + * + * A `NavLink` rather than a button with `navigate()`: middle-click, ⌘-click and + * "open in new tab" all work on a real anchor and none of them work on a + * button, and a setup screen is exactly the kind of thing someone parks in a + * second tab. + */ +function MenuLink({ entry, onNavigate }: { entry: MenuEntry; onNavigate: () => void }) { + const [isHovered, setIsHovered] = useState(false); + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + style={{ + display: 'flex', + alignItems: 'center', + gap: 10, + padding: '8px 12px', + borderRadius: 12, + textDecoration: 'none', + background: isHovered ? 'var(--color-surface-sunken)' : 'transparent', + color: 'var(--color-ink-1)', + transition: 'background .15s', + }} + > + + {entry.icon} + + + {entry.label} + {entry.note ? ( + + {entry.note} + + ) : null} + + + ); +} + +const Rule = () => ( +
+); + +function initials(name: string): string { + const parts = name.trim().split(/\s+/).filter(Boolean); + if (parts.length === 0) return '—'; + const first = parts[0]?.[0] ?? ''; + const last = parts.length > 1 ? (parts[parts.length - 1]?.[0] ?? '') : ''; + return (first + last).toUpperCase(); +} + +/** + * A 32px icon-only control. + * + * `label` is required, not optional — an icon-only control with no accessible + * name is a bug, so the API makes it impossible to omit. + */ +function IconButton({ + label, + children, + onClick, + hasUnread, +}: { + label: string; + children: ReactNode; + onClick?: () => void; + hasUnread?: boolean; +}) { + const [isHovered, setIsHovered] = useState(false); + return ( + + ); +} diff --git a/src/components/shell/AssistantPanel.tsx b/src/components/shell/AssistantPanel.tsx new file mode 100644 index 0000000..20decc1 --- /dev/null +++ b/src/components/shell/AssistantPanel.tsx @@ -0,0 +1,414 @@ +import { useRef, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { ArrowUp, History, Maximize2, Minimize2, PanelRightClose } from 'lucide-react'; + +/** Per-route context, so the panel knows which page it is sitting beside. */ +const CONTEXT: Record< + string, + { page: string; title: string; greeting: string; reading: string; prompts: string[] } +> = { + '/nearle/stores': { + page: 'Stores', + title: 'Good afternoon', + greeting: 'Every tenant on the platform, and which of them have branches sitting idle.', + reading: 'Reading the tenant directory — branch counts, status and per-tenant performance.', + prompts: [ + 'Which tenants have no branches?', + 'Who onboarded most recently?', + 'What needs attention?', + 'Summarise platform growth', + ], + }, + '/nearle/onboard/tenant': { + page: 'Onboard tenant', + title: 'Provisioning a tenant', + greeting: 'Registering the enterprise also creates its primary Administrator account.', + reading: 'Reading the onboarding form — required fields, category and head-office address.', + prompts: ['What does provisioning create?', 'Which fields are required?', 'What happens next?'], + }, + '/nearle/catalogue': { + page: 'Global catalogue', + title: 'Stocking a store', + greeting: 'The catalogue carries a price range, not a price — the store sets the real one.', + reading: 'Reading the global catalogue and what this tenant has already imported.', + prompts: [ + 'Which products are already imported?', + 'Catalogue or spreadsheet?', + 'What does re-importing do?', + 'Which columns does the sheet need?', + ], + }, + + /* ── Store Admin ──────────────────────────────────────────────────────── */ + + '/admin/console': { + page: 'Console', + title: 'Across your branches', + greeting: 'App sales, counter sales and imported bills are counted separately — they live in different ledgers.', + reading: 'Reading every branch — revenue by channel, stock health, till status and what is waiting on you.', + prompts: [ + 'Which branch is underperforming?', + 'Any tills not syncing?', + 'What needs my approval?', + 'Where is stock running out?', + ], + }, + '/admin/sales': { + page: 'Sales', + title: 'Orders and deliveries', + greeting: 'An app order and a counter bill are both sales, but only one of them has a delivery.', + reading: 'Reading orders, counter bills and delivery progress across your branches.', + prompts: [ + 'Why is the cancel rate high?', + 'Which orders are stuck?', + 'Compare online and counter sales', + 'What is out for delivery?', + ], + }, + '/admin/inventory': { + page: 'Inventory', + title: 'Catalogue and stock', + greeting: 'A product in the store catalogue does not mean stock on the shelf — that takes an approved request.', + reading: 'Reading your catalogue, per-branch stock levels and the stock requests waiting on you.', + prompts: [ + 'What is waiting for approval?', + 'Which products are unpublished?', + 'What is low or out of stock?', + 'How do I upload a product sheet?', + ], + }, + '/admin/users': { + page: 'Users & access', + title: 'Who can sign in', + greeting: 'A till account and a console login are two different things — a cashier has no console access at all.', + reading: 'Reading your back-office directory and the till accounts at each branch.', + prompts: [ + 'What is the difference between the two?', + 'How do I add a cashier?', + 'Why can I not delete someone?', + 'Who is inactive?', + ], + }, + '/admin/reports': { + page: 'Reports', + title: 'Revenue, sales and stock', + greeting: 'Fast and slow movers are the two lists that change what you order next.', + reading: 'Reading revenue by channel and branch, product performance and stock movement.', + prompts: [ + 'Which products are slow moving?', + 'Compare my three branches', + 'Online versus counter revenue', + 'What is my inventory worth?', + ], + }, +}; + +const FALLBACK = { + page: 'Console', + title: 'Good afternoon', + greeting: 'Ask about anything on this page.', + reading: 'Reading this page.', + prompts: ['What needs attention?', 'Summarise this page'], +}; + +/** + * Nearle Buddy, as a layout column. + * + * Not an overlay and not a floating bubble: the panel is part of the page on + * every supported route, so the page reflows beside it rather than being + * covered. It opens by stating the most useful thing about the page it is next + * to, says what it read, and offers prompts scoped to that page. + * + * 380px by default, sticky under the 56px header, and its own height is the + * viewport minus 104px so it never pushes the page taller than the screen. + */ +export function AssistantPanel({ + onClose, + isStacked = false, +}: { + onClose: () => void; + /** Below md the panel sits under the page rather than beside it. */ + isStacked?: boolean; +}) { + const { pathname } = useLocation(); + const [draft, setDraft] = useState(''); + const [isFocused, setIsFocused] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + const textareaRef = useRef(null); + + const key = Object.keys(CONTEXT).find((entry) => pathname.startsWith(entry)); + const context = (key ? CONTEXT[key] : undefined) ?? FALLBACK; + + const expandedStyle: React.CSSProperties = isExpanded + ? { + width: '50vw', + maxWidth: 'none', + } + : {}; + + return ( +