From e0c6fe04ab7cbf7a3e21b3aaacc57055f0cb5935 Mon Sep 17 00:00:00 2001 From: "hieu.cao" Date: Sat, 25 Jul 2026 10:12:47 +0700 Subject: [PATCH 1/9] Problem 1: three unique sum_to_n implementations (loop, Gauss, recursion) --- src/problem1/.keep | 0 src/problem1/README.md | 57 +++++++++++++++++++++++++++++ src/problem1/sum_to_n.js | 79 ++++++++++++++++++++++++++++++++++++++++ src/problem3/.keep | 0 4 files changed, 136 insertions(+) delete mode 100644 src/problem1/.keep create mode 100644 src/problem1/README.md create mode 100644 src/problem1/sum_to_n.js delete mode 100644 src/problem3/.keep diff --git a/src/problem1/.keep b/src/problem1/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem1/README.md b/src/problem1/README.md new file mode 100644 index 0000000000..a0eff6a41a --- /dev/null +++ b/src/problem1/README.md @@ -0,0 +1,57 @@ +# Problem 1 — `sum_to_n` + +Three unique implementations of a function that returns the summation to `n`, +e.g. `sum_to_n(5) === 1 + 2 + 3 + 4 + 5 === 15`. + +Input `n` is any integer; the result is assumed to stay below +`Number.MAX_SAFE_INTEGER`. + +## Run + +```bash +node sum_to_n.js +``` + +A clean run prints `all checks passed`. The file contains `console.assert` +sanity checks that stay silent on success and log a message on failure. + +## Negative-`n` convention + +Since `n` may be any integer, the three implementations agree on the following +consistent behaviour: + +| `n` | result | meaning | +| ---- | ------ | ------------------------------------ | +| `5` | `15` | sum of `1..5` | +| `0` | `0` | empty sum | +| `-3` | `-6` | sum of `-3, -2, -1` (i.e. `n..-1`) | + +## The three approaches + +| Fn | Approach | Time | Space | +| -------------- | ------------------------------- | ------ | --------------- | +| `sum_to_n_a` | Iterative `for` loop | `O(n)` | `O(1)` | +| `sum_to_n_b` | Closed-form Gauss formula | `O(1)` | `O(1)` | +| `sum_to_n_c` | Recursion (functional style) | `O(n)` | `O(n)` stack | + +### a) Iterative loop +Accumulates a running total, walking `1..n` for positive `n` or `n..-1` for +negative `n`. + +### b) Closed-form (Gauss) +Uses the triangular-number identity. Important detail: the bare +`n*(n+1)/2` does **not** satisfy the negative convention — for `n = -3` it +returns `3`, not `-6`. The sum of `n..-1` for `n < 0` equals the triangular +number of `|n|`, negated. The implementation therefore uses the sign-aware +closed form: + +``` +sign(n) * |n| * (|n| + 1) / 2 +``` + +which gives `15` for `n=5`, `0` for `n=0`, and `-6` for `n=-3`, all in constant +time. + +### c) Recursion +Peels one term off toward `0` per call: `f(n) = n + f(n ∓ 1)`. Clear and +functional, but uses `O(n)` call-stack space. diff --git a/src/problem1/sum_to_n.js b/src/problem1/sum_to_n.js new file mode 100644 index 0000000000..faba24cdf3 --- /dev/null +++ b/src/problem1/sum_to_n.js @@ -0,0 +1,79 @@ +/** + * Problem 1: Three unique implementations of `sum_to_n`. + * + * Task: + * Input: n - any integer (result assumed < Number.MAX_SAFE_INTEGER). + * Output: summation to n, e.g. sum_to_n(5) === 1 + 2 + 3 + 4 + 5 === 15. + * + * Negative-n convention (applied CONSISTENTLY across all three): + * - sum_to_n(0) === 0 + * - For n > 0: sum of 1..n -> sum_to_n(5) === 15 + * - For n < 0: sum of n..-1 -> sum_to_n(-3) === (-3)+(-2)+(-1) === -6 + * All three implementations honour this convention. Note the closed form + * needs a sign-aware variant (see sum_to_n_b) because the bare n*(n+1)/2 + * does not produce -6 for n = -3. + * + * Complexity summary: + * a) iterative for-loop : O(n) time, O(1) space + * b) closed-form (Gauss): O(1) time, O(1) space + * c) recursion : O(n) time, O(n) stack space + */ + +// a) Iterative for-loop. +// Walks either 1..n (n > 0) or n..-1 (n < 0), accumulating a running sum. +// O(n) time, O(1) space. +var sum_to_n_a = function (n) { + let sum = 0; + if (n >= 0) { + for (let i = 1; i <= n; i++) sum += i; + } else { + for (let i = n; i <= -1; i++) sum += i; + } + return sum; +}; + +// b) Closed-form Gauss formula. +// For n >= 0 this is the classic triangular number n*(n+1)/2. +// NOTE: the *plain* formula n*(n+1)/2 does NOT match our negative +// convention -- for n = -3 it gives (-3)*(-2)/2 = 3, but we want -6. +// The convention "sum of n..-1" for n < 0 equals -(|n|*(|n|+1)/2), i.e. +// the same triangular number negated. So the sign-aware closed form is: +// sign(n) * |n| * (|n| + 1) / 2 +// which yields 15 for n=5, 0 for n=0, and -6 for n=-3. O(1) time & space. +var sum_to_n_b = function (n) { + const m = Math.abs(n); + return Math.sign(n) * (m * (m + 1)) / 2; +}; + +// c) Recursive / functional approach. +// Peels one term off toward 0 each call: f(n) = n + f(n -/+ 1). +// O(n) time, O(n) call-stack space. +var sum_to_n_c = function (n) { + if (n === 0) return 0; + if (n > 0) return n + sum_to_n_c(n - 1); + return n + sum_to_n_c(n + 1); +}; + +// --- Sanity checks (runnable with `node sum_to_n.js`) -------------------- +const cases = [ + [5, 15], + [1, 1], + [0, 0], + [10, 55], + [100, 5050], + [-1, -1], + [-3, -6], + [-100, -5050], +]; + +for (const [n, expected] of cases) { + console.assert(sum_to_n_a(n) === expected, `a(${n}) => ${sum_to_n_a(n)}, want ${expected}`); + console.assert(sum_to_n_b(n) === expected, `b(${n}) => ${sum_to_n_b(n)}, want ${expected}`); + console.assert(sum_to_n_c(n) === expected, `c(${n}) => ${sum_to_n_c(n)}, want ${expected}`); + console.assert( + sum_to_n_a(n) === sum_to_n_b(n) && sum_to_n_b(n) === sum_to_n_c(n), + `disagreement at n=${n}` + ); +} + +console.log("all checks passed"); diff --git a/src/problem3/.keep b/src/problem3/.keep deleted file mode 100644 index e69de29bb2..0000000000 From a74a7e9e60f8706eb9682bc90d5cb11a4eb81171 Mon Sep 17 00:00:00 2001 From: "hieu.cao" Date: Sat, 25 Jul 2026 10:12:47 +0700 Subject: [PATCH 2/9] Problem 2: Fancy Form - Vite + React + TS + Tailwind currency swap Searchable token selectors with Switcheo icons, live prices.json rates, input validation, swap-direction flip, and a mocked loading->success submit. --- src/problem2/.gitignore | 25 + src/problem2/README.md | 82 + src/problem2/index.html | 44 +- src/problem2/package-lock.json | 2680 +++++++++++++++++ src/problem2/package.json | 25 + src/problem2/postcss.config.js | 6 + src/problem2/public/vite.svg | 10 + src/problem2/script.js | 0 src/problem2/src/App.tsx | 64 + src/problem2/src/components/SwapForm.tsx | 228 ++ src/problem2/src/components/Toast.tsx | 43 + src/problem2/src/components/TokenField.tsx | 97 + src/problem2/src/components/TokenIcon.tsx | 50 + .../src/components/TokenSelectModal.tsx | 136 + src/problem2/src/data/fallbackPrices.ts | 43 + src/problem2/src/hooks/usePrices.ts | 55 + src/problem2/src/index.css | 51 + src/problem2/src/main.tsx | 10 + src/problem2/src/types.ts | 18 + src/problem2/src/utils/format.ts | 46 + src/problem2/src/utils/swap.ts | 60 + src/problem2/src/vite-env.d.ts | 1 + src/problem2/style.css | 8 - src/problem2/tailwind.config.js | 34 + src/problem2/tsconfig.app.json | 22 + src/problem2/tsconfig.json | 7 + src/problem2/tsconfig.node.json | 20 + src/problem2/vite.config.ts | 7 + 28 files changed, 3838 insertions(+), 34 deletions(-) create mode 100644 src/problem2/.gitignore create mode 100644 src/problem2/README.md create mode 100644 src/problem2/package-lock.json create mode 100644 src/problem2/package.json create mode 100644 src/problem2/postcss.config.js create mode 100644 src/problem2/public/vite.svg delete mode 100644 src/problem2/script.js create mode 100644 src/problem2/src/App.tsx create mode 100644 src/problem2/src/components/SwapForm.tsx create mode 100644 src/problem2/src/components/Toast.tsx create mode 100644 src/problem2/src/components/TokenField.tsx create mode 100644 src/problem2/src/components/TokenIcon.tsx create mode 100644 src/problem2/src/components/TokenSelectModal.tsx create mode 100644 src/problem2/src/data/fallbackPrices.ts create mode 100644 src/problem2/src/hooks/usePrices.ts create mode 100644 src/problem2/src/index.css create mode 100644 src/problem2/src/main.tsx create mode 100644 src/problem2/src/types.ts create mode 100644 src/problem2/src/utils/format.ts create mode 100644 src/problem2/src/utils/swap.ts create mode 100644 src/problem2/src/vite-env.d.ts delete mode 100644 src/problem2/style.css create mode 100644 src/problem2/tailwind.config.js create mode 100644 src/problem2/tsconfig.app.json create mode 100644 src/problem2/tsconfig.json create mode 100644 src/problem2/tsconfig.node.json create mode 100644 src/problem2/vite.config.ts diff --git a/src/problem2/.gitignore b/src/problem2/.gitignore new file mode 100644 index 0000000000..b6cdf575d3 --- /dev/null +++ b/src/problem2/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +*.tsbuildinfo + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/problem2/README.md b/src/problem2/README.md new file mode 100644 index 0000000000..5160b1bd79 --- /dev/null +++ b/src/problem2/README.md @@ -0,0 +1,82 @@ +# Problem 2 — Currency Swap Form + +A polished, interactive currency swap form. A user picks a token to pay with, a +token to receive, enters an amount, and the form computes the exchange in real +time from live token prices. Submitting runs a mocked backend round-trip with a +loading state and a success toast. + +## Features + +- **Live exchange rates** derived from real token prices, with a `1 X = n Y` + rate line and USD value hints on both sides. +- **Searchable token picker** — a modal dialog listing every priced token with + its icon, USD price, and (mocked) wallet balance; filter by ticker. +- **Swap-direction button** that flips the two tokens and carries the amount + over for a seamless reverse quote. +- **MAX button** to fill the full wallet balance. +- **Inline validation** covering empty / non-numeric / zero / negative amounts, + identical from/to tokens, and amounts exceeding the mocked wallet balance. + The submit button is disabled while the form is invalid. +- **Mocked submit** — `CONFIRM SWAP` shows a spinner for ~1.5s, then a success + toast summarizing the trade. No real transaction is made. +- **Graceful token icons** — remote SVGs with a generated monogram fallback + when an icon is missing. +- **Resilient data loading** — if the live prices endpoint is unreachable the + app falls back to a bundled snapshot, so it always renders (offline / CI safe). +- Responsive layout, dark theme, subtle animations, and keyboard-accessible + controls (focus rings, `Esc` to close the picker, focus-on-open search). + +## Tech stack + +- **Vite** (bonus points) + **React 18** + **TypeScript** +- **Tailwind CSS v3** for styling +- No component library — all UI is hand-built and self-contained. + +## Getting started + +```bash +npm install +npm run dev # start the dev server (Vite prints the local URL) +npm run build # type-check + production build into dist/ +npm run preview # preview the production build +``` + +Requires Node 18+ (developed on Node 20). + +## Data sources + +- **Prices:** `https://interview.switcheo.com/prices.json` — an array of + `{ currency, date, price }`. On load the app dedupes by currency (keeping the + latest `date`) and keeps only tokens that have a positive price. +- **Icons:** `https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens/{SYMBOL}.svg`. + +## Notes on mocking + +- **Wallet balances** are mocked deterministically from each token's symbol and + price (roughly \$500–\$10,000 of holdings per token) so validation against a + balance is demonstrable. +- **The swap submission** is mocked with a `setTimeout` delay — there is no + backend and no on-chain activity. +- A **bundled fallback price list** (`src/data/fallbackPrices.ts`) is a snapshot + of the live endpoint, used only when the network request fails. + +## Project structure + +``` +src/ + components/ + SwapForm.tsx # form state, validation, submit flow + TokenField.tsx # one "pay"/"receive" row (amount + token trigger) + TokenSelectModal.tsx # searchable token picker dialog + TokenIcon.tsx # remote icon with monogram fallback + Toast.tsx # success toast + hooks/ + usePrices.ts # fetch + fallback, returns tradable tokens + utils/ + swap.ts # token building, rate math, conversion + format.ts # number / currency formatting + data/ + fallbackPrices.ts # offline snapshot of prices.json + types.ts # shared types + App.tsx # layout, loading skeleton, toast wiring +``` diff --git a/src/problem2/index.html b/src/problem2/index.html index 4058a68bff..c0de8a47e0 100644 --- a/src/problem2/index.html +++ b/src/problem2/index.html @@ -1,27 +1,19 @@ - - - - - Fancy Form - - - - - - - - -
-
Swap
- - - - - - - -
- - - + + + + + + + Swap · Currency Exchange + + + + + +
+ + diff --git a/src/problem2/package-lock.json b/src/problem2/package-lock.json new file mode 100644 index 0000000000..2c2fb81d4c --- /dev/null +++ b/src/problem2/package-lock.json @@ -0,0 +1,2680 @@ +{ + "name": "problem2-currency-swap", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "problem2-currency-swap", + "version": "1.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@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/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", + "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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/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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/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/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "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/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "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" + } + } +} diff --git a/src/problem2/package.json b/src/problem2/package.json new file mode 100644 index 0000000000..cfe23a32f4 --- /dev/null +++ b/src/problem2/package.json @@ -0,0 +1,25 @@ +{ + "name": "problem2-currency-swap", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } +} diff --git a/src/problem2/postcss.config.js b/src/problem2/postcss.config.js new file mode 100644 index 0000000000..2e7af2b7f1 --- /dev/null +++ b/src/problem2/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/problem2/public/vite.svg b/src/problem2/public/vite.svg new file mode 100644 index 0000000000..90fb737154 --- /dev/null +++ b/src/problem2/public/vite.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/problem2/script.js b/src/problem2/script.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem2/src/App.tsx b/src/problem2/src/App.tsx new file mode 100644 index 0000000000..a8e996c06e --- /dev/null +++ b/src/problem2/src/App.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react' +import { usePrices } from './hooks/usePrices' +import { SwapForm } from './components/SwapForm' +import { Toast } from './components/Toast' +import { formatTokenAmount } from './utils/format' + +interface ToastState { + message: string + detail: string +} + +export default function App() { + const { tokens, loading, usingFallback } = usePrices() + const [toast, setToast] = useState(null) + + return ( +
+
+ {loading ? ( + + ) : ( + + setToast({ + message: 'Swap confirmed', + detail: `${formatTokenAmount(s.fromAmount)} ${s.from} → ${formatTokenAmount( + s.toAmount, + )} ${s.to}`, + }) + } + /> + )} + + {usingFallback && !loading && ( +

+ Live prices are unreachable — showing a bundled snapshot so you can still explore the form. +

+ )} + +
+ Prices from interview.switcheo.com · Demo only, no real transactions +
+
+ + {toast && ( + setToast(null)} /> + )} +
+ ) +} + +/** Loading placeholder shown while prices are being fetched. */ +function FormSkeleton() { + return ( +
+
+
+
+
+
+
+ ) +} diff --git a/src/problem2/src/components/SwapForm.tsx b/src/problem2/src/components/SwapForm.tsx new file mode 100644 index 0000000000..e0663e851b --- /dev/null +++ b/src/problem2/src/components/SwapForm.tsx @@ -0,0 +1,228 @@ +import { useEffect, useMemo, useState } from 'react' +import type { Token } from '../types' +import { TokenField } from './TokenField' +import { TokenSelectModal } from './TokenSelectModal' +import { convert, exchangeRate } from '../utils/swap' +import { formatTokenAmount, formatUsd, parseAmount } from '../utils/format' + +interface SwapFormProps { + tokens: Token[] + onSuccess: (summary: { fromAmount: number; from: string; toAmount: number; to: string }) => void +} + +type ActiveSide = 'from' | 'to' | null + +/** Pick a sensible default token by symbol, falling back to an index. */ +function pick(tokens: Token[], symbol: string, fallbackIndex: number): Token { + return tokens.find((t) => t.symbol === symbol) ?? tokens[fallbackIndex] ?? tokens[0] +} + +/** The complete currency swap form: inputs, validation, and mocked submit. */ +export function SwapForm({ tokens, onSuccess }: SwapFormProps) { + const [fromToken, setFromToken] = useState(() => pick(tokens, 'ETH', 0)) + const [toToken, setToToken] = useState(() => pick(tokens, 'USDC', 1)) + const [amount, setAmount] = useState('') + const [activeSide, setActiveSide] = useState(null) + const [touched, setTouched] = useState(false) + const [submitting, setSubmitting] = useState(false) + + // If the token list identity changes (e.g. live data replaces fallback), + // re-resolve the selected tokens so prices/balances stay in sync. + useEffect(() => { + setFromToken((prev) => pick(tokens, prev.symbol, 0)) + setToToken((prev) => pick(tokens, prev.symbol, 1)) + }, [tokens]) + + const parsedAmount = parseAmount(amount) + const rate = exchangeRate(fromToken, toToken) + const receiveAmount = convert(parsedAmount, fromToken, toToken) + + const fromUsd = Number.isFinite(parsedAmount) ? parsedAmount * fromToken.price : NaN + const toUsd = Number.isFinite(receiveAmount) ? receiveAmount * toToken.price : NaN + + const error = useMemo(() => { + if (amount.trim() === '') return 'Enter an amount to swap.' + if (Number.isNaN(parsedAmount)) return 'Enter a valid number.' + if (parsedAmount <= 0) return 'Amount must be greater than zero.' + if (fromToken.symbol === toToken.symbol) return 'Choose two different tokens.' + if (parsedAmount > fromToken.balance) + return `Insufficient balance — you hold ${formatTokenAmount(fromToken.balance)} ${fromToken.symbol}.` + return null + }, [amount, parsedAmount, fromToken, toToken]) + + const showError = touched && error + + function handleSwapDirection() { + setFromToken(toToken) + setToToken(fromToken) + // Carry the computed receive amount over as the new pay amount for continuity. + if (Number.isFinite(receiveAmount) && parsedAmount > 0) { + setAmount(String(Number(receiveAmount.toFixed(8)))) + } + } + + function handleSelect(token: Token) { + if (activeSide === 'from') { + // Prevent selecting the same token as the other side; swap instead. + if (token.symbol === toToken.symbol) setToToken(fromToken) + setFromToken(token) + } else if (activeSide === 'to') { + if (token.symbol === fromToken.symbol) setFromToken(toToken) + setToToken(token) + } + setActiveSide(null) + } + + function handleMax() { + setTouched(true) + setAmount(String(Number(fromToken.balance.toFixed(8)))) + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setTouched(true) + if (error || submitting) return + setSubmitting(true) + // Mocked backend round-trip. + await new Promise((resolve) => setTimeout(resolve, 1500)) + setSubmitting(false) + onSuccess({ + fromAmount: parsedAmount, + from: fromToken.symbol, + toAmount: receiveAmount, + to: toToken.symbol, + }) + setAmount('') + setTouched(false) + } + + return ( +
+
+
+

Swap

+

Trade tokens in an instant

+
+ + + Live rates + +
+ +
+ { + setTouched(true) + setAmount(v) + }} + onOpenSelect={() => setActiveSide('from')} + onMax={handleMax} + error={showError ? error : undefined} + /> + + {/* Swap-direction button, overlapping the two fields */} +
+ +
+ + 0 && Number.isFinite(receiveAmount) + ? formatTokenAmount(receiveAmount) + : '' + } + readOnly + usdValue={toUsd} + onOpenSelect={() => setActiveSide('to')} + /> +
+ + {/* Exchange-rate line */} +
+ Rate + + 1 {fromToken.symbol} = {formatTokenAmount(rate)} {toToken.symbol} + +
+ + {/* Inline validation message */} +
+ {showError && ( +

+ + + + + {error} +

+ )} +
+ + + + {parsedAmount > 0 && !error && ( +

+ You will receive ≈ {formatTokenAmount(receiveAmount)} {toToken.symbol} ({formatUsd(toUsd)}) +

+ )} + + setActiveSide(null)} + /> + + ) +} diff --git a/src/problem2/src/components/Toast.tsx b/src/problem2/src/components/Toast.tsx new file mode 100644 index 0000000000..b71b9758be --- /dev/null +++ b/src/problem2/src/components/Toast.tsx @@ -0,0 +1,43 @@ +import { useEffect } from 'react' + +interface ToastProps { + message: string + detail?: string + onDismiss: () => void +} + +/** A transient success toast that auto-dismisses. */ +export function Toast({ message, detail, onDismiss }: ToastProps) { + useEffect(() => { + const t = setTimeout(onDismiss, 4200) + return () => clearTimeout(t) + }, [onDismiss]) + + return ( +
+
+ + + + + +
+

{message}

+ {detail &&

{detail}

} +
+ +
+
+ ) +} diff --git a/src/problem2/src/components/TokenField.tsx b/src/problem2/src/components/TokenField.tsx new file mode 100644 index 0000000000..56783ce6b6 --- /dev/null +++ b/src/problem2/src/components/TokenField.tsx @@ -0,0 +1,97 @@ +import type { Token } from '../types' +import { TokenIcon } from './TokenIcon' +import { formatBalance, formatUsd } from '../utils/format' + +interface TokenFieldProps { + label: string + token: Token | null + /** The raw string value shown in the input. */ + value: string + /** Whether the amount input is editable. The receive side is read-only. */ + readOnly?: boolean + /** USD value of the current amount, for the hint line. */ + usdValue: number + onValueChange?: (value: string) => void + onOpenSelect: () => void + /** Called when the user clicks MAX (pay side only). */ + onMax?: () => void + error?: string +} + +/** + * One side of the swap: a large amount input paired with a token selector + * trigger, balance readout, and USD hint. + */ +export function TokenField({ + label, + token, + value, + readOnly = false, + usdValue, + onValueChange, + onOpenSelect, + onMax, + error, +}: TokenFieldProps) { + return ( +
+
+ {label} + {token && ( + + Balance:{' '} + {formatBalance(token.balance)}{' '} + {onMax && ( + + )} + + )} +
+ +
+ onValueChange?.(e.target.value)} + className={`min-w-0 flex-1 bg-transparent text-3xl font-semibold tracking-tight text-white placeholder:text-slate-600 focus:outline-none + ${readOnly ? 'cursor-default text-slate-100' : ''}`} + /> + + +
+ +
+ {token && value && Number.isFinite(usdValue) ? formatUsd(usdValue) : ' '} +
+
+ ) +} diff --git a/src/problem2/src/components/TokenIcon.tsx b/src/problem2/src/components/TokenIcon.tsx new file mode 100644 index 0000000000..1f2ae1cb67 --- /dev/null +++ b/src/problem2/src/components/TokenIcon.tsx @@ -0,0 +1,50 @@ +import { useState } from 'react' + +interface TokenIconProps { + symbol: string + iconUrl: string + size?: number + className?: string +} + +/** + * Renders a token's remote SVG icon, gracefully falling back to a generated + * monogram badge when the image is missing or fails to load. + */ +export function TokenIcon({ symbol, iconUrl, size = 28, className = '' }: TokenIconProps) { + const [failed, setFailed] = useState(false) + const dimension = { width: size, height: size } + + if (failed) { + // Deterministic hue from the symbol for a stable, distinct placeholder. + let hash = 0 + for (let i = 0; i < symbol.length; i++) hash = symbol.charCodeAt(i) + ((hash << 5) - hash) + const hue = Math.abs(hash) % 360 + return ( + + {symbol.slice(0, 2).toUpperCase()} + + ) + } + + return ( + {`${symbol} setFailed(true)} + /> + ) +} diff --git a/src/problem2/src/components/TokenSelectModal.tsx b/src/problem2/src/components/TokenSelectModal.tsx new file mode 100644 index 0000000000..3d2494d386 --- /dev/null +++ b/src/problem2/src/components/TokenSelectModal.tsx @@ -0,0 +1,136 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import type { Token } from '../types' +import { TokenIcon } from './TokenIcon' +import { formatBalance, formatUsd } from '../utils/format' + +interface TokenSelectModalProps { + open: boolean + tokens: Token[] + /** Symbol to visually mark as selected. */ + selectedSymbol?: string + /** Symbol to disable (e.g. the token chosen on the other side). */ + disabledSymbol?: string + onSelect: (token: Token) => void + onClose: () => void +} + +/** + * A searchable, keyboard-accessible token picker rendered as a modal dialog. + */ +export function TokenSelectModal({ + open, + tokens, + selectedSymbol, + disabledSymbol, + onSelect, + onClose, +}: TokenSelectModalProps) { + const [query, setQuery] = useState('') + const inputRef = useRef(null) + + useEffect(() => { + if (open) { + setQuery('') + // Focus the search box shortly after the modal mounts. + const t = setTimeout(() => inputRef.current?.focus(), 40) + return () => clearTimeout(t) + } + }, [open]) + + useEffect(() => { + if (!open) return + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [open, onClose]) + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase() + if (!q) return tokens + return tokens.filter((t) => t.symbol.toLowerCase().includes(q)) + }, [tokens, query]) + + if (!open) return null + + return ( +
+
+
+
+

Select a token

+ +
+ +
+
+ + + + + setQuery(e.target.value)} + placeholder="Search name or ticker" + className="w-full bg-transparent text-sm text-white placeholder:text-slate-500 focus:outline-none" + /> +
+
+ +
    + {filtered.length === 0 && ( +
  • + No tokens match “{query}”. +
  • + )} + {filtered.map((token) => { + const isSelected = token.symbol === selectedSymbol + const isDisabled = token.symbol === disabledSymbol + return ( +
  • + +
  • + ) + })} +
+
+
+ ) +} diff --git a/src/problem2/src/data/fallbackPrices.ts b/src/problem2/src/data/fallbackPrices.ts new file mode 100644 index 0000000000..8402451344 --- /dev/null +++ b/src/problem2/src/data/fallbackPrices.ts @@ -0,0 +1,43 @@ +import type { PriceRecord } from '../types' + +/** + * A bundled snapshot of https://interview.switcheo.com/prices.json. + * + * Used as a graceful fallback so the app still renders when the network is + * unavailable (offline dev, CI, or a failed request). The live fetch takes + * precedence when it succeeds. + */ +export const FALLBACK_PRICES: PriceRecord[] = [ + { currency: 'BLUR', date: '2023-08-29T07:10:40.000Z', price: 0.20811525423728813 }, + { currency: 'bNEO', date: '2023-08-29T07:10:50.000Z', price: 7.1282679 }, + { currency: 'BUSD', date: '2023-08-29T07:10:40.000Z', price: 0.9998782611186441 }, + { currency: 'USD', date: '2023-08-29T07:10:30.000Z', price: 1 }, + { currency: 'ETH', date: '2023-08-29T07:10:52.000Z', price: 1645.9337373737374 }, + { currency: 'GMX', date: '2023-08-29T07:10:40.000Z', price: 36.345114372881355 }, + { currency: 'STEVMOS', date: '2023-08-29T07:10:40.000Z', price: 0.07276706779661017 }, + { currency: 'LUNA', date: '2023-08-29T07:10:40.000Z', price: 0.40955638983050846 }, + { currency: 'RATOM', date: '2023-08-29T07:10:40.000Z', price: 10.250918915254237 }, + { currency: 'STRD', date: '2023-08-29T07:10:40.000Z', price: 0.7386553389830508 }, + { currency: 'EVMOS', date: '2023-08-29T07:10:40.000Z', price: 0.06246181355932203 }, + { currency: 'IBCX', date: '2023-08-29T07:10:40.000Z', price: 41.26811355932203 }, + { currency: 'IRIS', date: '2023-08-29T07:10:40.000Z', price: 0.0177095593220339 }, + { currency: 'ampLUNA', date: '2023-08-29T07:10:40.000Z', price: 0.49548589830508477 }, + { currency: 'KUJI', date: '2023-08-29T07:10:45.000Z', price: 0.675 }, + { currency: 'STOSMO', date: '2023-08-29T07:10:40.000Z', price: 0.431318 }, + { currency: 'USDC', date: '2023-08-29T07:10:40.000Z', price: 0.989832 }, + { currency: 'axlUSDC', date: '2023-08-29T07:10:40.000Z', price: 0.989832 }, + { currency: 'ATOM', date: '2023-08-29T07:10:50.000Z', price: 7.186657333333334 }, + { currency: 'STATOM', date: '2023-08-29T07:10:45.000Z', price: 8.512162050847458 }, + { currency: 'OSMO', date: '2023-08-29T07:10:50.000Z', price: 0.3772974333333333 }, + { currency: 'rSWTH', date: '2023-08-29T07:10:40.000Z', price: 0.00408771 }, + { currency: 'STLUNA', date: '2023-08-29T07:10:40.000Z', price: 0.44232210169491526 }, + { currency: 'LSI', date: '2023-08-29T07:10:40.000Z', price: 67.69661525423729 }, + { currency: 'OKB', date: '2023-08-29T07:10:40.000Z', price: 42.97562059322034 }, + { currency: 'OKT', date: '2023-08-29T07:10:40.000Z', price: 13.561577966101694 }, + { currency: 'SWTH', date: '2023-08-29T07:10:40.000Z', price: 0.004039850455012084 }, + { currency: 'USC', date: '2023-08-29T07:10:40.000Z', price: 0.994 }, + { currency: 'WBTC', date: '2023-08-29T07:10:40.000Z', price: 26002.82202020202 }, + { currency: 'wstETH', date: '2023-08-29T07:10:40.000Z', price: 1872.2579742372882 }, + { currency: 'YieldUSD', date: '2023-08-29T07:10:40.000Z', price: 1.0290847966101695 }, + { currency: 'ZIL', date: '2023-08-29T07:10:40.000Z', price: 0.01651813559322034 }, +] diff --git a/src/problem2/src/hooks/usePrices.ts b/src/problem2/src/hooks/usePrices.ts new file mode 100644 index 0000000000..ace6fdad03 --- /dev/null +++ b/src/problem2/src/hooks/usePrices.ts @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react' +import type { Token } from '../types' +import { buildTokens } from '../utils/swap' +import { FALLBACK_PRICES } from '../data/fallbackPrices' + +const PRICES_URL = 'https://interview.switcheo.com/prices.json' + +interface PricesState { + tokens: Token[] + loading: boolean + /** Non-fatal note shown when the app fell back to bundled data. */ + usingFallback: boolean +} + +/** + * Loads token prices from the Switcheo endpoint and derives the tradable token + * list. Never throws: on any failure it falls back to a bundled snapshot so the + * form always renders. + */ +export function usePrices(): PricesState { + const [state, setState] = useState({ + tokens: [], + loading: true, + usingFallback: false, + }) + + useEffect(() => { + let cancelled = false + + async function load() { + // Small artificial delay so the loading skeleton is perceptible. + const settle = (tokens: Token[], usingFallback: boolean) => { + if (!cancelled) setState({ tokens, loading: false, usingFallback }) + } + + try { + const res = await fetch(PRICES_URL, { cache: 'no-store' }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + const tokens = buildTokens(data) + if (tokens.length === 0) throw new Error('No priced tokens') + settle(tokens, false) + } catch { + settle(buildTokens(FALLBACK_PRICES), true) + } + } + + load() + return () => { + cancelled = true + } + }, []) + + return state +} diff --git a/src/problem2/src/index.css b/src/problem2/src/index.css new file mode 100644 index 0000000000..94d4fe7660 --- /dev/null +++ b/src/problem2/src/index.css @@ -0,0 +1,51 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + color-scheme: dark; +} + +html, +body, +#root { + height: 100%; +} + +body { + margin: 0; + font-family: 'Inter', system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Ambient animated background */ +.app-bg { + background-color: #060814; + background-image: + radial-gradient(60rem 60rem at 15% -10%, rgba(99, 102, 241, 0.18), transparent 60%), + radial-gradient(50rem 50rem at 110% 10%, rgba(56, 189, 248, 0.14), transparent 55%), + radial-gradient(45rem 45rem at 50% 120%, rgba(168, 85, 247, 0.12), transparent 55%); +} + +/* Hide number input spinners */ +input[type='number']::-webkit-outer-spin-button, +input[type='number']::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +input[type='number'] { + -moz-appearance: textfield; +} + +/* Custom slim scrollbar for the token list */ +.slim-scroll::-webkit-scrollbar { + width: 8px; +} +.slim-scroll::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.3); + border-radius: 9999px; +} +.slim-scroll::-webkit-scrollbar-track { + background: transparent; +} diff --git a/src/problem2/src/main.tsx b/src/problem2/src/main.tsx new file mode 100644 index 0000000000..bef5202a32 --- /dev/null +++ b/src/problem2/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/problem2/src/types.ts b/src/problem2/src/types.ts new file mode 100644 index 0000000000..5aac2f94a1 --- /dev/null +++ b/src/problem2/src/types.ts @@ -0,0 +1,18 @@ +/** A single price record as returned by the Switcheo prices endpoint. */ +export interface PriceRecord { + currency: string + date: string + price: number +} + +/** A tradable token: a currency that has a known USD price. */ +export interface Token { + /** Ticker symbol, e.g. "ETH". */ + symbol: string + /** USD price per unit. */ + price: number + /** Remote SVG icon URL. */ + iconUrl: string + /** Mocked wallet balance for this token. */ + balance: number +} diff --git a/src/problem2/src/utils/format.ts b/src/problem2/src/utils/format.ts new file mode 100644 index 0000000000..0f7d1deb07 --- /dev/null +++ b/src/problem2/src/utils/format.ts @@ -0,0 +1,46 @@ +/** + * Formatting and number helpers for the swap UI. + */ + +/** Parse a user-typed amount, tolerating thousands separators and stray spaces. */ +export function parseAmount(raw: string): number { + if (!raw) return NaN + return Number(raw.replace(/,/g, '').trim()) +} + +/** Format a token amount with a sensible, magnitude-aware number of decimals. */ +export function formatTokenAmount(value: number): string { + if (!Number.isFinite(value)) return '0' + if (value === 0) return '0' + const abs = Math.abs(value) + let decimals = 4 + if (abs >= 1000) decimals = 2 + else if (abs >= 1) decimals = 4 + else if (abs >= 0.0001) decimals = 6 + else decimals = 8 + return value.toLocaleString('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: decimals, + }) +} + +/** Format a USD value as currency. */ +export function formatUsd(value: number): string { + if (!Number.isFinite(value)) return '$0.00' + const abs = Math.abs(value) + const decimals = abs > 0 && abs < 0.01 ? 6 : 2 + return value.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: decimals, + }) +} + +/** Compact display of a wallet balance. */ +export function formatBalance(value: number): string { + return value.toLocaleString('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: value >= 1 ? 4 : 6, + }) +} diff --git a/src/problem2/src/utils/swap.ts b/src/problem2/src/utils/swap.ts new file mode 100644 index 0000000000..809e72358d --- /dev/null +++ b/src/problem2/src/utils/swap.ts @@ -0,0 +1,60 @@ +import type { PriceRecord, Token } from '../types' + +const ICON_BASE = 'https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens' + +/** Remote SVG icon URL for a given currency symbol. */ +export function iconUrlFor(symbol: string): string { + return `${ICON_BASE}/${symbol}.svg` +} + +/** + * Deterministic pseudo-random mocked wallet balance derived from the symbol, + * scaled so that pricier tokens hold fewer units. Stable across renders. + */ +function mockBalanceFor(symbol: string, price: number): number { + let hash = 0 + for (let i = 0; i < symbol.length; i++) { + hash = (hash * 31 + symbol.charCodeAt(i)) % 100000 + } + const usdBudget = 500 + (hash % 9500) // between $500 and $10,000 of holdings + return price > 0 ? usdBudget / price : 0 +} + +/** + * Convert raw price records into a sorted, de-duplicated list of tradable + * tokens. Records without a positive price are omitted. When a currency appears + * multiple times, the record with the latest `date` wins. + */ +export function buildTokens(records: PriceRecord[]): Token[] { + const latest = new Map() + for (const r of records) { + if (!r || typeof r.price !== 'number' || !(r.price > 0)) continue + const existing = latest.get(r.currency) + if (!existing || new Date(r.date).getTime() >= new Date(existing.date).getTime()) { + latest.set(r.currency, r) + } + } + return Array.from(latest.values()) + .map((r) => ({ + symbol: r.currency, + price: r.price, + iconUrl: iconUrlFor(r.currency), + balance: mockBalanceFor(r.currency, r.price), + })) + .sort((a, b) => a.symbol.localeCompare(b.symbol)) +} + +/** + * Exchange rate: how many units of `to` you receive for 1 unit of `from`, + * computed via their shared USD prices. + */ +export function exchangeRate(from: Token, to: Token): number { + if (!from || !to || to.price <= 0) return 0 + return from.price / to.price +} + +/** Amount of `to` received for a given `from` amount. */ +export function convert(amount: number, from: Token, to: Token): number { + if (!Number.isFinite(amount)) return 0 + return amount * exchangeRate(from, to) +} diff --git a/src/problem2/src/vite-env.d.ts b/src/problem2/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/src/problem2/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/problem2/style.css b/src/problem2/style.css deleted file mode 100644 index 915af91c72..0000000000 --- a/src/problem2/style.css +++ /dev/null @@ -1,8 +0,0 @@ -body { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - min-width: 360px; - font-family: Arial, Helvetica, sans-serif; -} diff --git a/src/problem2/tailwind.config.js b/src/problem2/tailwind.config.js new file mode 100644 index 0000000000..2a1e2fee8a --- /dev/null +++ b/src/problem2/tailwind.config.js @@ -0,0 +1,34 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + }, + keyframes: { + 'fade-in': { + '0%': { opacity: '0', transform: 'translateY(6px)' }, + '100%': { opacity: '1', transform: 'translateY(0)' }, + }, + 'scale-in': { + '0%': { opacity: '0', transform: 'scale(0.96)' }, + '100%': { opacity: '1', transform: 'scale(1)' }, + }, + 'toast-in': { + '0%': { opacity: '0', transform: 'translateY(16px) scale(0.98)' }, + '100%': { opacity: '1', transform: 'translateY(0) scale(1)' }, + }, + shimmer: { + '100%': { transform: 'translateX(100%)' }, + }, + }, + animation: { + 'fade-in': 'fade-in 0.25s ease-out both', + 'scale-in': 'scale-in 0.15s ease-out both', + 'toast-in': 'toast-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both', + }, + }, + }, + plugins: [], +} diff --git a/src/problem2/tsconfig.app.json b/src/problem2/tsconfig.app.json new file mode 100644 index 0000000000..5405343290 --- /dev/null +++ b/src/problem2/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/src/problem2/tsconfig.json b/src/problem2/tsconfig.json new file mode 100644 index 0000000000..1ffef600d9 --- /dev/null +++ b/src/problem2/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/src/problem2/tsconfig.node.json b/src/problem2/tsconfig.node.json new file mode 100644 index 0000000000..b3fc13e547 --- /dev/null +++ b/src/problem2/tsconfig.node.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/src/problem2/vite.config.ts b/src/problem2/vite.config.ts new file mode 100644 index 0000000000..8b0f57b91a --- /dev/null +++ b/src/problem2/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From d0f4db8022497100af390188cce57c6e8c4a46fe Mon Sep 17 00:00:00 2001 From: "hieu.cao" Date: Sat, 25 Jul 2026 10:12:48 +0700 Subject: [PATCH 3/9] Problem 3: Messy React - inefficiency/anti-pattern analysis + refactor --- src/problem3/README.md | 205 ++++++++++++++++++++++++++++++++++++ src/problem3/refactored.tsx | 125 ++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 src/problem3/README.md create mode 100644 src/problem3/refactored.tsx diff --git a/src/problem3/README.md b/src/problem3/README.md new file mode 100644 index 0000000000..7b87208681 --- /dev/null +++ b/src/problem3/README.md @@ -0,0 +1,205 @@ +# Problem 3 — Messy React + +Analysis of the computational inefficiencies and anti-patterns in the original +`WalletPage` component, grouped by category. Each item states **what** is wrong, +**why** it matters, and **how** to fix it. The corrected implementation lives in +[`refactored.tsx`](./refactored.tsx). + +--- + +## A. Bugs / Correctness + +### A1. `lhsPriority` is undefined — `ReferenceError` +```ts +const balancePriority = getPriority(balance.blockchain); +if (lhsPriority > -99) { ... } // lhsPriority was never declared +``` +The filter computes `balancePriority` but then tests a variable named +`lhsPriority` that does not exist in scope. In a real build this is a +`ReferenceError` at runtime (or a compile error under `noUnusedLocals` / +`strict`), so the component never renders. + +**Fix:** reference the variable that was actually computed +(`balancePriority`), or inline the call. + +### A2. Inverted filter logic — keeps empty balances, drops real ones +```ts +if (balancePriority > -99) { + if (balance.amount <= 0) { + return true; // keeps zero / negative balances + } +} +return false; // drops every positive balance +``` +The predicate keeps balances whose `amount <= 0` and discards every balance with +a positive amount — the opposite of what a wallet view wants. It also nests two +`if`s where one boolean expression is clearer. + +**Fix:** +```ts +return getPriority(balance.blockchain) > -99 && balance.amount > 0; +``` + +### A3. `.sort()` comparator can return `undefined` +```ts +.sort((lhs, rhs) => { + const leftPriority = getPriority(lhs.blockchain); + const rightPriority = getPriority(rhs.blockchain); + if (leftPriority > rightPriority) return -1; + else if (rightPriority > leftPriority) return 1; + // equal case: falls through and returns undefined +}); +``` +When two items have equal priority the function returns `undefined`, which is +not a valid comparator result. `Array.prototype.sort` expects a number; a +non-number is coerced/treated inconsistently and produces implementation-defined, +potentially unstable ordering. + +**Fix:** return `0` for the equal case — or simply +`return rightPriority - leftPriority;`. + +### A4. `formattedBalances` is computed but never used; `rows` reads a field that doesn't exist +```ts +const formattedBalances = sortedBalances.map(...); // never referenced + +const rows = sortedBalances.map((balance: FormattedWalletBalance) => { + ... + formattedAmount={balance.formatted} // 'formatted' does not exist on WalletBalance +}); +``` +`formattedBalances` (the array that actually has the `formatted` field) is thrown +away. `rows` instead maps over `sortedBalances`, whose items are plain +`WalletBalance` and have **no** `formatted` property, so `balance.formatted` is +`undefined` at runtime. The parameter is also mis-typed as +`FormattedWalletBalance`, which hides the error from the compiler (a lie to the +type system). + +**Fix:** map over the formatted array (or format inside the single map that +builds the rows) so the value passed to `formattedAmount` actually exists. + +### A5. `prices[balance.currency]` can be `undefined` → `NaN` +```ts +const usdValue = prices[balance.currency] * balance.amount; +``` +If a currency is missing from `prices`, the multiplication yields `NaN`, which +then renders into the UI. + +**Fix:** guard the lookup, e.g. `const price = prices[balance.currency] ?? 0;`. + +### A6. `.toFixed()` with no argument +```ts +formatted: balance.amount.toFixed() +``` +With no digits argument, `toFixed` rounds to **zero** decimal places +(`1.987.toFixed()` → `"2"`), which is almost certainly not intended for a token +amount and gives inconsistent precision. + +**Fix:** pass an explicit precision, e.g. `.toFixed(2)` (or a per-currency +precision). + +--- + +## B. Performance / Computational inefficiency + +### B1. Wrong `useMemo` dependency array +```ts +const sortedBalances = useMemo(() => { ... }, [balances, prices]); +``` +The memoized computation uses only `balances` (and `getPriority`); it never reads +`prices`. Listing `prices` forces the filter+sort to recompute every time prices +tick — which, for a live price feed, can be many times per second — for no +benefit. + +**Fix:** depend on `[balances]` only (plus `getPriority` if it isn't hoisted). + +### B2. `getPriority` re-created every render and called O(n log n) times +`getPriority` is redefined on every render (new function identity each time), and +it is invoked *inside the sort comparator*, so for `n` items it runs roughly +`O(n log n)` times per sort — recomputing the same constant priority for the same +item repeatedly. + +**Fix:** hoist `getPriority` out of the component (it depends on nothing from +props/state) so it is defined once, and back it with a `Record` lookup instead of +a `switch`. Better still, compute each item's priority **once** by mapping to +`{ balance, priority }` before sorting, so the comparator just reads a number. + +### B3. `formattedBalances` is redundant work +As noted in A4, `formattedBalances` is fully computed and discarded — pure wasted +work on every render. Removing it (and doing the formatting once, where it's +actually consumed) eliminates an entire pass over the array. + +### B4. Row list not memoized +`rows` is rebuilt on every render even when neither `balances` nor `prices` +changed. Deriving it from a `useMemo` (keyed on the sorted/priced data) avoids +rebuilding the JSX array unnecessarily. + +--- + +## C. Type-safety + +### C1. `getPriority(blockchain: any)` +Using `any` disables all type checking on the argument, so typos or wrong shapes +pass silently. + +**Fix:** introduce a `Blockchain` union (or enum) type and type the parameter as +`Blockchain`. + +### C2. `WalletBalance` has no `blockchain` field +The code reads `balance.blockchain` throughout, but the `WalletBalance` interface +only declares `currency` and `amount`. The interface is incomplete; the reads +only "work" because `any`/loose typing hides it. + +**Fix:** add `blockchain: Blockchain` to `WalletBalance`. + +### C3. Mis-typed map callback (see A4) +`sortedBalances.map((balance: FormattedWalletBalance, ...))` annotates the item +as a type it does not actually have, defeating the purpose of TypeScript. Let +inference do its job or annotate accurately. + +--- + +## D. React anti-patterns / code smell + +### D1. `key={index}` +```tsx + +``` +Using the array index as a `key` breaks React's reconciliation when the list is +reordered, inserted into, or filtered (exactly what this component does — it +sorts and filters). It can cause wrong rows to update, lost input state, and +subtle rendering bugs. + +**Fix:** use a stable, unique key such as `balance.currency` (or a blockchain + +currency composite if currencies can repeat). + +### D2. Empty `interface Props extends BoxProps {}` +An empty interface that only extends another type adds nothing and triggers the +`@typescript-eslint/no-empty-interface` lint. + +**Fix:** use `type Props = BoxProps;` (or add the real extra props). + +### D3. `children` destructured but never used +```ts +const { children, ...rest } = props; +``` +`children` is pulled out and discarded. Either render it or don't destructure it +(so it stays in `...rest` and passes through the spread naturally). + +### D4. Building JSX in the component body / other minor smells +- `classes` (used as `className={classes.row}`) is referenced but never + imported/shown — a dangling dependency. +- Deriving the JSX array inline (`rows`) mixes data transformation with markup; + extracting the data pipeline into memoized derivations keeps render clean. + +--- + +## Summary of fixes applied in `refactored.tsx` +1. `Blockchain` union type + `blockchain` field added to `WalletBalance`. +2. `getPriority` hoisted out of the component, backed by a `Record` lookup. +3. Single `useMemo` with correct deps `[balances]`, computing priority **once** + per item, filtering `amount > 0`, and a complete comparator (`return 0`). +4. One formatting pass, whose result is what `rows` actually consumes. +5. Guarded `usdValue` (missing price → `0`, no `NaN`). +6. Explicit `toFixed(2)`. +7. Stable `key={balance.currency}`. +8. `type Props = BoxProps`, `children` handled cleanly. diff --git a/src/problem3/refactored.tsx b/src/problem3/refactored.tsx new file mode 100644 index 0000000000..5b7c084b45 --- /dev/null +++ b/src/problem3/refactored.tsx @@ -0,0 +1,125 @@ +import React, { useMemo } from 'react'; + +/** + * Refactored version of the Problem 3 WalletPage. + * + * This file is illustrative and is not meant to compile standalone: the symbols + * below are assumed to come from the surrounding application (a design-system + * `Box`, data hooks, a CSS-modules `classes` object, and the `WalletRow` + * component). Minimal stubs/comments are provided so the intent is clear and the + * file is internally consistent. + */ + +// --------------------------------------------------------------------------- +// External dependencies (provided elsewhere in the real app) +// --------------------------------------------------------------------------- +type BoxProps = React.HTMLAttributes; + +declare const classes: { row: string }; + +declare function useWalletBalances(): WalletBalance[]; +declare function usePrices(): Record; + +interface WalletRowProps { + className?: string; + amount: number; + usdValue: number; + formattedAmount: string; +} +declare const WalletRow: React.FC; + +// --------------------------------------------------------------------------- +// Domain types +// --------------------------------------------------------------------------- + +/** All blockchains we know how to prioritise. Replace `any` with a real union. */ +type Blockchain = 'Osmosis' | 'Ethereum' | 'Arbitrum' | 'Zilliqa' | 'Neo'; + +interface WalletBalance { + currency: string; + amount: number; + blockchain: Blockchain; // was missing from the original interface +} + +interface FormattedWalletBalance extends WalletBalance { + formatted: string; + usdValue: number; +} + +// --------------------------------------------------------------------------- +// Pure helpers — hoisted out of the component so they are defined once, +// carry no per-render identity, and are trivially testable. +// --------------------------------------------------------------------------- + +const DEFAULT_PRIORITY = -99; + +/** Priority lookup table; O(1) instead of a re-created switch each render. */ +const BLOCKCHAIN_PRIORITY: Record = { + Osmosis: 100, + Ethereum: 50, + Arbitrum: 30, + Zilliqa: 20, + Neo: 20, +}; + +const getPriority = (blockchain: Blockchain): number => + BLOCKCHAIN_PRIORITY[blockchain] ?? DEFAULT_PRIORITY; + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +type Props = BoxProps; + +const WalletPage: React.FC = (props: Props) => { + // `children` intentionally left inside `...rest` so it passes through the + // spread rather than being destructured and dropped. + const { ...rest } = props; + const balances = useWalletBalances(); + const prices = usePrices(); + + // Single derivation: filter -> sort. Priority is computed once per item + // (not on every comparator call), the filter keeps positive balances, and + // the comparator always returns a number. Depends only on `balances`. + const sortedBalances = useMemo(() => { + return balances + .filter( + (balance) => + getPriority(balance.blockchain) > DEFAULT_PRIORITY && + balance.amount > 0, + ) + .map((balance) => ({ balance, priority: getPriority(balance.blockchain) })) + .sort((lhs, rhs) => rhs.priority - lhs.priority) // descending, total order + .map(({ balance }) => balance); + }, [balances]); + + // Formatting + USD value computed once, and this is the array `rows` consumes. + const formattedBalances = useMemo(() => { + return sortedBalances.map((balance) => { + const price = prices[balance.currency] ?? 0; // guard against missing price / NaN + return { + ...balance, + formatted: balance.amount.toFixed(2), // explicit precision + usdValue: price * balance.amount, + }; + }); + }, [sortedBalances, prices]); + + const rows = useMemo( + () => + formattedBalances.map((balance) => ( + + )), + [formattedBalances], + ); + + return
{rows}
; +}; + +export default WalletPage; From 85436f763798c622e930d5ade8a11a787962e9ea Mon Sep 17 00:00:00 2001 From: "hieu.cao" Date: Sat, 25 Jul 2026 10:24:52 +0700 Subject: [PATCH 4/9] P1: ES module exports, node:test unit tests (128), expanded docs --- src/problem1/README.md | 89 ++++++++++++++++++++--------- src/problem1/package.json | 15 +++++ src/problem1/sum_to_n.js | 104 +++++++++++++++++++++------------- src/problem1/sum_to_n.test.js | 61 ++++++++++++++++++++ 4 files changed, 204 insertions(+), 65 deletions(-) create mode 100644 src/problem1/package.json create mode 100644 src/problem1/sum_to_n.test.js diff --git a/src/problem1/README.md b/src/problem1/README.md index a0eff6a41a..dbe633b8c3 100644 --- a/src/problem1/README.md +++ b/src/problem1/README.md @@ -3,42 +3,35 @@ Three unique implementations of a function that returns the summation to `n`, e.g. `sum_to_n(5) === 1 + 2 + 3 + 4 + 5 === 15`. -Input `n` is any integer; the result is assumed to stay below -`Number.MAX_SAFE_INTEGER`. +## Problem statement -## Run +> Provide 3 unique implementations of the following function. +> +> **Input**: `n` — any integer. _Assuming this input will always produce a +> result lesser than `Number.MAX_SAFE_INTEGER`._ +> +> **Output**: `return` — summation to `n`, i.e. +> `sum_to_n(5) === 1 + 2 + 3 + 4 + 5 === 15`. -```bash -node sum_to_n.js -``` - -A clean run prints `all checks passed`. The file contains `console.assert` -sanity checks that stay silent on success and log a message on failure. - -## Negative-`n` convention - -Since `n` may be any integer, the three implementations agree on the following -consistent behaviour: - -| `n` | result | meaning | -| ---- | ------ | ------------------------------------ | -| `5` | `15` | sum of `1..5` | -| `0` | `0` | empty sum | -| `-3` | `-6` | sum of `-3, -2, -1` (i.e. `n..-1`) | +The module (`sum_to_n.js`) is a side-effect-free ES module that exports all +three functions (`sum_to_n_a`, `sum_to_n_b`, `sum_to_n_c`), so it can be +imported cleanly from tests or other code. ## The three approaches -| Fn | Approach | Time | Space | -| -------------- | ------------------------------- | ------ | --------------- | -| `sum_to_n_a` | Iterative `for` loop | `O(n)` | `O(1)` | -| `sum_to_n_b` | Closed-form Gauss formula | `O(1)` | `O(1)` | -| `sum_to_n_c` | Recursion (functional style) | `O(n)` | `O(n)` stack | +| Fn | Approach | Time | Space | +| ------------ | ---------------------------- | ------ | ------------ | +| `sum_to_n_a` | Iterative `for` loop | `O(n)` | `O(1)` | +| `sum_to_n_b` | Closed-form Gauss formula | `O(1)` | `O(1)` | +| `sum_to_n_c` | Recursion (functional style) | `O(n)` | `O(n)` stack | ### a) Iterative loop + Accumulates a running total, walking `1..n` for positive `n` or `n..-1` for negative `n`. ### b) Closed-form (Gauss) + Uses the triangular-number identity. Important detail: the bare `n*(n+1)/2` does **not** satisfy the negative convention — for `n = -3` it returns `3`, not `-6`. The sum of `n..-1` for `n < 0` equals the triangular @@ -53,5 +46,49 @@ which gives `15` for `n=5`, `0` for `n=0`, and `-6` for `n=-3`, all in constant time. ### c) Recursion + Peels one term off toward `0` per call: `f(n) = n + f(n ∓ 1)`. Clear and -functional, but uses `O(n)` call-stack space. +functional, but uses `O(n)` call-stack space (so very large `|n|` can overflow +the stack — a trade-off documented here for completeness). + +## Negative-`n` assumption + +Since `n` may be any integer, the three implementations agree on the following +consistent behaviour: + +| `n` | result | meaning | +| ---- | ------ | ---------------------------------- | +| `5` | `15` | sum of `1..5` | +| `0` | `0` | empty sum | +| `-3` | `-6` | sum of `-3, -2, -1` (i.e. `n..-1`) | + +## Run the demo + +```bash +node sum_to_n.js +# or +npm start +``` + +A clean run prints `all checks passed`. The file contains `console.assert` +sanity checks (behind an `import.meta` main-module guard) that stay silent on +success and log a message on failure. + +## How to run tests + +Tests use Node's built-in test runner (`node:test`) and assertions +(`node:assert/strict`) — **no dependencies to install**. Requires **Node 20+**. + +```bash +node --test +# or +npm test +``` + +The suite (`sum_to_n.test.js`) covers: + +- the spec example (`sum_to_n(5) === 15`), zero, and one; +- large values (`n=100 → 5050`, `n=1000 → 500500`); +- negatives (`-1`, `-3`, `-100`) per the convention above; +- a parametrized **three-way-agreement** check asserting all three + implementations return identical results for every `n` in `-50..50`. diff --git a/src/problem1/package.json b/src/problem1/package.json new file mode 100644 index 0000000000..b37dc47e00 --- /dev/null +++ b/src/problem1/package.json @@ -0,0 +1,15 @@ +{ + "name": "problem1-sum-to-n", + "version": "1.0.0", + "description": "Three implementations of sum_to_n with unit tests (99Tech frontend challenge, Problem 1).", + "type": "module", + "private": true, + "main": "sum_to_n.js", + "scripts": { + "test": "node --test", + "start": "node sum_to_n.js" + }, + "engines": { + "node": ">=20" + } +} diff --git a/src/problem1/sum_to_n.js b/src/problem1/sum_to_n.js index faba24cdf3..db546551b1 100644 --- a/src/problem1/sum_to_n.js +++ b/src/problem1/sum_to_n.js @@ -17,12 +17,22 @@ * a) iterative for-loop : O(n) time, O(1) space * b) closed-form (Gauss): O(1) time, O(1) space * c) recursion : O(n) time, O(n) stack space + * + * This module has NO top-level side effects, so it is safe to import from + * tests or other modules. A tiny runnable demo lives behind the + * `import.meta` main-module guard at the bottom of the file. */ -// a) Iterative for-loop. -// Walks either 1..n (n > 0) or n..-1 (n < 0), accumulating a running sum. -// O(n) time, O(1) space. -var sum_to_n_a = function (n) { +/** + * a) Iterative for-loop. + * + * Walks either 1..n (n > 0) or n..-1 (n < 0), accumulating a running sum. + * O(n) time, O(1) space. + * + * @param {number} n - any integer + * @returns {number} the summation to n + */ +export const sum_to_n_a = function (n) { let sum = 0; if (n >= 0) { for (let i = 1; i <= n; i++) sum += i; @@ -32,48 +42,64 @@ var sum_to_n_a = function (n) { return sum; }; -// b) Closed-form Gauss formula. -// For n >= 0 this is the classic triangular number n*(n+1)/2. -// NOTE: the *plain* formula n*(n+1)/2 does NOT match our negative -// convention -- for n = -3 it gives (-3)*(-2)/2 = 3, but we want -6. -// The convention "sum of n..-1" for n < 0 equals -(|n|*(|n|+1)/2), i.e. -// the same triangular number negated. So the sign-aware closed form is: -// sign(n) * |n| * (|n| + 1) / 2 -// which yields 15 for n=5, 0 for n=0, and -6 for n=-3. O(1) time & space. -var sum_to_n_b = function (n) { +/** + * b) Closed-form Gauss formula. + * + * For n >= 0 this is the classic triangular number n*(n+1)/2. + * NOTE: the *plain* formula n*(n+1)/2 does NOT match our negative + * convention -- for n = -3 it gives (-3)*(-2)/2 = 3, but we want -6. + * The convention "sum of n..-1" for n < 0 equals -(|n|*(|n|+1)/2), i.e. + * the same triangular number negated. So the sign-aware closed form is: + * sign(n) * |n| * (|n| + 1) / 2 + * which yields 15 for n=5, 0 for n=0, and -6 for n=-3. O(1) time & space. + * + * @param {number} n - any integer + * @returns {number} the summation to n + */ +export const sum_to_n_b = function (n) { const m = Math.abs(n); - return Math.sign(n) * (m * (m + 1)) / 2; + return (Math.sign(n) * (m * (m + 1))) / 2; }; -// c) Recursive / functional approach. -// Peels one term off toward 0 each call: f(n) = n + f(n -/+ 1). -// O(n) time, O(n) call-stack space. -var sum_to_n_c = function (n) { +/** + * c) Recursive / functional approach. + * + * Peels one term off toward 0 each call: f(n) = n + f(n -/+ 1). + * O(n) time, O(n) call-stack space. + * + * @param {number} n - any integer + * @returns {number} the summation to n + */ +export const sum_to_n_c = function (n) { if (n === 0) return 0; if (n > 0) return n + sum_to_n_c(n - 1); return n + sum_to_n_c(n + 1); }; -// --- Sanity checks (runnable with `node sum_to_n.js`) -------------------- -const cases = [ - [5, 15], - [1, 1], - [0, 0], - [10, 55], - [100, 5050], - [-1, -1], - [-3, -6], - [-100, -5050], -]; +// --- Runnable demo -------------------------------------------------------- +// Only runs when this file is executed directly (`node sum_to_n.js`), +// never when imported. Keeps the module free of import-time side effects. +if (import.meta.url === `file://${process.argv[1]}`) { + const cases = [ + [5, 15], + [1, 1], + [0, 0], + [10, 55], + [100, 5050], + [-1, -1], + [-3, -6], + [-100, -5050], + ]; -for (const [n, expected] of cases) { - console.assert(sum_to_n_a(n) === expected, `a(${n}) => ${sum_to_n_a(n)}, want ${expected}`); - console.assert(sum_to_n_b(n) === expected, `b(${n}) => ${sum_to_n_b(n)}, want ${expected}`); - console.assert(sum_to_n_c(n) === expected, `c(${n}) => ${sum_to_n_c(n)}, want ${expected}`); - console.assert( - sum_to_n_a(n) === sum_to_n_b(n) && sum_to_n_b(n) === sum_to_n_c(n), - `disagreement at n=${n}` - ); -} + for (const [n, expected] of cases) { + console.assert(sum_to_n_a(n) === expected, `a(${n}) => ${sum_to_n_a(n)}, want ${expected}`); + console.assert(sum_to_n_b(n) === expected, `b(${n}) => ${sum_to_n_b(n)}, want ${expected}`); + console.assert(sum_to_n_c(n) === expected, `c(${n}) => ${sum_to_n_c(n)}, want ${expected}`); + console.assert( + sum_to_n_a(n) === sum_to_n_b(n) && sum_to_n_b(n) === sum_to_n_c(n), + `disagreement at n=${n}`, + ); + } -console.log("all checks passed"); + console.log("all checks passed"); +} diff --git a/src/problem1/sum_to_n.test.js b/src/problem1/sum_to_n.test.js new file mode 100644 index 0000000000..fd4ed4795c --- /dev/null +++ b/src/problem1/sum_to_n.test.js @@ -0,0 +1,61 @@ +/** + * Unit tests for Problem 1 `sum_to_n` implementations. + * + * Uses Node's built-in test runner (`node:test`) and assertions + * (`node:assert/strict`) so there are zero external dependencies. + * + * Run with: node --test (Node 20+) + */ + +import { test, describe } from "node:test"; +import assert from "node:assert/strict"; + +import { sum_to_n_a, sum_to_n_b, sum_to_n_c } from "./sum_to_n.js"; + +// Shared table of (input, expected) pairs covering the spec example, the +// boundary cases (0, 1), a couple of large values, and the documented +// negative-n convention (sum of n..-1). +const cases = [ + [5, 15], // spec example + [0, 0], // empty sum + [1, 1], // smallest positive + [10, 55], + [100, 5050], // large-ish + [1000, 500500], // large + [-1, -1], + [-3, -6], // negative convention: (-3)+(-2)+(-1) + [-100, -5050], +]; + +// Each implementation is exercised against the same table so behaviour is +// verified to be identical from the caller's point of view. +const implementations = [ + ["sum_to_n_a (iterative loop)", sum_to_n_a], + ["sum_to_n_b (closed-form Gauss)", sum_to_n_b], + ["sum_to_n_c (recursion)", sum_to_n_c], +]; + +for (const [name, fn] of implementations) { + describe(name, () => { + for (const [n, expected] of cases) { + test(`sum_to_n(${n}) === ${expected}`, () => { + assert.equal(fn(n), expected); + }); + } + }); +} + +describe("three-way agreement", () => { + // Parametrized cross-check: every implementation must return an identical + // result for each input across a contiguous range spanning negatives, + // zero, and positives. + for (let n = -50; n <= 50; n++) { + test(`all three agree at n=${n}`, () => { + const a = sum_to_n_a(n); + const b = sum_to_n_b(n); + const c = sum_to_n_c(n); + assert.equal(a, b, `a and b disagree at n=${n}`); + assert.equal(b, c, `b and c disagree at n=${n}`); + }); + } +}); From 7e5db36abc0d896e0ffcb61b3f1eaf666aa5f1a5 Mon Sep 17 00:00:00 2001 From: "hieu.cao" Date: Sat, 25 Jul 2026 10:24:52 +0700 Subject: [PATCH 5/9] P2: Vitest + RTL test suite (53), a11y labels, testing docs Unit tests for swap/format utils and usePrices; component tests for SwapForm validation/flip/submit and TokenSelect search. --- src/problem2/.gitignore | 3 + src/problem2/README.md | 72 +- src/problem2/package-lock.json | 2676 ++++++++++++++++- src/problem2/package.json | 14 +- src/problem2/src/components/SwapForm.test.tsx | 108 + src/problem2/src/components/TokenField.tsx | 2 + .../src/components/TokenSelectModal.test.tsx | 85 + src/problem2/src/hooks/usePrices.test.ts | 59 + src/problem2/src/test/fixtures.ts | 32 + src/problem2/src/test/setup.ts | 16 + src/problem2/src/utils/format.test.ts | 89 + src/problem2/src/utils/swap.test.ts | 120 + src/problem2/tsconfig.app.json | 7 +- src/problem2/vite.config.ts | 11 +- 14 files changed, 3244 insertions(+), 50 deletions(-) create mode 100644 src/problem2/src/components/SwapForm.test.tsx create mode 100644 src/problem2/src/components/TokenSelectModal.test.tsx create mode 100644 src/problem2/src/hooks/usePrices.test.ts create mode 100644 src/problem2/src/test/fixtures.ts create mode 100644 src/problem2/src/test/setup.ts create mode 100644 src/problem2/src/utils/format.test.ts create mode 100644 src/problem2/src/utils/swap.test.ts diff --git a/src/problem2/.gitignore b/src/problem2/.gitignore index b6cdf575d3..d8112c1de1 100644 --- a/src/problem2/.gitignore +++ b/src/problem2/.gitignore @@ -13,6 +13,9 @@ dist-ssr *.local *.tsbuildinfo +# Test coverage +coverage + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/src/problem2/README.md b/src/problem2/README.md index 5160b1bd79..8457f95f95 100644 --- a/src/problem2/README.md +++ b/src/problem2/README.md @@ -36,13 +36,70 @@ loading state and a success toast. ```bash npm install -npm run dev # start the dev server (Vite prints the local URL) -npm run build # type-check + production build into dist/ -npm run preview # preview the production build +npm run dev # start the dev server (Vite prints the local URL) +npm run build # type-check + production build into dist/ +npm run preview # preview the production build +npm test # run the unit / component test suite once +npm run test:watch # re-run tests on change +npm run typecheck # type-check without emitting ``` Requires Node 18+ (developed on Node 20). +## Testing + +**Stack:** [Vitest](https://vitest.dev/) + [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) +running in a `jsdom` environment, with `@testing-library/jest-dom` matchers and +`@testing-library/user-event` for realistic interactions. + +```bash +npm test # single run (CI-friendly) +npm run test:watch # watch mode +npm run test:coverage # run with a V8 coverage report +``` + +Configuration lives in the `test` block of `vite.config.ts` (`globals: true`, +`environment: 'jsdom'`, `setupFiles: ['./src/test/setup.ts']`). Test files +(`*.test.ts`/`*.test.tsx`) and the `src/test/` helpers are excluded from the +production `tsc` build, so `npm run build` stays clean. + +**What's covered (53 tests):** + +- **`utils/swap.ts`** — exchange-rate math via USD prices, same-token and + missing-price edge cases, conversion (including non-finite amounts and + round-trips), and `buildTokens` (alphabetical sort, deterministic mocked + balance, dedupe-by-latest-date, dropping non-positive / malformed records). +- **`utils/format.ts`** — the tolerant amount parser (empty, non-numeric, + thousands separators, trailing dot, very large / very small, negatives) and + the magnitude-aware token / USD / balance formatters. +- **`hooks/usePrices.ts`** — network success path plus graceful fallback to the + bundled snapshot on fetch failure / non-OK response, with `fetch` stubbed via + `vi.stubGlobal` (deterministic and offline). +- **`SwapForm`** — renders the default pair and rate line; inline validation for + empty / zero / negative / non-numeric / over-balance amounts (with submit + disabled); receive-amount computation; the swap-direction flip; token + selection with the opposite side disabled; and the submit flow (loading state + → mocked ~1.5 s round-trip → `onSuccess`). +- **`TokenSelectModal`** — search filtering, empty state, selection callback, + the disabled opposite-side token, and `Esc`-to-close. + +Tests query by role and accessible name (not brittle CSS selectors), so they +stay resilient to styling changes. + +## Architecture / best practices + +- **Pure logic is decoupled from React.** All rate/conversion math, token + building, and the price dedupe-by-latest-date transform live as exported pure + functions in `utils/` (`buildTokens`, `exchangeRate`, `convert`) and are unit + tested in isolation from any component or network call. +- **Data fetching is isolated in a hook.** `usePrices` owns the fetch, the + fallback, and the loading flag; it never throws, so the UI always renders. +- **Components stay presentational and prop-driven.** `SwapForm` receives its + token list as a prop, which keeps it deterministic and trivially testable + without mocking the network. +- **Accessibility as a contract.** Interactive controls carry `aria-label`s and + proper roles (`dialog`, `status`), which double as stable test hooks. + ## Data sources - **Prices:** `https://interview.switcheo.com/prices.json` — an array of @@ -72,11 +129,20 @@ src/ Toast.tsx # success toast hooks/ usePrices.ts # fetch + fallback, returns tradable tokens + usePrices.test.ts # hook test (fetch stubbed) utils/ swap.ts # token building, rate math, conversion + swap.test.ts # pure-logic unit tests format.ts # number / currency formatting + format.test.ts # formatter / parser unit tests data/ fallbackPrices.ts # offline snapshot of prices.json + test/ + setup.ts # jest-dom matchers + cleanup (Vitest setupFiles) + fixtures.ts # deterministic Token fixtures for tests types.ts # shared types App.tsx # layout, loading skeleton, toast wiring ``` + +`SwapForm.test.tsx` and `TokenSelectModal.test.tsx` sit alongside their +components in `src/components/`. diff --git a/src/problem2/package-lock.json b/src/problem2/package-lock.json index 2c2fb81d4c..5b73c538aa 100644 --- a/src/problem2/package-lock.json +++ b/src/problem2/package-lock.json @@ -12,16 +12,29 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^4.1.10", "autoprefixer": "^10.4.20", + "jsdom": "^29.1.1", "postcss": "^8.4.49", "tailwindcss": "^3.4.17", "typescript": "^5.6.3", - "vite": "^5.4.11" + "vite": "^5.4.11", + "vitest": "^4.1.10" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -35,6 +48,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -269,6 +333,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -317,6 +391,203 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -606,6 +877,24 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -623,6 +912,24 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -640,6 +947,24 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -708,6 +1033,24 @@ "node": ">=12" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -758,6 +1101,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -796,31 +1158,37 @@ "node": ">= 8" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -828,24 +1196,285 @@ "license": "MIT", "optional": true, "os": [ - "android" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", @@ -1153,6 +1782,122 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1198,6 +1943,24 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1254,7 +2017,149 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/any-promise": { + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", @@ -1282,6 +2187,45 @@ "dev": true, "license": "MIT" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.5.4", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", @@ -1332,6 +2276,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1423,6 +2377,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1478,6 +2442,27 @@ "dev": true, "license": "MIT" }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1498,6 +2483,20 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1516,6 +2515,33 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1530,6 +2556,14 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/electron-to-chromium": { "version": "1.5.396", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", @@ -1537,6 +2571,19 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -1547,6 +2594,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1596,6 +2650,26 @@ "node": ">=6" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1711,6 +2785,16 @@ "node": ">=10.13.0" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -1724,6 +2808,36 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1786,6 +2900,52 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -1802,6 +2962,57 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -1828,23 +3039,284 @@ "node": ">=6" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "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/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/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "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/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/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/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/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/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/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/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/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/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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, "license": "MIT" }, @@ -1870,6 +3342,75 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1894,6 +3435,16 @@ "node": ">=8.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1972,6 +3523,33 @@ "node": ">= 6" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1979,6 +3557,13 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2182,6 +3767,32 @@ "dev": true, "license": "MIT" }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2228,6 +3839,14 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2261,6 +3880,30 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -2294,17 +3937,58 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/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/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { "node": ">=18.0.0", @@ -2363,6 +4047,19 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -2382,6 +4079,13 @@ "semver": "bin/semver.js" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2392,6 +4096,33 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -2415,6 +4146,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2428,6 +4172,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -2489,6 +4240,23 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2537,6 +4305,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2550,6 +4348,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -2557,6 +4381,14 @@ "dev": true, "license": "Apache-2.0" }, + "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", + "optional": true + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2571,6 +4403,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2669,6 +4511,754 @@ } } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/vitest/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/vitest/node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/src/problem2/package.json b/src/problem2/package.json index cfe23a32f4..348113d7e7 100644 --- a/src/problem2/package.json +++ b/src/problem2/package.json @@ -6,20 +6,30 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "typecheck": "tsc -b", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^4.1.10", "autoprefixer": "^10.4.20", + "jsdom": "^29.1.1", "postcss": "^8.4.49", "tailwindcss": "^3.4.17", "typescript": "^5.6.3", - "vite": "^5.4.11" + "vite": "^5.4.11", + "vitest": "^4.1.10" } } diff --git a/src/problem2/src/components/SwapForm.test.tsx b/src/problem2/src/components/SwapForm.test.tsx new file mode 100644 index 0000000000..b885722250 --- /dev/null +++ b/src/problem2/src/components/SwapForm.test.tsx @@ -0,0 +1,108 @@ +import { describe, expect, it, vi } from 'vitest' +import { render, screen, waitFor, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { SwapForm } from './SwapForm' +import { TEST_TOKENS } from '../test/fixtures' + +function renderForm() { + const onSuccess = vi.fn() + render() + return { onSuccess } +} + +const payInput = () => screen.getByLabelText('You pay amount') as HTMLInputElement +const receiveInput = () => screen.getByLabelText('You receive amount') as HTMLInputElement +const submitButton = () => screen.getByRole('button', { name: /confirm swap|enter an amount|amount|number|different|balance/i }) + +describe('SwapForm', () => { + it('renders both sides with the default ETH -> USDC pair and a rate line', () => { + renderForm() + expect(payInput()).toBeInTheDocument() + expect(receiveInput()).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'You pay token: ETH' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'You receive token: USDC' })).toBeInTheDocument() + // 1 ETH ($2000) = 2000 USDC ($1) + expect(screen.getByText(/1 ETH = 2,000 USDC/)).toBeInTheDocument() + }) + + it('disables submit initially with no amount entered', () => { + renderForm() + expect(screen.getByRole('button', { name: 'Enter an amount' })).toBeDisabled() + }) + + it.each([ + ['0', /greater than zero/i], + ['-5', /greater than zero/i], + ['abc', /valid number/i], + ])('shows an inline error and disables submit for invalid amount %s', async (value, message) => { + const user = userEvent.setup() + renderForm() + await user.type(payInput(), value) + // The message renders both in the inline error and on the submit button. + expect(screen.getAllByText(message).length).toBeGreaterThan(0) + expect(submitButton()).toBeDisabled() + }) + + it('errors when the amount exceeds the wallet balance', async () => { + const user = userEvent.setup() + renderForm() + await user.type(payInput(), '999') // ETH balance is 10 + expect(screen.getAllByText(/insufficient balance/i).length).toBeGreaterThan(0) + expect(submitButton()).toBeDisabled() + }) + + it('computes the receive amount and enables submit for a valid amount', async () => { + const user = userEvent.setup() + renderForm() + await user.type(payInput(), '2') + expect(receiveInput().value).toBe('4,000') // 2 ETH * 2000 + expect(screen.getByRole('button', { name: 'CONFIRM SWAP' })).toBeEnabled() + }) + + it('flips the two tokens when the swap-direction button is clicked', async () => { + const user = userEvent.setup() + renderForm() + await user.click(screen.getByRole('button', { name: 'Swap direction' })) + expect(screen.getByRole('button', { name: 'You pay token: USDC' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'You receive token: ETH' })).toBeInTheDocument() + }) + + it('lets the user pick a new pay token, with the opposite token disabled', async () => { + const user = userEvent.setup() + renderForm() + await user.click(screen.getByRole('button', { name: 'You pay token: ETH' })) + + // The current receive token (USDC) must be disabled in the picker. + const dialog = within(screen.getByRole('dialog')) + expect(dialog.getByRole('button', { name: /USDC/ })).toBeDisabled() + + await user.click(dialog.getByRole('button', { name: /WBTC/ })) + expect(screen.getByRole('button', { name: 'You pay token: WBTC' })).toBeInTheDocument() + }) + + it('shows a loading state then reports success on submit', async () => { + const user = userEvent.setup() + const { onSuccess } = renderForm() + await user.type(payInput(), '2') + + await user.click(screen.getByRole('button', { name: 'CONFIRM SWAP' })) + + // Immediately after clicking, the mocked round-trip is in flight. + expect(screen.getByRole('button', { name: /confirming swap/i })).toBeDisabled() + + await waitFor( + () => { + expect(onSuccess).toHaveBeenCalledWith({ + fromAmount: 2, + from: 'ETH', + toAmount: 4000, + to: 'USDC', + }) + }, + { timeout: 3000 }, + ) + + // The amount resets after a successful swap. + expect(payInput().value).toBe('') + }) +}) diff --git a/src/problem2/src/components/TokenField.tsx b/src/problem2/src/components/TokenField.tsx index 56783ce6b6..0cbcab4158 100644 --- a/src/problem2/src/components/TokenField.tsx +++ b/src/problem2/src/components/TokenField.tsx @@ -73,6 +73,8 @@ export function TokenField({ + ) + })} +
+ ) +} diff --git a/src/problem2/src/components/SwapForm.test.tsx b/src/problem2/src/components/SwapForm.test.tsx index ba9fe73014..6fd02a21d6 100644 --- a/src/problem2/src/components/SwapForm.test.tsx +++ b/src/problem2/src/components/SwapForm.test.tsx @@ -2,12 +2,36 @@ import { describe, expect, it, vi } from 'vitest' import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { SwapForm } from './SwapForm' +import { SwapError, type SwapIntent, type SwapReceipt } from '../services/swapService' import { ETH, TEST_TOKENS, USDC } from '../test/fixtures' -function renderForm() { +/** Build a deterministic receipt from an intent, applying the trade to balances. */ +function receiptFor(intent: SwapIntent): SwapReceipt { + return { + txHash: '0xtest', + ...intent, + executedAt: 0, + newBalances: { + ETH: 10 - (intent.from === 'ETH' ? intent.fromAmount : 0), + USDC: 5000 + (intent.to === 'USDC' ? intent.toAmount : 0), + }, + } +} + +function renderForm( + overrides: Partial> = {}, +) { const onSuccess = vi.fn() - render() - return { onSuccess } + const onError = vi.fn() + // Default submit resolves on a short delay so the loading state is observable. + const onSubmit = vi.fn( + (intent: SwapIntent) => + new Promise((resolve) => setTimeout(() => resolve(receiptFor(intent)), 20)), + ) + render( + , + ) + return { onSuccess, onError, onSubmit } } const payInput = () => screen.getByLabelText('You pay amount') as HTMLInputElement @@ -96,29 +120,50 @@ describe('SwapForm', () => { expect(screen.getByRole('button', { name: 'You pay token: WBTC' })).toBeInTheDocument() }) - it('shows a loading state then reports success on submit', async () => { + it('shows a loading state then reports success (with adjusted balances) on submit', async () => { const user = userEvent.setup() const { onSuccess } = renderForm() await user.type(payInput(), '2') await user.click(screen.getByRole('button', { name: 'CONFIRM SWAP' })) - // Immediately after clicking, the mocked round-trip is in flight. + // Immediately after clicking, the round-trip is in flight. expect(screen.getByRole('button', { name: /confirming swap/i })).toBeDisabled() await waitFor( () => { - expect(onSuccess).toHaveBeenCalledWith({ - fromAmount: 2, - from: 'ETH', - toAmount: 4000, - to: 'USDC', - }) + expect(onSuccess).toHaveBeenCalledWith( + expect.objectContaining({ fromAmount: 2, from: 'ETH', toAmount: 4000, to: 'USDC' }), + ) }, { timeout: 3000 }, ) + // The receipt carries balances reduced for the "pay" side. + const receipt = onSuccess.mock.calls[0][0] as SwapReceipt + expect(receipt.newBalances.ETH).toBe(8) // 10 - 2 + // The amount resets after a successful swap. expect(payInput().value).toBe('') }) + + it('reports an error and re-enables submit when the swap fails', async () => { + const user = userEvent.setup() + const onError = vi.fn() + const onSubmit = vi.fn(() => Promise.reject(new SwapError('NETWORK'))) + const { onSuccess } = renderForm({ onSubmit, onError }) + await user.type(payInput(), '2') + + await user.click(screen.getByRole('button', { name: 'CONFIRM SWAP' })) + + await waitFor(() => expect(onError).toHaveBeenCalled()) + const [error, retry] = onError.mock.calls[0] + expect(error).toBeInstanceOf(SwapError) + expect(typeof retry).toBe('function') + + // No success, amount preserved for retry, button usable again. + expect(onSuccess).not.toHaveBeenCalled() + expect(payInput().value).toBe('2') + expect(screen.getByRole('button', { name: 'CONFIRM SWAP' })).toBeEnabled() + }) }) diff --git a/src/problem2/src/components/SwapForm.tsx b/src/problem2/src/components/SwapForm.tsx index 245617d4e2..63c4124886 100644 --- a/src/problem2/src/components/SwapForm.tsx +++ b/src/problem2/src/components/SwapForm.tsx @@ -1,13 +1,31 @@ import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import type { Token } from '../types' import { TokenField } from './TokenField' import { TokenSelectModal } from './TokenSelectModal' import { convert, exchangeRate } from '../utils/swap' import { formatTokenAmount, formatUsd, parseAmount } from '../utils/format' +import { localeFor } from '../i18n' +import { + executeSwap, + SwapError, + type SwapIntent, + type SwapReceipt, +} from '../services/swapService' interface SwapFormProps { tokens: Token[] - onSuccess: (summary: { fromAmount: number; from: string; toAmount: number; to: string }) => void + /** + * Executes the swap through a backend. Defaults to the mock service using the + * balances on `tokens`; the app injects the wallet-backed submit so balances + * persist across swaps. + */ + onSubmit?: (intent: SwapIntent) => Promise + onSuccess: (receipt: SwapReceipt) => void + /** Called with the error and a retry callback when a swap fails. */ + onError?: (error: SwapError, retry: () => void) => void + /** When provided, renders a "refresh rates" control in the header. */ + onRefreshRates?: () => void } type ActiveSide = 'from' | 'to' | null @@ -17,8 +35,10 @@ function pick(tokens: Token[], symbol: string, fallbackIndex: number): Token { return tokens.find((t) => t.symbol === symbol) ?? tokens[fallbackIndex] ?? tokens[0] } -/** The complete currency swap form: inputs, validation, and mocked submit. */ -export function SwapForm({ tokens, onSuccess }: SwapFormProps) { +/** The complete currency swap form: inputs, validation, and service-backed submit. */ +export function SwapForm({ tokens, onSubmit, onSuccess, onError, onRefreshRates }: SwapFormProps) { + const { t, i18n } = useTranslation() + const locale = localeFor(i18n.language) const [fromToken, setFromToken] = useState(() => pick(tokens, 'ETH', 0)) const [toToken, setToToken] = useState(() => pick(tokens, 'USDC', 1)) const [amount, setAmount] = useState('') @@ -40,19 +60,25 @@ export function SwapForm({ tokens, onSuccess }: SwapFormProps) { const fromUsd = Number.isFinite(parsedAmount) ? parsedAmount * fromToken.price : NaN const toUsd = Number.isFinite(receiveAmount) ? receiveAmount * toToken.price : NaN - const error = useMemo(() => { - if (amount.trim() === '') return 'Enter an amount to swap.' - if (Number.isNaN(parsedAmount)) return 'Enter a valid number.' - if (parsedAmount <= 0) return 'Amount must be greater than zero.' - if (fromToken.symbol === toToken.symbol) return 'Choose two different tokens.' + // Validation returns a translation key (+ params) rather than a finished + // string, so the message re-localizes automatically when the language changes. + const error = useMemo<{ key: string; params?: Record } | null>(() => { + if (amount.trim() === '') return { key: 'validation.enterAmount' } + if (Number.isNaN(parsedAmount)) return { key: 'validation.validNumber' } + if (parsedAmount <= 0) return { key: 'validation.greaterThanZero' } + if (fromToken.symbol === toToken.symbol) return { key: 'validation.differentTokens' } // Tolerate floating-point noise so an exact MAX (or a value equal to the // balance) is not wrongly rejected. The tolerance scales with magnitude. const balanceEpsilon = Math.max(1e-8, Math.abs(fromToken.balance) * 1e-9) if (parsedAmount > fromToken.balance + balanceEpsilon) - return `Insufficient balance - you hold ${formatTokenAmount(fromToken.balance)} ${fromToken.symbol}.` + return { + key: 'validation.insufficientBalance', + params: { amount: formatTokenAmount(fromToken.balance, locale), symbol: fromToken.symbol }, + } return null - }, [amount, parsedAmount, fromToken, toToken]) + }, [amount, parsedAmount, fromToken, toToken, locale]) + const errorMessage = error ? t(error.key, error.params) : null const showError = touched && error function handleSwapDirection() { @@ -85,22 +111,41 @@ export function SwapForm({ tokens, onSuccess }: SwapFormProps) { setAmount(String(max)) } - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - setTouched(true) + // Fallback submit used when the app doesn't inject a wallet-backed one: + // calls the mock backend directly using the balances on the current tokens. + async function defaultSubmit(intent: SwapIntent): Promise { + const balances = Object.fromEntries(tokens.map((tk) => [tk.symbol, tk.balance])) + return executeSwap({ ...intent, balances }) + } + + async function runSwap() { if (error || submitting) return - setSubmitting(true) - // Mocked backend round-trip. - await new Promise((resolve) => setTimeout(resolve, 1500)) - setSubmitting(false) - onSuccess({ - fromAmount: parsedAmount, + const intent: SwapIntent = { from: fromToken.symbol, - toAmount: receiveAmount, + fromAmount: parsedAmount, to: toToken.symbol, - }) - setAmount('') - setTouched(false) + toAmount: receiveAmount, + } + setSubmitting(true) + try { + const receipt = await (onSubmit ?? defaultSubmit)(intent) + onSuccess(receipt) + setAmount('') + setTouched(false) + } catch (e) { + const err = e instanceof SwapError ? e : new SwapError('NETWORK') + // Keep the amount so the user can retry the same trade. + onError?.(err, runSwap) + } finally { + setSubmitting(false) + } + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setTouched(true) + if (error) return + void runSwap() } return ( @@ -110,18 +155,32 @@ export function SwapForm({ tokens, onSuccess }: SwapFormProps) { >
-

Swap

-

Trade tokens in an instant

+

{t('form.title')}

+

{t('form.subtitle')}

+
+
+ + + {t('form.liveRates')} + + {onRefreshRates && ( + + )}
- - - Live rates -
setActiveSide('from')} onMax={handleMax} - error={showError ? error : undefined} + error={showError ? errorMessage ?? undefined : undefined} /> {/* Swap-direction button, overlapping the two fields */} @@ -139,7 +198,7 @@ export function SwapForm({ tokens, onSuccess }: SwapFormProps) {
@@ -205,20 +268,24 @@ export function SwapForm({ tokens, onSuccess }: SwapFormProps) { - Confirming swap... + {t('form.confirming')} ) : amount.trim() === '' ? ( - 'Enter an amount' - ) : error ? ( - error + t('form.enterAmountShort') + ) : errorMessage ? ( + errorMessage ) : ( - 'CONFIRM SWAP' + t('form.confirmSwap') )} {parsedAmount > 0 && !error && (

- You will receive ≈ {formatTokenAmount(receiveAmount)} {toToken.symbol} ({formatUsd(toUsd)}) + {t('form.receiveHint', { + amount: formatTokenAmount(receiveAmount, locale), + symbol: toToken.symbol, + usd: formatUsd(toUsd, locale), + })}

)} diff --git a/src/problem2/src/components/Toast.tsx b/src/problem2/src/components/Toast.tsx index b71b9758be..496450e006 100644 --- a/src/problem2/src/components/Toast.tsx +++ b/src/problem2/src/components/Toast.tsx @@ -1,36 +1,69 @@ import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' + +export type ToastVariant = 'success' | 'error' interface ToastProps { message: string detail?: string + variant?: ToastVariant + /** Optional retry action, shown as a button (used by the error toast). */ + onRetry?: () => void onDismiss: () => void } -/** A transient success toast that auto-dismisses. */ -export function Toast({ message, detail, onDismiss }: ToastProps) { +const VARIANTS = { + success: { + ring: 'border-emerald-400/25', + iconWrap: 'bg-emerald-500/20 text-emerald-400', + icon: , + }, + error: { + ring: 'border-rose-400/30', + iconWrap: 'bg-rose-500/20 text-rose-400', + icon: , + }, +} as const + +/** A transient toast that auto-dismisses; success (default) or error variant. */ +export function Toast({ message, detail, variant = 'success', onRetry, onDismiss }: ToastProps) { + const { t } = useTranslation() + useEffect(() => { - const t = setTimeout(onDismiss, 4200) - return () => clearTimeout(t) - }, [onDismiss]) + // Error toasts linger a little longer so the retry action is reachable. + const timeout = variant === 'error' ? 6000 : 4200 + const timer = setTimeout(onDismiss, timeout) + return () => clearTimeout(timer) + }, [onDismiss, variant]) + + const v = VARIANTS[variant] return (
- + - + {v.icon}

{message}

{detail &&

{detail}

} + {onRetry && ( + + )}
)}
@@ -63,8 +67,8 @@ export function TokenField({ type="text" value={value} readOnly={readOnly} - placeholder="0.0" - aria-label={`${label} amount`} + placeholder={t('field.amountPlaceholder')} + aria-label={t('a11y.fieldAmount', { label })} onChange={(e) => onValueChange?.(e.target.value)} className={`min-w-0 flex-1 bg-transparent text-3xl font-semibold tracking-tight text-white placeholder:text-slate-600 focus:outline-none ${readOnly ? 'cursor-default text-slate-100' : ''}`} @@ -73,7 +77,10 @@ export function TokenField({
- {token && value && Number.isFinite(usdValue) ? formatUsd(usdValue) : ' '} + {token && value && Number.isFinite(usdValue) ? formatUsd(usdValue, locale) : ' '}
) diff --git a/src/problem2/src/components/TokenIcon.tsx b/src/problem2/src/components/TokenIcon.tsx index 1f2ae1cb67..63ccc78655 100644 --- a/src/problem2/src/components/TokenIcon.tsx +++ b/src/problem2/src/components/TokenIcon.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' interface TokenIconProps { symbol: string @@ -12,6 +13,7 @@ interface TokenIconProps { * monogram badge when the image is missing or fails to load. */ export function TokenIcon({ symbol, iconUrl, size = 28, className = '' }: TokenIconProps) { + const { t } = useTranslation() const [failed, setFailed] = useState(false) const dimension = { width: size, height: size } @@ -38,7 +40,7 @@ export function TokenIcon({ symbol, iconUrl, size = 28, className = '' }: TokenI return ( {`${symbol}(null) @@ -59,7 +63,7 @@ export function TokenSelectModal({ className="fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-4" role="dialog" aria-modal="true" - aria-label="Select a token" + aria-label={t('modal.selectAToken')} >
-

Select a token

+

{t('modal.selectAToken')}

@@ -98,7 +102,7 @@ export function TokenSelectModal({
    {filtered.length === 0 && (
  • - No tokens match "{query}". + {t('modal.noMatch', { query })}
  • )} {filtered.map((token) => { @@ -117,12 +121,12 @@ export function TokenSelectModal({ {token.symbol} - {formatUsd(token.price)} + {formatUsd(token.price, locale)} - Balance + {t('modal.balance')} - {formatBalance(token.balance)} + {formatBalance(token.balance, locale)} diff --git a/src/problem2/src/hooks/usePrices.ts b/src/problem2/src/hooks/usePrices.ts index ace6fdad03..04aa7e12d7 100644 --- a/src/problem2/src/hooks/usePrices.ts +++ b/src/problem2/src/hooks/usePrices.ts @@ -1,9 +1,6 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import type { Token } from '../types' -import { buildTokens } from '../utils/swap' -import { FALLBACK_PRICES } from '../data/fallbackPrices' - -const PRICES_URL = 'https://interview.switcheo.com/prices.json' +import { fetchPrices } from '../services/pricesService' interface PricesState { tokens: Token[] @@ -12,12 +9,17 @@ interface PricesState { usingFallback: boolean } +interface UsePricesResult extends PricesState { + /** Re-fetch prices with a small live-market jitter so rates visibly move. */ + refresh: () => void +} + /** - * Loads token prices from the Switcheo endpoint and derives the tradable token - * list. Never throws: on any failure it falls back to a bundled snapshot so the - * form always renders. + * Loads token prices via the prices service (real fetch + graceful fallback) + * and exposes a `refresh` that simulates a live-market re-quote. Never throws: + * the service always resolves with data so the form always renders. */ -export function usePrices(): PricesState { +export function usePrices(): UsePricesResult { const [state, setState] = useState({ tokens: [], loading: true, @@ -26,30 +28,19 @@ export function usePrices(): PricesState { useEffect(() => { let cancelled = false - - async function load() { - // Small artificial delay so the loading skeleton is perceptible. - const settle = (tokens: Token[], usingFallback: boolean) => { - if (!cancelled) setState({ tokens, loading: false, usingFallback }) - } - - try { - const res = await fetch(PRICES_URL, { cache: 'no-store' }) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - const data = await res.json() - const tokens = buildTokens(data) - if (tokens.length === 0) throw new Error('No priced tokens') - settle(tokens, false) - } catch { - settle(buildTokens(FALLBACK_PRICES), true) - } - } - - load() + fetchPrices().then(({ tokens, usingFallback }) => { + if (!cancelled) setState({ tokens, loading: false, usingFallback }) + }) return () => { cancelled = true } }, []) - return state + const refresh = useCallback(() => { + fetchPrices({ jitter: true }).then(({ tokens, usingFallback }) => { + setState((prev) => ({ ...prev, tokens, usingFallback })) + }) + }, []) + + return { ...state, refresh } } diff --git a/src/problem2/src/hooks/useWallet.test.ts b/src/problem2/src/hooks/useWallet.test.ts new file mode 100644 index 0000000000..c2f45b9334 --- /dev/null +++ b/src/problem2/src/hooks/useWallet.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' +import { act, renderHook, waitFor } from '@testing-library/react' +import { useWallet } from './useWallet' +import { SwapError } from '../services/swapService' +import { ETH, TEST_TOKENS, USDC } from '../test/fixtures' + +const intent = { from: 'ETH', fromAmount: 2, to: 'USDC', toAmount: 4000 } + +describe('useWallet', () => { + it('seeds balances from the token list', async () => { + const { result } = renderHook(() => useWallet(TEST_TOKENS)) + await waitFor(() => expect(result.current.balances.ETH).toBe(ETH.balance)) + expect(result.current.balances.USDC).toBe(USDC.balance) + }) + + it('reduces the "from" balance and increases the "to" balance on a successful swap', async () => { + const { result } = renderHook(() => + useWallet(TEST_TOKENS, { delayMs: 0, random: () => 0.99 }), + ) + await waitFor(() => expect(result.current.balances.ETH).toBe(10)) + + await act(async () => { + await result.current.submit(intent) + }) + + expect(result.current.balances.ETH).toBe(8) // 10 - 2 + expect(result.current.balances.USDC).toBe(9000) // 5000 + 4000 + // Merged tokens reflect the new balance. + expect(result.current.tokens.find((t) => t.symbol === 'ETH')!.balance).toBe(8) + expect(result.current.swapState).toBe('idle') + }) + + it('leaves balances unchanged and exposes the error on a failed swap', async () => { + const { result } = renderHook(() => + useWallet(TEST_TOKENS, { delayMs: 0, failureRate: 1, random: () => 0 }), + ) + await waitFor(() => expect(result.current.balances.ETH).toBe(10)) + + await act(async () => { + await expect(result.current.submit(intent)).rejects.toBeInstanceOf(SwapError) + }) + + expect(result.current.balances.ETH).toBe(10) + expect(result.current.balances.USDC).toBe(5000) + expect(result.current.swapState).toBe('error') + expect(result.current.error?.code).toBe('NETWORK') + }) +}) diff --git a/src/problem2/src/hooks/useWallet.ts b/src/problem2/src/hooks/useWallet.ts new file mode 100644 index 0000000000..5cbf1bc65b --- /dev/null +++ b/src/problem2/src/hooks/useWallet.ts @@ -0,0 +1,77 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import type { Token } from '../types' +import { + executeSwap, + SwapError, + type SwapIntent, + type SwapOptions, + type SwapReceipt, +} from '../services/swapService' + +export type SwapState = 'idle' | 'submitting' | 'error' + +export interface WalletApi { + /** The input tokens with their balances overlaid from the live wallet. */ + tokens: Token[] + /** Current balances keyed by symbol. */ + balances: Record + swapState: SwapState + error: SwapError | null + /** Execute a swap through the mock backend and apply the resulting balances. */ + submit: (intent: SwapIntent) => Promise +} + +/** + * Owns the (mutable) wallet balances and orchestrates swaps through the mock + * backend. Balances are seeded from the incoming token list and then evolve as + * swaps succeed, so the UI reflects real post-trade holdings. `opts` lets tests + * inject deterministic latency / RNG. + */ +export function useWallet(tokens: Token[], opts?: SwapOptions): WalletApi { + const [balances, setBalances] = useState>({}) + const [swapState, setSwapState] = useState('idle') + const [error, setError] = useState(null) + + // Seed balances for any newly-seen symbol, but preserve balances we already + // track — so a price refresh (new token identities) never resets holdings. + useEffect(() => { + if (tokens.length === 0) return + setBalances((prev) => { + let changed = false + const next = { ...prev } + for (const t of tokens) { + if (!(t.symbol in next)) { + next[t.symbol] = t.balance + changed = true + } + } + return changed ? next : prev + }) + }, [tokens]) + + const mergedTokens = useMemo( + () => tokens.map((t) => ({ ...t, balance: balances[t.symbol] ?? t.balance })), + [tokens, balances], + ) + + const submit = useCallback( + async (intent: SwapIntent): Promise => { + setSwapState('submitting') + setError(null) + try { + const receipt = await executeSwap({ ...intent, balances }, opts) + setBalances(receipt.newBalances) + setSwapState('idle') + return receipt + } catch (e) { + const err = e instanceof SwapError ? e : new SwapError('NETWORK') + setError(err) + setSwapState('error') + throw err + } + }, + [balances, opts], + ) + + return { tokens: mergedTokens, balances, swapState, error, submit } +} diff --git a/src/problem2/src/i18n/i18n.test.ts b/src/problem2/src/i18n/i18n.test.ts new file mode 100644 index 0000000000..1d98b168a4 --- /dev/null +++ b/src/problem2/src/i18n/i18n.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest' +import i18n from './index' +import en from './locales/en.json' +import viCatalog from './locales/vi.json' + +/** Recursively collect dotted key paths from a nested catalog object. */ +function keyPaths(obj: Record, prefix = ''): string[] { + return Object.entries(obj).flatMap(([k, v]) => { + const path = prefix ? `${prefix}.${k}` : k + return typeof v === 'object' && v !== null + ? keyPaths(v as Record, path) + : [path] + }) +} + +/** Collect every {{placeholder}} name from a catalog's leaf strings. */ +function placeholders(obj: Record): Record { + const out: Record = {} + const walk = (o: Record, prefix = '') => { + for (const [k, v] of Object.entries(o)) { + const path = prefix ? `${prefix}.${k}` : k + if (typeof v === 'object' && v !== null) walk(v as Record, path) + else if (typeof v === 'string') { + out[path] = [...v.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]).sort() + } + } + } + walk(obj) + return out +} + +describe('i18n catalogs', () => { + it('vi exposes exactly the same key set as en', () => { + expect(keyPaths(viCatalog).sort()).toEqual(keyPaths(en).sort()) + }) + + it('vi keeps the same interpolation placeholders as en', () => { + expect(placeholders(viCatalog)).toEqual(placeholders(en)) + }) + + it('resolves interpolated keys in both languages', async () => { + await i18n.changeLanguage('en') + expect(i18n.t('form.rate', { fromSymbol: 'ETH', rate: '2,000', toSymbol: 'USDC' })).toBe( + '1 ETH = 2,000 USDC', + ) + expect(i18n.t('validation.insufficientBalance', { amount: '10', symbol: 'ETH' })).toBe( + 'Insufficient balance - you hold 10 ETH.', + ) + + await i18n.changeLanguage('vi') + expect(i18n.t('form.confirmSwap')).toBe('XÁC NHẬN ĐỔI') + expect(i18n.t('validation.insufficientBalance', { amount: '10', symbol: 'ETH' })).toBe( + 'Số dư không đủ - bạn đang có 10 ETH.', + ) + + await i18n.changeLanguage('en') + }) +}) diff --git a/src/problem2/src/i18n/index.ts b/src/problem2/src/i18n/index.ts new file mode 100644 index 0000000000..804e04ce45 --- /dev/null +++ b/src/problem2/src/i18n/index.ts @@ -0,0 +1,54 @@ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import LanguageDetector from 'i18next-browser-languagedetector' +import en from './locales/en.json' +import viJson from './locales/vi.json' +import type { TranslationCatalog } from './locales/types' + +// Compile-time guard: every locale must expose exactly the English key set. +const vi: TranslationCatalog = viJson + +export const SUPPORTED_LANGUAGES = ['en', 'vi'] as const +export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number] + +/** Map an i18next language code to a BCP-47 locale for number formatting. */ +export function localeFor(language: string): string { + return language.startsWith('vi') ? 'vi-VN' : 'en-US' +} + +// Resources are bundled inline (no HTTP backend) so translations resolve +// synchronously and neither the UI nor the tests need to await a load. +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + en: { translation: en }, + vi: { translation: vi }, + }, + fallbackLng: 'en', + supportedLngs: SUPPORTED_LANGUAGES as unknown as string[], + detection: { + order: ['localStorage', 'navigator'], + caches: ['localStorage'], + }, + interpolation: { + // React already escapes; our values contain characters like → and " that + // must not be HTML-escaped. + escapeValue: false, + }, + react: { + useSuspense: false, + }, + }) + +// Keep the document language attribute in sync with the active locale. +if (typeof document !== 'undefined') { + const applyLang = (lng: string) => { + document.documentElement.lang = lng.startsWith('vi') ? 'vi' : 'en' + } + applyLang(i18n.language ?? 'en') + i18n.on('languageChanged', applyLang) +} + +export default i18n diff --git a/src/problem2/src/i18n/locales/en.json b/src/problem2/src/i18n/locales/en.json new file mode 100644 index 0000000000..cb532d59ec --- /dev/null +++ b/src/problem2/src/i18n/locales/en.json @@ -0,0 +1,60 @@ +{ + "app": { + "title": "Swap · Currency Exchange", + "fallbackBanner": "Live prices are unreachable - showing a bundled snapshot so you can still explore the form.", + "footer": "Prices from interview.switcheo.com · Demo only, no real transactions" + }, + "form": { + "title": "Swap", + "subtitle": "Trade tokens in an instant", + "liveRates": "Live rates", + "youPay": "You pay", + "youReceive": "You receive", + "rateLabel": "Rate", + "rate": "1 {{fromSymbol}} = {{rate}} {{toSymbol}}", + "confirming": "Confirming swap...", + "enterAmountShort": "Enter an amount", + "confirmSwap": "CONFIRM SWAP", + "receiveHint": "You will receive ≈ {{amount}} {{symbol}} ({{usd}})" + }, + "validation": { + "enterAmount": "Enter an amount to swap.", + "validNumber": "Enter a valid number.", + "greaterThanZero": "Amount must be greater than zero.", + "differentTokens": "Choose two different tokens.", + "insufficientBalance": "Insufficient balance - you hold {{amount}} {{symbol}}." + }, + "field": { + "balance": "Balance:", + "max": "MAX", + "amountPlaceholder": "0.0", + "select": "Select", + "selectAria": "select" + }, + "modal": { + "selectAToken": "Select a token", + "searchPlaceholder": "Search name or ticker", + "noMatch": "No tokens match \"{{query}}\".", + "balance": "Balance" + }, + "toast": { + "swapConfirmed": "Swap confirmed", + "swapDetail": "{{fromAmount}} {{from}} → {{toAmount}} {{to}}", + "swapFailed": "Swap failed", + "retry": "Retry" + }, + "errors": { + "network": "Network error - please try again.", + "insufficientLiquidity": "Insufficient liquidity for this trade.", + "generic": "Something went wrong - please try again." + }, + "a11y": { + "swapDirection": "Swap direction", + "fieldAmount": "{{label}} amount", + "fieldToken": "{{label}} token: {{symbol}}", + "close": "Close", + "dismiss": "Dismiss", + "tokenIcon": "{{symbol}} icon", + "refreshRates": "Refresh rates" + } +} diff --git a/src/problem2/src/i18n/locales/types.ts b/src/problem2/src/i18n/locales/types.ts new file mode 100644 index 0000000000..651741c293 --- /dev/null +++ b/src/problem2/src/i18n/locales/types.ts @@ -0,0 +1,8 @@ +import en from './en.json' + +/** + * The catalog shape, derived from the English JSON (JSON imports are typed with + * `string` leaves). This powers `t()`'s compile-time key checking via the + * react-i18next module augmentation, and other locales are checked against it. + */ +export type TranslationCatalog = typeof en diff --git a/src/problem2/src/i18n/locales/vi.json b/src/problem2/src/i18n/locales/vi.json new file mode 100644 index 0000000000..bf121861d1 --- /dev/null +++ b/src/problem2/src/i18n/locales/vi.json @@ -0,0 +1,60 @@ +{ + "app": { + "title": "Swap · Sàn đổi tiền", + "fallbackBanner": "Không thể lấy giá trực tiếp - đang hiển thị dữ liệu ngoại tuyến để bạn vẫn dùng thử biểu mẫu.", + "footer": "Giá từ interview.switcheo.com · Chỉ để minh hoạ, không có giao dịch thật" + }, + "form": { + "title": "Đổi token", + "subtitle": "Đổi token trong tích tắc", + "liveRates": "Giá trực tiếp", + "youPay": "Bạn trả", + "youReceive": "Bạn nhận", + "rateLabel": "Tỷ giá", + "rate": "1 {{fromSymbol}} = {{rate}} {{toSymbol}}", + "confirming": "Đang xác nhận...", + "enterAmountShort": "Nhập số lượng", + "confirmSwap": "XÁC NHẬN ĐỔI", + "receiveHint": "Bạn sẽ nhận ≈ {{amount}} {{symbol}} ({{usd}})" + }, + "validation": { + "enterAmount": "Nhập số lượng cần đổi.", + "validNumber": "Nhập một số hợp lệ.", + "greaterThanZero": "Số lượng phải lớn hơn 0.", + "differentTokens": "Chọn hai token khác nhau.", + "insufficientBalance": "Số dư không đủ - bạn đang có {{amount}} {{symbol}}." + }, + "field": { + "balance": "Số dư:", + "max": "TỐI ĐA", + "amountPlaceholder": "0.0", + "select": "Chọn", + "selectAria": "chọn" + }, + "modal": { + "selectAToken": "Chọn token", + "searchPlaceholder": "Tìm theo tên hoặc mã", + "noMatch": "Không có token nào khớp \"{{query}}\".", + "balance": "Số dư" + }, + "toast": { + "swapConfirmed": "Đã xác nhận đổi", + "swapDetail": "{{fromAmount}} {{from}} → {{toAmount}} {{to}}", + "swapFailed": "Đổi thất bại", + "retry": "Thử lại" + }, + "errors": { + "network": "Lỗi mạng - vui lòng thử lại.", + "insufficientLiquidity": "Không đủ thanh khoản cho giao dịch này.", + "generic": "Đã xảy ra lỗi - vui lòng thử lại." + }, + "a11y": { + "swapDirection": "Đổi chiều", + "fieldAmount": "Số lượng {{label}}", + "fieldToken": "{{label}} token: {{symbol}}", + "close": "Đóng", + "dismiss": "Bỏ qua", + "tokenIcon": "Biểu tượng {{symbol}}", + "refreshRates": "Làm mới tỷ giá" + } +} diff --git a/src/problem2/src/i18n/react-i18next.d.ts b/src/problem2/src/i18n/react-i18next.d.ts new file mode 100644 index 0000000000..d7051d8601 --- /dev/null +++ b/src/problem2/src/i18n/react-i18next.d.ts @@ -0,0 +1,12 @@ +import 'react-i18next' +import type { TranslationCatalog } from './locales/types' + +// Give `t()` compile-time key checking against the English catalog. +declare module 'react-i18next' { + interface CustomTypeOptions { + defaultNS: 'translation' + resources: { + translation: TranslationCatalog + } + } +} diff --git a/src/problem2/src/main.tsx b/src/problem2/src/main.tsx index bef5202a32..cd81cee26b 100644 --- a/src/problem2/src/main.tsx +++ b/src/problem2/src/main.tsx @@ -1,6 +1,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' +import './i18n' import App from './App.tsx' createRoot(document.getElementById('root')!).render( diff --git a/src/problem2/src/services/pricesService.test.ts b/src/problem2/src/services/pricesService.test.ts new file mode 100644 index 0000000000..00ccdd18e2 --- /dev/null +++ b/src/problem2/src/services/pricesService.test.ts @@ -0,0 +1,53 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { fetchPrices } from './pricesService' +import { FALLBACK_PRICES } from '../data/fallbackPrices' + +const OK_PAYLOAD = [ + { currency: 'ETH', date: '2023-08-29T07:10:52.000Z', price: 1645.93 }, + { currency: 'USDC', date: '2023-08-29T07:10:40.000Z', price: 0.9898 }, +] + +describe('fetchPrices', () => { + beforeEach(() => { + vi.restoreAllMocks() + }) + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('returns built tokens from the network on success', async () => { + vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, json: async () => OK_PAYLOAD }) as Response)) + + const { tokens, usingFallback } = await fetchPrices() + expect(usingFallback).toBe(false) + expect(tokens.map((t) => t.symbol)).toEqual(['ETH', 'USDC']) + }) + + it('falls back to the bundled snapshot when the request fails', async () => { + vi.stubGlobal('fetch', vi.fn(async () => { + throw new Error('offline') + })) + + const { tokens, usingFallback } = await fetchPrices() + expect(usingFallback).toBe(true) + expect(tokens.length).toBe(FALLBACK_PRICES.length) + }) + + it('falls back when the response is not ok', async () => { + vi.stubGlobal('fetch', vi.fn(async () => ({ ok: false, status: 500 }) as Response)) + + const { usingFallback } = await fetchPrices() + expect(usingFallback).toBe(true) + }) + + it('jitter changes prices but not the set of symbols', async () => { + vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, json: async () => OK_PAYLOAD }) as Response)) + + const plain = await fetchPrices() + const jittered = await fetchPrices({ jitter: true, random: () => 0.9 }) + + expect(jittered.tokens.map((t) => t.symbol)).toEqual(plain.tokens.map((t) => t.symbol)) + // With random() = 0.9, drift = 1.016, so every price shifts. + expect(jittered.tokens[0].price).not.toBe(plain.tokens[0].price) + }) +}) diff --git a/src/problem2/src/services/pricesService.ts b/src/problem2/src/services/pricesService.ts new file mode 100644 index 0000000000..8664217b20 --- /dev/null +++ b/src/problem2/src/services/pricesService.ts @@ -0,0 +1,44 @@ +import type { Token } from '../types' +import { buildTokens } from '../utils/swap' +import { FALLBACK_PRICES } from '../data/fallbackPrices' + +const PRICES_URL = 'https://interview.switcheo.com/prices.json' + +export interface FetchPricesResult { + tokens: Token[] + /** True when the live request failed and the bundled snapshot was used. */ + usingFallback: boolean +} + +export interface FetchPricesOptions { + /** Apply a small random price drift to simulate a live market on refresh. */ + jitter?: boolean + /** Injectable RNG for deterministic tests. Defaults to Math.random. */ + random?: () => number +} + +/** Apply up to +/-2% drift per token so a manual refresh visibly moves rates. */ +function applyJitter(tokens: Token[], random: () => number): Token[] { + return tokens.map((tk) => ({ ...tk, price: tk.price * (1 + (random() - 0.5) * 0.04) })) +} + +/** + * Fetches live token prices from the Switcheo endpoint and derives the tradable + * token list. Never throws: on any failure it returns the bundled snapshot with + * `usingFallback: true`, so the UI always has data to render. + */ +export async function fetchPrices(opts: FetchPricesOptions = {}): Promise { + const maybeJitter = (tokens: Token[]) => + opts.jitter ? applyJitter(tokens, opts.random ?? Math.random) : tokens + + try { + const res = await fetch(PRICES_URL, { cache: 'no-store' }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + const tokens = buildTokens(data) + if (tokens.length === 0) throw new Error('No priced tokens') + return { tokens: maybeJitter(tokens), usingFallback: false } + } catch { + return { tokens: maybeJitter(buildTokens(FALLBACK_PRICES)), usingFallback: true } + } +} diff --git a/src/problem2/src/services/swapService.test.ts b/src/problem2/src/services/swapService.test.ts new file mode 100644 index 0000000000..44bfcc567d --- /dev/null +++ b/src/problem2/src/services/swapService.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it, vi } from 'vitest' +import { executeSwap, SwapError, type SwapRequest } from './swapService' + +const baseReq: SwapRequest = { + from: 'ETH', + fromAmount: 2, + to: 'USDC', + toAmount: 4000, + balances: { ETH: 10, USDC: 5000 }, +} + +describe('executeSwap', () => { + it('returns a receipt with balances adjusted for the trade on success', async () => { + // random() >= failureRate -> success path + const receipt = await executeSwap(baseReq, { delayMs: 0, random: () => 0.99 }) + + expect(receipt.newBalances.ETH).toBe(8) // 10 - 2 + expect(receipt.newBalances.USDC).toBe(9000) // 5000 + 4000 + expect(receipt.from).toBe('ETH') + expect(receipt.to).toBe('USDC') + expect(receipt.txHash).toMatch(/^0x[0-9a-f]+$/) + // Original balances object is not mutated. + expect(baseReq.balances.ETH).toBe(10) + }) + + it('does not let a balance go negative', async () => { + const receipt = await executeSwap( + { ...baseReq, fromAmount: 999, balances: { ETH: 10, USDC: 5000 } }, + { delayMs: 0, random: () => 0.99 }, + ) + expect(receipt.newBalances.ETH).toBe(0) + }) + + it('throws a typed SwapError when the simulated call fails', async () => { + // random() < failureRate -> failure path + await expect( + executeSwap(baseReq, { delayMs: 0, failureRate: 1, random: () => 0 }), + ).rejects.toBeInstanceOf(SwapError) + + await expect( + executeSwap(baseReq, { delayMs: 0, failureRate: 1, random: () => 0 }), + ).rejects.toMatchObject({ code: 'NETWORK' }) + }) + + it('respects the configured latency', async () => { + vi.useFakeTimers() + try { + let done = false + const promise = executeSwap(baseReq, { delayMs: 1500, random: () => 0.99 }).then((r) => { + done = true + return r + }) + + await vi.advanceTimersByTimeAsync(1499) + expect(done).toBe(false) + + await vi.advanceTimersByTimeAsync(1) + const receipt = await promise + expect(done).toBe(true) + expect(receipt.newBalances.ETH).toBe(8) + } finally { + vi.useRealTimers() + } + }) +}) diff --git a/src/problem2/src/services/swapService.ts b/src/problem2/src/services/swapService.ts new file mode 100644 index 0000000000..54e3c6c09f --- /dev/null +++ b/src/problem2/src/services/swapService.ts @@ -0,0 +1,104 @@ +/** + * Mock "backend" for executing a swap. It behaves like a real service call: + * network-like latency, intermittent failures, and — on success — a receipt + * whose `newBalances` reflect the trade (the "pay" token goes down, the + * "receive" token goes up). All non-determinism (latency, RNG, clock) is + * injectable so tests are fully deterministic. + */ + +export type SwapErrorCode = 'NETWORK' | 'INSUFFICIENT_LIQUIDITY' + +/** i18n key each error code maps to, so the UI can translate it. */ +export const SWAP_ERROR_MESSAGE_KEY: Record = { + NETWORK: 'errors.network', + INSUFFICIENT_LIQUIDITY: 'errors.insufficientLiquidity', +} + +export class SwapError extends Error { + code: SwapErrorCode + constructor(code: SwapErrorCode) { + super(code) + this.name = 'SwapError' + this.code = code + } +} + +/** What the UI knows when the user confirms: the trade, without wallet state. */ +export interface SwapIntent { + from: string + fromAmount: number + to: string + toAmount: number +} + +export interface SwapRequest extends SwapIntent { + /** Current wallet balances keyed by token symbol. */ + balances: Record +} + +export interface SwapReceipt { + txHash: string + from: string + fromAmount: number + to: string + toAmount: number + executedAt: number + /** Balances after applying the trade. */ + newBalances: Record +} + +export interface SwapOptions { + /** Fixed latency in ms; defaults to a jittered 800-1600ms. */ + delayMs?: number + /** Probability [0,1] of a simulated failure. Defaults to 0.12. */ + failureRate?: number + /** Injectable RNG for deterministic tests. Defaults to Math.random. */ + random?: () => number + /** Injectable clock. Defaults to Date.now. */ + now?: () => number +} + +// Monotonic counter so tx hashes are unique without relying on the clock. +let txCounter = 0 + +function makeTxHash(req: SwapRequest, seed: number): string { + const base = `${req.from}-${req.to}-${req.fromAmount}-${seed}` + let hash = 0 + for (let i = 0; i < base.length; i++) hash = (hash * 31 + base.charCodeAt(i)) >>> 0 + return '0x' + hash.toString(16).padStart(8, '0') + seed.toString(16).padStart(4, '0') +} + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +export async function executeSwap(req: SwapRequest, opts: SwapOptions = {}): Promise { + const random = opts.random ?? Math.random + const failureRate = opts.failureRate ?? 0.12 + const now = opts.now ?? Date.now + const delayMs = opts.delayMs ?? 800 + Math.floor(random() * 800) + + await delay(delayMs) + + // Simulate an intermittent backend failure. + if (random() < failureRate) { + const code: SwapErrorCode = random() < 0.5 ? 'NETWORK' : 'INSUFFICIENT_LIQUIDITY' + throw new SwapError(code) + } + + const currentFrom = req.balances[req.from] ?? 0 + const currentTo = req.balances[req.to] ?? 0 + const newBalances: Record = { + ...req.balances, + [req.from]: Math.max(0, currentFrom - req.fromAmount), + [req.to]: currentTo + req.toAmount, + } + + return { + txHash: makeTxHash(req, ++txCounter), + from: req.from, + fromAmount: req.fromAmount, + to: req.to, + toAmount: req.toAmount, + executedAt: now(), + newBalances, + } +} diff --git a/src/problem2/src/test/setup.ts b/src/problem2/src/test/setup.ts index 9278a0500e..0130a7c901 100644 --- a/src/problem2/src/test/setup.ts +++ b/src/problem2/src/test/setup.ts @@ -10,7 +10,15 @@ import '@testing-library/jest-dom/vitest' import { afterEach } from 'vitest' import { cleanup } from '@testing-library/react' +// Initialize i18n once so `t()` resolves to real strings (default 'en') in +// every test without any per-file setup. +import i18n from '../i18n' afterEach(() => { cleanup() + // Reset to the default locale so a test that switches language can't leak + // into the next one via the shared i18next singleton / localStorage cache. + if (i18n.language !== 'en') { + i18n.changeLanguage('en') + } }) diff --git a/src/problem2/src/utils/format.ts b/src/problem2/src/utils/format.ts index 0f7d1deb07..3b2153240f 100644 --- a/src/problem2/src/utils/format.ts +++ b/src/problem2/src/utils/format.ts @@ -8,8 +8,15 @@ export function parseAmount(raw: string): number { return Number(raw.replace(/,/g, '').trim()) } +/** + * Formatters take an optional BCP-47 `locale` so grouping/decimal separators + * follow the active language. It defaults to `'en-US'`, which keeps existing + * callers (and their tests) byte-for-byte unchanged; the UI passes a locale + * derived from the active i18n language. + */ + /** Format a token amount with a sensible, magnitude-aware number of decimals. */ -export function formatTokenAmount(value: number): string { +export function formatTokenAmount(value: number, locale = 'en-US'): string { if (!Number.isFinite(value)) return '0' if (value === 0) return '0' const abs = Math.abs(value) @@ -18,18 +25,18 @@ export function formatTokenAmount(value: number): string { else if (abs >= 1) decimals = 4 else if (abs >= 0.0001) decimals = 6 else decimals = 8 - return value.toLocaleString('en-US', { + return value.toLocaleString(locale, { minimumFractionDigits: 0, maximumFractionDigits: decimals, }) } /** Format a USD value as currency. */ -export function formatUsd(value: number): string { +export function formatUsd(value: number, locale = 'en-US'): string { if (!Number.isFinite(value)) return '$0.00' const abs = Math.abs(value) const decimals = abs > 0 && abs < 0.01 ? 6 : 2 - return value.toLocaleString('en-US', { + return value.toLocaleString(locale, { style: 'currency', currency: 'USD', minimumFractionDigits: 2, @@ -38,8 +45,8 @@ export function formatUsd(value: number): string { } /** Compact display of a wallet balance. */ -export function formatBalance(value: number): string { - return value.toLocaleString('en-US', { +export function formatBalance(value: number, locale = 'en-US'): string { + return value.toLocaleString(locale, { minimumFractionDigits: 0, maximumFractionDigits: value >= 1 ? 4 : 6, }) diff --git a/src/problem2/tsconfig.app.json b/src/problem2/tsconfig.app.json index cb720650c9..736e1da31d 100644 --- a/src/problem2/tsconfig.app.json +++ b/src/problem2/tsconfig.app.json @@ -8,6 +8,7 @@ "moduleResolution": "bundler", "allowImportingTsExtensions": true, + "resolveJsonModule": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, diff --git a/src/problem3/README.md b/src/problem3/README.md index 154bf405db..fa7f43e99d 100644 --- a/src/problem3/README.md +++ b/src/problem3/README.md @@ -3,7 +3,7 @@ Analysis of the computational inefficiencies and anti-patterns in the original `WalletPage` component, grouped by category. Each item states **what** is wrong, **why** it matters, and **how** to fix it. The corrected implementation lives in -[`refactored.tsx`](./refactored.tsx), and the pure business logic it relies on is +[`src/refactored.tsx`](./src/refactored.tsx), and the pure business logic it relies on is extracted into [`src/walletLogic.ts`](./src/walletLogic.ts) and unit tested in [`src/walletLogic.test.ts`](./src/walletLogic.test.ts). @@ -216,8 +216,8 @@ covered by unit tests: ``` src/problem3/ -├── refactored.tsx # thin React component; imports the logic below ├── src/ +│ ├── refactored.tsx # thin React component; imports the logic below │ ├── walletLogic.ts # pure logic: types, getPriority, filter/sort/format │ └── walletLogic.test.ts # Vitest unit + regression tests (18 cases) ├── package.json # private; vitest + typescript dev deps diff --git a/src/problem3/refactored.tsx b/src/problem3/src/refactored.tsx similarity index 95% rename from src/problem3/refactored.tsx rename to src/problem3/src/refactored.tsx index de933a76a1..a613cf7a9c 100644 --- a/src/problem3/refactored.tsx +++ b/src/problem3/src/refactored.tsx @@ -4,13 +4,13 @@ import { type FormattedWalletBalance, type Prices, type WalletBalance, -} from './src/walletLogic'; +} from './walletLogic'; /** * Refactored version of the Problem 3 WalletPage. * * The component is intentionally thin: all the business logic (filtering, - * sorting, formatting, USD valuation) lives in `./src/walletLogic.ts`, which is + * sorting, formatting, USD valuation) lives in `./walletLogic.ts`, which is * pure and unit tested. This file only wires that logic into React. * * The design-system / data-hook symbols below (`Box`, `useWalletBalances`, diff --git a/src/problem3/tsconfig.json b/src/problem3/tsconfig.json index 14dfe05b72..9985f09c11 100644 --- a/src/problem3/tsconfig.json +++ b/src/problem3/tsconfig.json @@ -21,5 +21,5 @@ "isolatedModules": true, "noEmit": true }, - "include": ["src", "refactored.tsx", "vitest.config.ts"] + "include": ["src", "vitest.config.ts"] }