diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..a547bf36d8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000000..eb0fc86e23
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+pnpm lint && pnpm test
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..c2e4ada032
--- /dev/null
+++ b/README.md
@@ -0,0 +1,127 @@
+# Assessment
+
+A React + TypeScript + Vite project for the 99tech technical assessment. The app is organized into 3 independent problems, each accessible via a tab in the UI.
+
+## Tech Stack
+
+- **React 19** + **TypeScript**
+- **Vite 8**
+- **Tailwind CSS v4** (via `@tailwindcss/vite`)
+- **react-hook-form** — form validation
+- **@tanstack/react-query** — async data fetching + client state
+- Mock i18n utility (`src/lib/i18n.ts`)
+
+## Project Structure
+
+```
+src/
+├── components/
+│ ├── Task.tsx # Shared problem-statement card
+│ └── TokenSelector.tsx # Reusable token dropdown with icon
+├── lib/
+│ └── i18n.ts # Mock i18n / translation helper
+├── problems/
+│ ├── problem1/ # Sum to N
+│ │ ├── index.tsx
+│ │ ├── solutions.ts
+│ │ └── README.md
+│ ├── problem2/ # Currency Swap Form
+│ │ ├── index.tsx
+│ │ ├── logic.ts
+│ │ ├── logic.test.ts
+│ │ ├── types.ts
+│ │ ├── useSwapForm.ts
+│ │ └── README.md
+│ └── problem3/
+│ ├── index.tsx
+│ └── README.md
+├── App.tsx # Tab navigation
+└── index.css # Global styles + Tailwind entry
+```
+
+## Problems
+
+### Problem 1 — Sum to N
+
+> Provide 3 unique implementations of the following function in JavaScript.
+>
+> **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`.
+
+**Approach**
+
+| | Approach | Time | Space |
+| --- | ---------------------------- | ---- | ----- |
+| A | Gaussian formula `n*(n+1)/2` | O(1) | O(1) |
+| B | Iterative `for` loop | O(n) | O(1) |
+| C | Recursion `n + sum(n-1)` | O(n) | O(n) |
+
+
+
+### Problem 2 — Currency Swap Form
+
+> Create a currency swap form based on the template provided in the folder. A user would use this form to swap assets from one currency to another.
+>
+> _You may use any third party plugin, library, and/or framework for this problem._
+>
+> 1. You may add input validation/error messages to make the form interactive.
+> 2. Your submission will be rated on its usage intuitiveness and visual attractiveness.
+> 3. Show us your frontend development and design skills, feel free to totally disregard the provided files for this problem.
+> 4. You may use this [repo](https://github.com/Switcheo/token-icons/tree/main/tokens) for token images, e.g. [SVG image](https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens/SWTH.svg).
+> 5. You may use this [URL](https://interview.switcheo.com/prices.json) for token price information and to compute exchange rates (not every token has a price, those that do not can be omitted).
+>
+> ✨ Bonus: extra points if you use [Vite](https://vite.dev/) for this task!
+>
+> 💡 Hint: feel free to simulate or mock interactions with a backend service, e.g. implement a loading indicator with a timeout delay for the submit button is good enough.
+
+**Approach**
+
+- Prices fetched via `@tanstack/react-query`; deduplicated by keeping the latest date entry per token; zero/negative prices discarded
+- Exchange rate: `fromToken.price / toToken.price`; output recomputes on every keystroke
+- Banker's rounding (round-half-to-even) with magnitude-adaptive decimal precision (2 dp for ≥100k, up to 8+ dp for very small values)
+- Flip button swaps the two token selectors; amount stays unchanged, rate inverts automatically
+- Simulated 1.5 s swap with loading spinner; last 10 swaps shown in history panel
+- Network errors and HTTP errors surfaced with distinct messages; token images from the Switcheo token-icons repo
+
+### Problem 3 — Messy React
+
+> List out the computational inefficiencies and anti-patterns found in the code block below.
+>
+> This code block uses ReactJS with TypeScript, functional components, and React Hooks.
+>
+> You should also provide a refactored version of the code, but more points are awarded to accurately stating the issues and explaining correctly how to improve them.
+
+**Issues identified**
+
+| # | Issue | Category |
+| --- | ------------------------------------------------------------------------------------------------------- | -------------------- |
+| 1 | `lhsPriority` used in `filter` but never declared — `balancePriority` was assigned instead | Bug / ReferenceError |
+| 2 | Filter logic inverted — keeps balances with `amount <= 0`, discards positive balances | Logic bug |
+| 3 | `prices` in `useMemo` dependency array but never read inside the memo | Spurious dependency |
+| 4 | `getPriority` re-created on every render (defined inside component, not memoised) | Performance |
+| 5 | `formattedBalances` computed but `rows` iterates `sortedBalances` instead — formatted values never used | Dead computation |
+| 6 | `rows` types each element as `FormattedWalletBalance` but the source array is `WalletBalance` | Type mismatch |
+| 7 | `balance.formatted` accessed on `WalletBalance` which has no `formatted` field | Runtime error |
+| 8 | `balance.amount.toFixed()` — no precision argument; produces integer string | Precision bug |
+| 9 | `key={index}` on a sorted list — index keys are unstable after re-sort | React anti-pattern |
+| 10 | `sort` comparator has no return for equal priorities — returns `undefined` (treated as 0) | Sort instability |
+| 11 | `blockchain` typed as `any` in `getPriority` — defeats TypeScript safety | Type safety |
+| 12 | Unused `children` destructured from props but never rendered | Dead code |
+
+
+
+## Demo
+
+
+
+> Video not rendering? Download directly: [video_demo.mov](video_demo.mov)
+
+## Getting Started
+
+```bash
+pnpm install
+pnpm dev
+```
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000000..ef614d25c1
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,22 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/index.html b/index.html
new file mode 100644
index 0000000000..c35b38c410
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ 99tech_assessment
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..182f56d51a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "99tech_assessment",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "test": "vitest run --passWithNoTests",
+ "preview": "vite preview",
+ "prepare": "husky"
+ },
+ "dependencies": {
+ "@tailwindcss/vite": "^4.3.0",
+ "@tanstack/react-query": "^5.100.14",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "react-hook-form": "^7.76.1",
+ "tailwindcss": "^4.3.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.29.0",
+ "@eslint/js": "^10.0.1",
+ "@rolldown/plugin-babel": "^0.2.3",
+ "@types/babel__core": "^7.20.5",
+ "@types/node": "^24.12.3",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "eslint": "^10.3.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.6.0",
+ "husky": "^9.1.7",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.59.2",
+ "vite": "^8.0.12",
+ "vitest": "^4.1.7"
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000000..0c45971152
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,2275 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.3.0
+ version: 4.3.0(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))
+ '@tanstack/react-query':
+ specifier: ^5.100.14
+ version: 5.100.14(react@19.2.6)
+ react:
+ specifier: ^19.2.6
+ version: 19.2.6
+ react-dom:
+ specifier: ^19.2.6
+ version: 19.2.6(react@19.2.6)
+ react-hook-form:
+ specifier: ^7.76.1
+ version: 7.76.1(react@19.2.6)
+ tailwindcss:
+ specifier: ^4.3.0
+ version: 4.3.0
+ devDependencies:
+ '@babel/core':
+ specifier: ^7.29.0
+ version: 7.29.7
+ '@eslint/js':
+ specifier: ^10.0.1
+ version: 10.0.1(eslint@10.4.0(jiti@2.7.0))
+ '@rolldown/plugin-babel':
+ specifier: ^0.2.3
+ version: 0.2.3(@babel/core@7.29.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))
+ '@types/babel__core':
+ specifier: ^7.20.5
+ version: 7.20.5
+ '@types/node':
+ specifier: ^24.12.3
+ version: 24.12.4
+ '@types/react':
+ specifier: ^19.2.14
+ version: 19.2.15
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.15)
+ '@vitejs/plugin-react':
+ specifier: ^6.0.1
+ version: 6.0.2(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))
+ babel-plugin-react-compiler:
+ specifier: ^1.0.0
+ version: 1.0.0
+ eslint:
+ specifier: ^10.3.0
+ version: 10.4.0(jiti@2.7.0)
+ eslint-plugin-react-hooks:
+ specifier: ^7.1.1
+ version: 7.1.1(eslint@10.4.0(jiti@2.7.0))
+ eslint-plugin-react-refresh:
+ specifier: ^0.5.2
+ version: 0.5.2(eslint@10.4.0(jiti@2.7.0))
+ globals:
+ specifier: ^17.6.0
+ version: 17.6.0
+ husky:
+ specifier: ^9.1.7
+ version: 9.1.7
+ typescript:
+ specifier: ~6.0.2
+ version: 6.0.3
+ typescript-eslint:
+ specifier: ^8.59.2
+ version: 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
+ vite:
+ specifier: ^8.0.12
+ version: 8.0.14(@types/node@24.12.4)(jiti@2.7.0)
+ vitest:
+ specifier: ^4.1.7
+ version: 4.1.7(@types/node@24.12.4)(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))
+
+packages:
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.7':
+ resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.7':
+ resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.7':
+ resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.7':
+ resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.7':
+ resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.7':
+ resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
+ engines: {node: '>=6.9.0'}
+
+ '@emnapi/core@1.10.0':
+ resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
+
+ '@emnapi/runtime@1.10.0':
+ resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
+
+ '@emnapi/wasi-threads@1.2.1':
+ resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
+
+ '@eslint-community/eslint-utils@4.9.1':
+ resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.23.5':
+ resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/config-helpers@0.6.0':
+ resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/core@1.2.1':
+ resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/js@10.0.1':
+ resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ peerDependencies:
+ eslint: ^10.0.0
+ peerDependenciesMeta:
+ eslint:
+ optional: true
+
+ '@eslint/object-schema@3.0.5':
+ resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/plugin-kit@0.7.1':
+ resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@humanfs/core@0.19.2':
+ resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.8':
+ resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/types@0.15.0':
+ resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/wasm-runtime@1.1.4':
+ resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@oxc-project/types@0.132.0':
+ resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==}
+
+ '@rolldown/binding-android-arm64@1.0.2':
+ resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.2':
+ resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.2':
+ resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.2':
+ resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.2':
+ resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.2':
+ resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.2':
+ resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.2':
+ resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.2':
+ resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.2':
+ resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-musl@1.0.2':
+ resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-openharmony-arm64@1.0.2':
+ resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.2':
+ resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.2':
+ resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.2':
+ resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/plugin-babel@0.2.3':
+ resolution: {integrity: sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==}
+ engines: {node: '>=22.12.0 || ^24.0.0'}
+ peerDependencies:
+ '@babel/core': ^7.29.0 || ^8.0.0-rc.1
+ '@babel/plugin-transform-runtime': ^7.29.0 || ^8.0.0-rc.1
+ '@babel/runtime': ^7.27.0 || ^8.0.0-rc.1
+ rolldown: ^1.0.0-rc.5
+ vite: ^8.0.0
+ peerDependenciesMeta:
+ '@babel/plugin-transform-runtime':
+ optional: true
+ '@babel/runtime':
+ optional: true
+ vite:
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@tailwindcss/node@4.3.0':
+ resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==}
+
+ '@tailwindcss/oxide-android-arm64@4.3.0':
+ resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.0':
+ resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.3.0':
+ resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.0':
+ resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0':
+ resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.0':
+ resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.0':
+ resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.0':
+ resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.0':
+ resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.0':
+ resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.0':
+ resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.0':
+ resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.3.0':
+ resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/vite@4.3.0':
+ resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+
+ '@tanstack/query-core@5.100.14':
+ resolution: {integrity: sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==}
+
+ '@tanstack/react-query@5.100.14':
+ resolution: {integrity: sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==}
+ peerDependencies:
+ react: ^18 || ^19
+
+ '@tybys/wasm-util@0.10.2':
+ resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/esrecurse@4.3.1':
+ resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/node@24.12.4':
+ resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==}
+
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.15':
+ resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==}
+
+ '@typescript-eslint/eslint-plugin@8.60.0':
+ resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.60.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/parser@8.60.0':
+ resolution: {integrity: sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/project-service@8.60.0':
+ resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/scope-manager@8.60.0':
+ resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.60.0':
+ resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/type-utils@8.60.0':
+ resolution: {integrity: sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/types@8.60.0':
+ resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.60.0':
+ resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/utils@8.60.0':
+ resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/visitor-keys@8.60.0':
+ resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react@6.0.2':
+ resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
+ babel-plugin-react-compiler: ^1.0.0
+ vite: ^8.0.0
+ peerDependenciesMeta:
+ '@rolldown/plugin-babel':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+
+ '@vitest/expect@4.1.7':
+ resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==}
+
+ '@vitest/mocker@4.1.7':
+ resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.1.7':
+ resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==}
+
+ '@vitest/runner@4.1.7':
+ resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==}
+
+ '@vitest/snapshot@4.1.7':
+ resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==}
+
+ '@vitest/spy@4.1.7':
+ resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==}
+
+ '@vitest/utils@4.1.7':
+ resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.16.0:
+ resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.15.0:
+ resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ babel-plugin-react-compiler@1.0.0:
+ resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ baseline-browser-mapping@2.10.32:
+ resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ brace-expansion@5.0.6:
+ resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
+ engines: {node: 18 || 20 || >=22}
+
+ browserslist@4.28.2:
+ resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ caniuse-lite@1.0.30001793:
+ resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
+
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ electron-to-chromium@1.5.362:
+ resolution: {integrity: sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==}
+
+ enhanced-resolve@5.22.0:
+ resolution: {integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==}
+ engines: {node: '>=10.13.0'}
+
+ es-module-lexer@2.1.0:
+ resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-react-hooks@7.1.1:
+ resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0
+
+ eslint-plugin-react-refresh@0.5.2:
+ resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==}
+ peerDependencies:
+ eslint: ^9 || ^10
+
+ eslint-scope@9.1.2:
+ resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint@10.4.0:
+ resolution: {integrity: sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@11.2.0:
+ resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ expect-type@1.3.0:
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+ engines: {node: '>=12.0.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.4.2:
+ resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@17.6.0:
+ resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==}
+ engines: {node: '>=18'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ hermes-estree@0.25.1:
+ resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+
+ hermes-parser@0.25.1:
+ resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+
+ husky@9.1.7:
+ resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jiti@2.7.0:
+ resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
+ hasBin: true
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ minimatch@10.2.5:
+ resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
+ engines: {node: 18 || 20 || >=22}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.12:
+ resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ node-releases@2.0.46:
+ resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==}
+ engines: {node: '>=18'}
+
+ obug@2.1.1:
+ resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.15:
+ resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ react-dom@19.2.6:
+ resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==}
+ peerDependencies:
+ react: ^19.2.6
+
+ react-hook-form@7.76.1:
+ resolution: {integrity: sha512-rYM7tPiWlu3nZchkR/ex7piyzui2vFPyaLnXnI/RnblB/L4qfMmyses8llJVtF1NpE9WBBsJlGtcSZzPCXW1qQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
+ react@19.2.6:
+ resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
+ engines: {node: '>=0.10.0'}
+
+ rolldown@1.0.2:
+ resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.8.1:
+ resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@4.1.0:
+ resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
+
+ tailwindcss@4.3.0:
+ resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==}
+
+ tapable@2.3.3:
+ resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
+ engines: {node: '>=6'}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@1.2.2:
+ resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.16:
+ resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
+ engines: {node: '>=12.0.0'}
+
+ tinyrainbow@3.1.0:
+ resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
+ engines: {node: '>=14.0.0'}
+
+ ts-api-utils@2.5.0:
+ resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.60.0:
+ resolution: {integrity: sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ vite@8.0.14:
+ resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.1.18
+ 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
+
+ vitest@4.1.7:
+ resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ 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.7
+ '@vitest/browser-preview': 4.1.7
+ '@vitest/browser-webdriverio': 4.1.7
+ '@vitest/coverage-istanbul': 4.1.7
+ '@vitest/coverage-v8': 4.1.7
+ '@vitest/ui': 4.1.7
+ 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
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zod-validation-error@4.0.2:
+ resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ zod: ^3.25.0 || ^4.0.0
+
+ zod@4.4.3:
+ resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+
+snapshots:
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.7': {}
+
+ '@babel/core@7.29.7':
+ 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/core@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.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.7':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.29.7':
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.28.2
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.29.7': {}
+
+ '@babel/helper-module-imports@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-option@7.29.7': {}
+
+ '@babel/helpers@7.29.7':
+ dependencies:
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@babel/parser@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@babel/template@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@babel/traverse@7.29.7':
+ 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.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.7':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@emnapi/core@1.10.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.1
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.10.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@eslint-community/eslint-utils@4.9.1(eslint@10.4.0(jiti@2.7.0))':
+ dependencies:
+ eslint: 10.4.0(jiti@2.7.0)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.2': {}
+
+ '@eslint/config-array@0.23.5':
+ dependencies:
+ '@eslint/object-schema': 3.0.5
+ debug: 4.4.3
+ minimatch: 10.2.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.6.0':
+ dependencies:
+ '@eslint/core': 1.2.1
+
+ '@eslint/core@1.2.1':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/js@10.0.1(eslint@10.4.0(jiti@2.7.0))':
+ optionalDependencies:
+ eslint: 10.4.0(jiti@2.7.0)
+
+ '@eslint/object-schema@3.0.5': {}
+
+ '@eslint/plugin-kit@0.7.1':
+ dependencies:
+ '@eslint/core': 1.2.1
+ levn: 0.4.1
+
+ '@humanfs/core@0.19.2':
+ dependencies:
+ '@humanfs/types': 0.15.0
+
+ '@humanfs/node@0.16.8':
+ dependencies:
+ '@humanfs/core': 0.19.2
+ '@humanfs/types': 0.15.0
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanfs/types@0.15.0': {}
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@tybys/wasm-util': 0.10.2
+ optional: true
+
+ '@oxc-project/types@0.132.0': {}
+
+ '@rolldown/binding-android-arm64@1.0.2':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.2':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.2':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.2':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.2':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.2':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.2':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.2':
+ optional: true
+
+ '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))':
+ dependencies:
+ '@babel/core': 7.29.7
+ picomatch: 4.0.4
+ rolldown: 1.0.2
+ optionalDependencies:
+ vite: 8.0.14(@types/node@24.12.4)(jiti@2.7.0)
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@tailwindcss/node@4.3.0':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.22.0
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.3.0
+
+ '@tailwindcss/oxide-android-arm64@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide@4.3.0':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.3.0
+ '@tailwindcss/oxide-darwin-arm64': 4.3.0
+ '@tailwindcss/oxide-darwin-x64': 4.3.0
+ '@tailwindcss/oxide-freebsd-x64': 4.3.0
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0
+ '@tailwindcss/oxide-linux-arm64-musl': 4.3.0
+ '@tailwindcss/oxide-linux-x64-gnu': 4.3.0
+ '@tailwindcss/oxide-linux-x64-musl': 4.3.0
+ '@tailwindcss/oxide-wasm32-wasi': 4.3.0
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
+ '@tailwindcss/oxide-win32-x64-msvc': 4.3.0
+
+ '@tailwindcss/vite@4.3.0(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))':
+ dependencies:
+ '@tailwindcss/node': 4.3.0
+ '@tailwindcss/oxide': 4.3.0
+ tailwindcss: 4.3.0
+ vite: 8.0.14(@types/node@24.12.4)(jiti@2.7.0)
+
+ '@tanstack/query-core@5.100.14': {}
+
+ '@tanstack/react-query@5.100.14(react@19.2.6)':
+ dependencies:
+ '@tanstack/query-core': 5.100.14
+ react: 19.2.6
+
+ '@tybys/wasm-util@0.10.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/esrecurse@4.3.1': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/node@24.12.4':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/react-dom@19.2.3(@types/react@19.2.15)':
+ dependencies:
+ '@types/react': 19.2.15
+
+ '@types/react@19.2.15':
+ dependencies:
+ csstype: 3.2.3
+
+ '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.60.0
+ '@typescript-eslint/type-utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.60.0
+ eslint: 10.4.0(jiti@2.7.0)
+ ignore: 7.0.5
+ natural-compare: 1.4.0
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.60.0
+ '@typescript-eslint/types': 8.60.0
+ '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.60.0
+ debug: 4.4.3
+ eslint: 10.4.0(jiti@2.7.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.60.0(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.60.0
+ debug: 4.4.3
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.60.0':
+ dependencies:
+ '@typescript-eslint/types': 8.60.0
+ '@typescript-eslint/visitor-keys': 8.60.0
+
+ '@typescript-eslint/tsconfig-utils@8.60.0(typescript@6.0.3)':
+ dependencies:
+ typescript: 6.0.3
+
+ '@typescript-eslint/type-utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.60.0
+ '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
+ debug: 4.4.3
+ eslint: 10.4.0(jiti@2.7.0)
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.60.0': {}
+
+ '@typescript-eslint/typescript-estree@8.60.0(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.60.0
+ '@typescript-eslint/visitor-keys': 8.60.0
+ debug: 4.4.3
+ minimatch: 10.2.5
+ semver: 7.8.1
+ tinyglobby: 0.2.16
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0))
+ '@typescript-eslint/scope-manager': 8.60.0
+ '@typescript-eslint/types': 8.60.0
+ '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3)
+ eslint: 10.4.0(jiti@2.7.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.60.0':
+ dependencies:
+ '@typescript-eslint/types': 8.60.0
+ eslint-visitor-keys: 5.0.1
+
+ '@vitejs/plugin-react@6.0.2(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.1
+ vite: 8.0.14(@types/node@24.12.4)(jiti@2.7.0)
+ optionalDependencies:
+ '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7)(rolldown@1.0.2)(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))
+ babel-plugin-react-compiler: 1.0.0
+
+ '@vitest/expect@4.1.7':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.1.7
+ '@vitest/utils': 4.1.7
+ chai: 6.2.2
+ tinyrainbow: 3.1.0
+
+ '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))':
+ dependencies:
+ '@vitest/spy': 4.1.7
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 8.0.14(@types/node@24.12.4)(jiti@2.7.0)
+
+ '@vitest/pretty-format@4.1.7':
+ dependencies:
+ tinyrainbow: 3.1.0
+
+ '@vitest/runner@4.1.7':
+ dependencies:
+ '@vitest/utils': 4.1.7
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.1.7':
+ dependencies:
+ '@vitest/pretty-format': 4.1.7
+ '@vitest/utils': 4.1.7
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.1.7': {}
+
+ '@vitest/utils@4.1.7':
+ dependencies:
+ '@vitest/pretty-format': 4.1.7
+ convert-source-map: 2.0.0
+ tinyrainbow: 3.1.0
+
+ acorn-jsx@5.3.2(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+
+ acorn@8.16.0: {}
+
+ ajv@6.15.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ assertion-error@2.0.1: {}
+
+ babel-plugin-react-compiler@1.0.0:
+ dependencies:
+ '@babel/types': 7.29.7
+
+ balanced-match@4.0.4: {}
+
+ baseline-browser-mapping@2.10.32: {}
+
+ brace-expansion@5.0.6:
+ dependencies:
+ balanced-match: 4.0.4
+
+ browserslist@4.28.2:
+ dependencies:
+ baseline-browser-mapping: 2.10.32
+ caniuse-lite: 1.0.30001793
+ electron-to-chromium: 1.5.362
+ node-releases: 2.0.46
+ update-browserslist-db: 1.2.3(browserslist@4.28.2)
+
+ caniuse-lite@1.0.30001793: {}
+
+ chai@6.2.2: {}
+
+ convert-source-map@2.0.0: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.2.3: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ detect-libc@2.1.2: {}
+
+ electron-to-chromium@1.5.362: {}
+
+ enhanced-resolve@5.22.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.3
+
+ es-module-lexer@2.1.0: {}
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@7.1.1(eslint@10.4.0(jiti@2.7.0)):
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/parser': 7.29.7
+ eslint: 10.4.0(jiti@2.7.0)
+ hermes-parser: 0.25.1
+ zod: 4.4.3
+ zod-validation-error: 4.0.2(zod@4.4.3)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-react-refresh@0.5.2(eslint@10.4.0(jiti@2.7.0)):
+ dependencies:
+ eslint: 10.4.0(jiti@2.7.0)
+
+ eslint-scope@9.1.2:
+ dependencies:
+ '@types/esrecurse': 4.3.1
+ '@types/estree': 1.0.9
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@5.0.1: {}
+
+ eslint@10.4.0(jiti@2.7.0):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0))
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.23.5
+ '@eslint/config-helpers': 0.6.0
+ '@eslint/core': 1.2.1
+ '@eslint/plugin-kit': 0.7.1
+ '@humanfs/node': 0.16.8
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.9
+ ajv: 6.15.0
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ escape-string-regexp: 4.0.0
+ eslint-scope: 9.1.2
+ eslint-visitor-keys: 5.0.1
+ espree: 11.2.0
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ minimatch: 10.2.5
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.7.0
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@11.2.0:
+ dependencies:
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ eslint-visitor-keys: 5.0.1
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ esutils@2.0.3: {}
+
+ expect-type@1.3.0: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.4.2
+ keyv: 4.5.4
+
+ flatted@3.4.2: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ gensync@1.0.0-beta.2: {}
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@17.6.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ hermes-estree@0.25.1: {}
+
+ hermes-parser@0.25.1:
+ dependencies:
+ hermes-estree: 0.25.1
+
+ husky@9.1.7: {}
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ imurmurhash@0.1.4: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ isexe@2.0.0: {}
+
+ jiti@2.7.0: {}
+
+ js-tokens@4.0.0: {}
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ minimatch@10.2.5:
+ dependencies:
+ brace-expansion: 5.0.6
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.12: {}
+
+ natural-compare@1.4.0: {}
+
+ node-releases@2.0.46: {}
+
+ obug@2.1.1: {}
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ pathe@2.0.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.4: {}
+
+ postcss@8.5.15:
+ dependencies:
+ nanoid: 3.3.12
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ punycode@2.3.1: {}
+
+ react-dom@19.2.6(react@19.2.6):
+ dependencies:
+ react: 19.2.6
+ scheduler: 0.27.0
+
+ react-hook-form@7.76.1(react@19.2.6):
+ dependencies:
+ react: 19.2.6
+
+ react@19.2.6: {}
+
+ rolldown@1.0.2:
+ dependencies:
+ '@oxc-project/types': 0.132.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.2
+ '@rolldown/binding-darwin-arm64': 1.0.2
+ '@rolldown/binding-darwin-x64': 1.0.2
+ '@rolldown/binding-freebsd-x64': 1.0.2
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.2
+ '@rolldown/binding-linux-arm64-gnu': 1.0.2
+ '@rolldown/binding-linux-arm64-musl': 1.0.2
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.2
+ '@rolldown/binding-linux-s390x-gnu': 1.0.2
+ '@rolldown/binding-linux-x64-gnu': 1.0.2
+ '@rolldown/binding-linux-x64-musl': 1.0.2
+ '@rolldown/binding-openharmony-arm64': 1.0.2
+ '@rolldown/binding-wasm32-wasi': 1.0.2
+ '@rolldown/binding-win32-arm64-msvc': 1.0.2
+ '@rolldown/binding-win32-x64-msvc': 1.0.2
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.8.1: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ siginfo@2.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ stackback@0.0.2: {}
+
+ std-env@4.1.0: {}
+
+ tailwindcss@4.3.0: {}
+
+ tapable@2.3.3: {}
+
+ tinybench@2.9.0: {}
+
+ tinyexec@1.2.2: {}
+
+ tinyglobby@0.2.16:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ tinyrainbow@3.1.0: {}
+
+ ts-api-utils@2.5.0(typescript@6.0.3):
+ dependencies:
+ typescript: 6.0.3
+
+ tslib@2.8.1:
+ optional: true
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
+ eslint: 10.4.0(jiti@2.7.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@6.0.3: {}
+
+ undici-types@7.16.0: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.2):
+ dependencies:
+ browserslist: 4.28.2
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.15
+ rolldown: 1.0.2
+ tinyglobby: 0.2.16
+ optionalDependencies:
+ '@types/node': 24.12.4
+ fsevents: 2.3.3
+ jiti: 2.7.0
+
+ vitest@4.1.7(@types/node@24.12.4)(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0)):
+ dependencies:
+ '@vitest/expect': 4.1.7
+ '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))
+ '@vitest/pretty-format': 4.1.7
+ '@vitest/runner': 4.1.7
+ '@vitest/snapshot': 4.1.7
+ '@vitest/spy': 4.1.7
+ '@vitest/utils': 4.1.7
+ es-module-lexer: 2.1.0
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ obug: 2.1.1
+ pathe: 2.0.3
+ picomatch: 4.0.4
+ std-env: 4.1.0
+ tinybench: 2.9.0
+ tinyexec: 1.2.2
+ tinyglobby: 0.2.16
+ tinyrainbow: 3.1.0
+ vite: 8.0.14(@types/node@24.12.4)(jiti@2.7.0)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 24.12.4
+ transitivePeerDependencies:
+ - msw
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ word-wrap@1.2.5: {}
+
+ yallist@3.1.1: {}
+
+ yocto-queue@0.1.0: {}
+
+ zod-validation-error@4.0.2(zod@4.4.3):
+ dependencies:
+ zod: 4.4.3
+
+ zod@4.4.3: {}
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000000..6893eb1323
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons.svg b/public/icons.svg
new file mode 100644
index 0000000000..e9522193d9
--- /dev/null
+++ b/public/icons.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/readme.md b/readme.md
deleted file mode 100644
index 1ff4bc95b4..0000000000
--- a/readme.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# 99Tech Code Challenge #1 #
-
-Note that if you fork this repository, your responses may be publicly linked to this repo.
-Please submit your application along with the solutions attached or linked.
-
-It is important that you minimally attempt the problems, even if you do not arrive at a working solution.
-
-## Submission ##
-You can either provide a link to an online repository, attach the solution in your application, or whichever method you prefer.
-We're cool as long as we can view your solution without any pain.
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000000..34a8a12818
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,50 @@
+import { useState } from 'react';
+import Problem1 from './problems/problem1';
+import Problem2 from './problems/problem2';
+import Problem3 from './problems/problem3';
+
+const TABS = [
+ { id: 1, label: 'Problem 1', component: },
+ { id: 2, label: 'Problem 2', component: },
+ { id: 3, label: 'Problem 3', component: },
+];
+
+function App() {
+ const [activeTab, setActiveTab] = useState(1);
+
+ const active = TABS.find((t) => t.id === activeTab)!;
+
+ return (
+
+
+
+ Assessment
+
+
+ {TABS.map((tab) => (
+ setActiveTab(tab.id)}
+ className={[
+ 'px-5 py-2 rounded-t-lg border-b-2 text-sm cursor-pointer transition',
+ activeTab === tab.id
+ ? 'text-[var(--accent)] border-[var(--accent)] font-semibold'
+ : 'text-[var(--text)] border-transparent hover:bg-[var(--accent-bg)] hover:text-[var(--text-h)]',
+ ].join(' ')}
+ >
+ {tab.label}
+
+ ))}
+
+
+
+
+ {active.component}
+
+
+ );
+}
+
+export default App;
diff --git a/src/assets/hero.png b/src/assets/hero.png
new file mode 100644
index 0000000000..02251f4b95
Binary files /dev/null and b/src/assets/hero.png differ
diff --git a/src/assets/react.svg b/src/assets/react.svg
new file mode 100644
index 0000000000..6c87de9bb3
--- /dev/null
+++ b/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/vite.svg b/src/assets/vite.svg
new file mode 100644
index 0000000000..5101b674df
--- /dev/null
+++ b/src/assets/vite.svg
@@ -0,0 +1 @@
+Vite
diff --git a/src/components/Task.css b/src/components/Task.css
new file mode 100644
index 0000000000..90d61bc4d2
--- /dev/null
+++ b/src/components/Task.css
@@ -0,0 +1,64 @@
+.task-card {
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ overflow: hidden;
+ margin-bottom: 32px;
+}
+
+.task-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px 24px;
+ background: var(--accent-bg);
+ border-bottom: 1px solid var(--border);
+}
+
+.task-badge {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--accent);
+ background: var(--accent-border);
+ border-radius: 4px;
+ padding: 2px 8px;
+}
+
+.task-title {
+ margin: 0;
+ font-size: 18px;
+ color: var(--text-h);
+}
+
+.task-body {
+ padding: 20px 24px;
+ line-height: 1.7;
+}
+
+.task-body p {
+ margin: 0 0 10px;
+}
+
+.task-body p:last-child {
+ margin-bottom: 0;
+}
+
+.task-body code {
+ font-size: 14px;
+}
+
+.task-body pre {
+ background: var(--code-bg);
+ border-radius: 8px;
+ padding: 16px 20px;
+ overflow-x: auto;
+ margin: 12px 0;
+}
+
+.task-body pre code {
+ background: none;
+ padding: 0;
+ font-size: 14px;
+ line-height: 1.6;
+}
diff --git a/src/components/Task.tsx b/src/components/Task.tsx
new file mode 100644
index 0000000000..17a8158e62
--- /dev/null
+++ b/src/components/Task.tsx
@@ -0,0 +1,19 @@
+import type { ReactNode } from 'react'
+import './Task.css'
+
+interface TaskProps {
+ title?: string
+ children: ReactNode
+}
+
+export default function Task({ title = 'Task', children }: TaskProps) {
+ return (
+
+
+ Task
+
{title}
+
+ {children}
+
+ )
+}
diff --git a/src/components/TokenSelector.tsx b/src/components/TokenSelector.tsx
new file mode 100644
index 0000000000..f8f4b25515
--- /dev/null
+++ b/src/components/TokenSelector.tsx
@@ -0,0 +1,225 @@
+import { useState, useRef, useEffect, forwardRef } from 'react'
+import type { ChangeEvent } from 'react'
+import type { TokenInfo } from '../problems/problem2/types'
+
+interface TokenSelectorProps {
+ name: string
+ tokens: TokenInfo[]
+ value?: string
+ onChange: (e: ChangeEvent) => void
+ onBlur: () => void
+ hasError?: boolean
+ placeholder?: string
+ disabledSymbol?: string
+}
+
+const TokenSelector = forwardRef(
+ ({ name, tokens, value, onChange, onBlur, hasError, placeholder, disabledSymbol }, ref) => {
+ const [open, setOpen] = useState(false)
+ const [search, setSearch] = useState('')
+ const containerRef = useRef(null)
+ const searchRef = useRef(null)
+
+ const selected = tokens.find((t) => t.symbol === value)
+
+ const filtered = tokens.filter((t) => {
+ if (!search) {
+ return true
+ }
+ return t.symbol.toLowerCase().includes(search.toLowerCase())
+ })
+
+ const openDropdown = () => {
+ setSearch('')
+ setOpen(true)
+ }
+
+ const closeDropdown = () => {
+ setOpen(false)
+ setSearch('')
+ onBlur()
+ }
+
+ const selectToken = (symbol: string) => {
+ if (symbol === disabledSymbol) {
+ return
+ }
+ const syntheticEvent = {
+ target: { name, value: symbol },
+ type: 'change',
+ } as ChangeEvent
+ onChange(syntheticEvent)
+ closeDropdown()
+ }
+
+ useEffect(() => {
+ if (open) {
+ searchRef.current?.focus()
+ }
+ }, [open])
+
+ useEffect(() => {
+ const handleClick = (e: MouseEvent) => {
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
+ setOpen(false)
+ setSearch('')
+ onBlur()
+ }
+ }
+ if (open) {
+ document.addEventListener('mousedown', handleClick)
+ }
+ return () => document.removeEventListener('mousedown', handleClick)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [open])
+
+ return (
+
+ {/* Hidden input for react-hook-form ref */}
+
+
+ {/* Trigger button */}
+
+ {selected
+ ? (
+ <>
+ { (e.currentTarget as HTMLImageElement).style.display = 'none' }}
+ />
+ {selected.symbol}
+
+ ${selected.price.toLocaleString('en-US', { maximumFractionDigits: 2 })}
+
+ >
+ )
+ : (
+ {placeholder ?? 'Select token'}
+ )}
+
+
+
+
+
+ {/* Popover */}
+
+ {/* Search */}
+
+ setSearch(e.target.value)}
+ placeholder="Search token..."
+ className={[
+ 'w-full px-3 py-1.5 rounded-lg text-sm bg-(--code-bg)',
+ 'text-(--text-h) placeholder:text-(--text)',
+ 'border border-transparent focus:outline-none focus:border-(--accent)',
+ 'transition-colors duration-150',
+ ].join(' ')}
+ />
+
+
+ {/* List */}
+
+ {filtered.length === 0
+ ? (
+
+ No tokens found
+
+ )
+ : filtered.map((token) => {
+ const isSelected = token.symbol === value
+ const isDisabled = token.symbol === disabledSymbol
+
+ return (
+ selectToken(token.symbol)}
+ className={[
+ 'flex items-center gap-2.5 px-3 py-2 cursor-pointer transition-colors duration-100',
+ isDisabled
+ ? 'opacity-40 cursor-not-allowed'
+ : isSelected
+ ? 'bg-(--accent-bg) text-(--accent)'
+ : 'hover:bg-(--code-bg)',
+ ].join(' ')}
+ >
+ { (e.currentTarget as HTMLImageElement).style.display = 'none' }}
+ />
+
+
+ {token.symbol}
+
+
+
+ ${token.price.toLocaleString('en-US', { maximumFractionDigits: 4 })}
+
+ {isSelected && (
+
+
+
+ )}
+
+ )
+ })}
+
+
+
+ )
+ },
+)
+
+TokenSelector.displayName = 'TokenSelector'
+
+export default TokenSelector
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000000..3259cfefa1
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,59 @@
+@import "tailwindcss";
+
+:root {
+ --text: #6b6375;
+ --text-h: #08060d;
+ --bg: #fff;
+ --border: #e5e4e7;
+ --code-bg: #f4f3ec;
+ --accent: #0e8cfa;
+ --accent-bg: rgba(59, 92, 255, 0.1);
+ --accent-border: rgba(142, 168, 225, 0.5);
+ --social-bg: rgba(244, 243, 236, 0.5);
+ --shadow:
+ rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+
+ --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --mono: ui-monospace, Consolas, monospace;
+
+ font: 18px/145% var(--sans);
+ letter-spacing: 0.18px;
+ color-scheme: light dark;
+ color: var(--text);
+ background: var(--bg);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ @media (max-width: 1024px) {
+ font-size: 16px;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --text: #9ca3af;
+ --text-h: #f3f4f6;
+ --bg: #16171d;
+ --border: #2e303a;
+ --code-bg: #1f2028;
+ --accent: #c084fc;
+ --accent-bg: rgba(192, 132, 252, 0.15);
+ --accent-border: rgba(192, 132, 252, 0.5);
+ --social-bg: rgba(47, 48, 58, 0.5);
+ --shadow:
+ rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
+ }
+}
+
+#root {
+ min-height: 100svh;
+ display: flex;
+ flex-direction: column;
+}
+
+body {
+ margin: 0;
+}
diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts
new file mode 100644
index 0000000000..2cdc62fd3c
--- /dev/null
+++ b/src/lib/i18n.ts
@@ -0,0 +1,51 @@
+const messages: Record = {
+ // Validation
+ 'validation.required': 'This field is required.',
+ 'validation.integer': 'Value must be a valid integer.',
+ 'validation.max_safe':
+ 'The result sum_to_n(n) must be less than Number.MAX_SAFE_INTEGER (2⁵³ − 1).',
+ 'validation.method_required': 'Please select an implementation.',
+ 'validation.amount_number': 'Must be a valid number.',
+ 'validation.amount_positive': 'Amount must be greater than 0.',
+ 'validation.same_token': 'From and To tokens must be different.',
+
+ // Form labels
+ 'form.n_label': 'Value of n',
+ 'form.n_placeholder': 'e.g. 5',
+ 'form.method_label': 'Implementation',
+ 'form.submit': 'Calculate',
+ 'form.reset': 'Reset',
+
+ // Result
+ 'result.heading': 'Result',
+ 'result.expression': 'sum_to_n({n}) = {value}',
+ 'result.reference': 'Reference',
+
+ // Swap form
+ 'swap.from_label': 'You send',
+ 'swap.to_label': 'You receive',
+ 'swap.amount_placeholder': '0.00',
+ 'swap.select_token': 'Select token',
+ 'swap.submit': 'Swap',
+ 'swap.submitting': 'Swapping...',
+ 'swap.reset': 'Reset',
+ 'swap.rate_display': '1 {from} = {rate} {to}',
+ 'swap.result_heading': 'Swap Confirmed',
+ 'swap.result_detail': '{fromAmount} {from} → {toAmount} {to}',
+ 'swap.history_heading': 'Recent Swaps',
+ 'swap.loading': 'Loading tokens...',
+ 'swap.error': 'Failed to load token prices. Please try again.',
+ 'swap.error_network': 'Network error. Check your connection and try again.',
+ 'swap.error_http': 'Failed to load prices (HTTP {status}). Please try again.',
+};
+
+// i18n mock function, in a real application this would be replaced with a proper i18n library
+export function t(key: string, vars?: Record): string {
+ let msg = messages[key] ?? key;
+ if (vars) {
+ for (const [k, v] of Object.entries(vars)) {
+ msg = msg.replaceAll(`{${k}}`, String(v));
+ }
+ }
+ return msg;
+}
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000000..e8de355f8a
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,23 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import './index.css'
+import App from './App.tsx'
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 60_000,
+ gcTime: 5 * 60_000,
+ retry: 2,
+ },
+ },
+})
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+)
diff --git a/src/problems/problem1/index.tsx b/src/problems/problem1/index.tsx
new file mode 100644
index 0000000000..91665544be
--- /dev/null
+++ b/src/problems/problem1/index.tsx
@@ -0,0 +1,213 @@
+import { useState } from 'react'
+import { useForm } from 'react-hook-form'
+import Task from '../../components/Task'
+import { t } from '../../lib/i18n'
+import { sum_to_n_a, sum_to_n_b, sum_to_n_c } from './solutions'
+import solutionPreview from './problem1_solution.png'
+
+type FormValues = {
+ n: string
+ method: 'a' | 'b' | 'c'
+}
+
+type Result = {
+ n: number
+ value: number
+ method: 'a' | 'b' | 'c'
+}
+
+const METHODS = [
+ {
+ id: 'a' as const,
+ label: 'A — Gaussian formula',
+ description: 'Closed-form: n × (n + 1) / 2',
+ complexity: 'O(1)',
+ fn: sum_to_n_a,
+ path: 'src/problems/problem1/solutions.ts',
+ line: 2,
+ },
+ {
+ id: 'b' as const,
+ label: 'B — Iterative loop',
+ description: 'Accumulates with a for-loop from 1 to n',
+ complexity: 'O(n)',
+ fn: sum_to_n_b,
+ path: 'src/problems/problem1/solutions.ts',
+ line: 8,
+ },
+ {
+ id: 'c' as const,
+ label: 'C — Recursion',
+ description: 'Reduces: sum(n) = n + sum(n − 1)',
+ complexity: 'O(n)',
+ fn: sum_to_n_c,
+ path: 'src/problems/problem1/solutions.ts',
+ line: 17,
+ },
+]
+
+export default function Problem1() {
+ const [result, setResult] = useState(null)
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors },
+ } = useForm()
+
+ const onSubmit = (data: FormValues) => {
+ const n = Number(data.n)
+ const method = METHODS.find((m) => m.id === data.method)!
+ setResult({ n, value: method.fn(n), method: data.method })
+ }
+
+ const onReset = () => {
+ reset()
+ setResult(null)
+ }
+
+ const resultMethod = result ? METHODS.find((m) => m.id === result.method)! : null
+
+ return (
+
+
+ Provide 3 unique implementations of the following function in JavaScript.
+ 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.
+
+ {`var sum_to_n_a = function(n) { // your code here };
+var sum_to_n_b = function(n) { // your code here };
+var sum_to_n_c = function(n) { // your code here };`}
+
+
+
+ {/* solution preview */}
+
+
+ Solution Preview
+
+
+
+
+
+
+ {/* result */}
+ {result && resultMethod && (
+
+
+ {t('result.heading')}
+
+
+ {t('result.expression', { n: result.n, value: result.value })}
+
+
+ {t('result.reference')}
+
+ {resultMethod.path}:{resultMethod.line}
+
+
+
+ )}
+
+ )
+}
diff --git a/src/problems/problem1/problem1_solution.png b/src/problems/problem1/problem1_solution.png
new file mode 100644
index 0000000000..d85fe5c4a8
Binary files /dev/null and b/src/problems/problem1/problem1_solution.png differ
diff --git a/src/problems/problem1/solutions.test.ts b/src/problems/problem1/solutions.test.ts
new file mode 100644
index 0000000000..5aad9d4d96
--- /dev/null
+++ b/src/problems/problem1/solutions.test.ts
@@ -0,0 +1,82 @@
+import { describe, expect, it } from 'vitest'
+import { sum_to_n_a, sum_to_n_b, sum_to_n_c } from './solutions'
+
+// ── sum_to_n_a (Gaussian closed-form) ────────────────────────────────────────
+describe('sum_to_n_a', () => {
+ it('returns expected output for typical input', () => {
+ expect(sum_to_n_a(5)).toBe(15)
+ })
+
+ it('returns neutral value for zero input', () => {
+ expect(sum_to_n_a(0)).toBe(0)
+ })
+
+ it('returns neutral value for negative input', () => {
+ expect(sum_to_n_a(-3)).toBe(0)
+ })
+
+ it('handles minimum valid input', () => {
+ expect(sum_to_n_a(1)).toBe(1)
+ })
+
+ it('handles input at the upper boundary', () => {
+ expect(sum_to_n_a(1000)).toBe(500500)
+ })
+})
+
+// ── sum_to_n_b (iterative loop) ───────────────────────────────────────────────
+describe('sum_to_n_b', () => {
+ it('returns expected output for typical input', () => {
+ expect(sum_to_n_b(5)).toBe(15)
+ })
+
+ it('returns neutral value for zero input', () => {
+ expect(sum_to_n_b(0)).toBe(0)
+ })
+
+ it('returns neutral value for negative input', () => {
+ expect(sum_to_n_b(-3)).toBe(0)
+ })
+
+ it('handles minimum valid input', () => {
+ expect(sum_to_n_b(1)).toBe(1)
+ })
+
+ it('handles input at the upper boundary', () => {
+ expect(sum_to_n_b(1000)).toBe(500500)
+ })
+})
+
+// ── sum_to_n_c (recursion) ────────────────────────────────────────────────────
+describe('sum_to_n_c', () => {
+ it('returns expected output for typical input', () => {
+ expect(sum_to_n_c(5)).toBe(15)
+ })
+
+ it('returns neutral value for zero input', () => {
+ expect(sum_to_n_c(0)).toBe(0)
+ })
+
+ it('returns neutral value for negative input', () => {
+ expect(sum_to_n_c(-3)).toBe(0)
+ })
+
+ it('handles minimum valid input', () => {
+ expect(sum_to_n_c(1)).toBe(1)
+ })
+
+ it('handles input at the upper boundary', () => {
+ expect(sum_to_n_c(1000)).toBe(500500)
+ })
+})
+
+// ── Parity ────────────────────────────────────────────────────────────────────
+describe('parity across all three implementations', () => {
+ it('all implementations produce the same output', () => {
+ const inputs = [1, 2, 5, 10, 100, 1000]
+ for (const input of inputs) {
+ expect(sum_to_n_b(input)).toBe(sum_to_n_a(input))
+ expect(sum_to_n_c(input)).toBe(sum_to_n_a(input))
+ }
+ })
+})
diff --git a/src/problems/problem1/solutions.ts b/src/problems/problem1/solutions.ts
new file mode 100644
index 0000000000..ffaf7602a6
--- /dev/null
+++ b/src/problems/problem1/solutions.ts
@@ -0,0 +1,30 @@
+// ── Implementation A: Gaussian closed-form formula O(1) ──────────────────────
+export const sum_to_n_a = (n: number): number => {
+ if (n <= 0) {
+ return 0
+ }
+ return (n * (n + 1)) / 2
+}
+
+// ── Implementation B: Iterative loop O(n) ────────────────────────────────────
+export const sum_to_n_b = (n: number): number => {
+ if (n <= 0) {
+ return 0
+ }
+ let sum = 0
+ for (let i = 1; i <= n; i++) {
+ sum += i
+ }
+ return sum
+}
+
+// ── Implementation C: Recursion O(n) ─────────────────────────────────────────
+export const sum_to_n_c = (n: number): number => {
+ if (n <= 0) {
+ return 0
+ }
+ if (n === 1) {
+ return 1
+ }
+ return n + sum_to_n_c(n - 1)
+}
diff --git a/src/problems/problem2/index.tsx b/src/problems/problem2/index.tsx
new file mode 100644
index 0000000000..15f0ce4125
--- /dev/null
+++ b/src/problems/problem2/index.tsx
@@ -0,0 +1,468 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import Task from '../../components/Task';
+import TokenSelector from '../../components/TokenSelector';
+import { t } from '../../lib/i18n';
+import {
+ normalizeTokenPrices,
+ fetchTokenPrices,
+ simulateSwap,
+ formatAmount,
+} from './logic';
+import { useSwapForm } from './useSwapForm';
+import type { SwapRecord } from './types';
+
+// ── Skeleton ───────────────────────────────────────────────────────────────
+const FieldSkeleton = () => (
+
+);
+
+// ── History item ───────────────────────────────────────────────────────────
+const HistoryItem = ({ record }: { record: SwapRecord }) => (
+
+
+
+ {formatAmount(record.fromAmount)} {record.fromSymbol}
+ {' → '}
+ {formatAmount(record.toAmount)} {record.toSymbol}
+
+
+ 1 {record.fromSymbol} = {formatAmount(record.rate)} {record.toSymbol}
+ {' · '}
+ {record.timestamp.toLocaleTimeString()}
+
+
+
+ Done
+
+
+);
+
+// ── Main component ─────────────────────────────────────────────────────────
+export default function Problem2() {
+ const queryClient = useQueryClient();
+
+ // ── API: token prices ──────────────────────────────────────────────────
+ const {
+ data: tokens = [],
+ isLoading,
+ isError,
+ error,
+ } = useQuery({
+ queryKey: ['token-prices'],
+ queryFn: fetchTokenPrices,
+ select: normalizeTokenPrices,
+ staleTime: 60_000,
+ });
+
+ // ── API: swap history (client state via setQueryData) ──────────────────
+ const { data: history = [] } = useQuery({
+ queryKey: ['swap-history'],
+ queryFn: (): SwapRecord[] => [],
+ staleTime: Infinity,
+ gcTime: Infinity,
+ });
+
+ // ── Form hook ──────────────────────────────────────────────────────────
+ const {
+ fromTokenField,
+ fromAmountField,
+ toTokenField,
+ errors,
+ isValid,
+ handleSubmit,
+ setValue,
+ rate,
+ toAmount,
+ fromToken,
+ toToken,
+ watchedFrom,
+ watchedTo,
+ watchedAmount,
+ flipTokens,
+ resetForm,
+ } = useSwapForm(tokens);
+
+ // ── Mutation: simulate swap ────────────────────────────────────────────
+ const {
+ mutate: submitSwap,
+ isPending,
+ isSuccess,
+ reset: resetMutation,
+ } = useMutation({
+ mutationFn: () => simulateSwap(1500),
+ onSuccess: () => {
+ if (!fromToken || !toToken || rate === null) {
+ return;
+ }
+ const record: SwapRecord = {
+ id: crypto.randomUUID(),
+ fromSymbol: fromToken.symbol,
+ fromAmount: Number(watchedAmount),
+ toSymbol: toToken.symbol,
+ toAmount: Number(watchedAmount) * rate,
+ rate,
+ timestamp: new Date(),
+ };
+ queryClient.setQueryData(['swap-history'], (old = []) =>
+ [record, ...old].slice(0, 10),
+ );
+ },
+ });
+
+ const onSubmit = handleSubmit(() => {
+ submitSwap();
+ });
+
+ const onReset = () => {
+ resetForm();
+ resetMutation();
+ };
+
+ const hasRate = rate !== null && fromToken && toToken;
+
+ return (
+
+ {/* ── Task statement ─────────────────────────────────────────────── */}
+
+ Create a currency swap form based on the template provided.
+
+ The submission is required to:
+
+
+ Retrieve token prices from the provided API endpoint.
+ Allow users to select tokens and input a swap amount.
+ Display the converted output amount in real time.
+ Provide UI feedback for pending and error states.
+
+
+ The form must not use any form state management other than{' '}
+ react-hook-form, and all token data must be fetched from
+ the live endpoint below.
+
+
+ https://interview.switcheo.com/prices.json
+
+
+
+ {/* ── Form card ──────────────────────────────────────────────────── */}
+
+
+ Swap
+
+
+ Exchange tokens at real-time rates
+
+
+ {/* Error state */}
+ {isError && (
+
+ {error?.message ?? t('swap.error')}
+
+ )}
+
+
+
+ {/* ── Success result ────────────────────────────────────────────── */}
+
0
+ ? 'grid-rows-[1fr] mt-4'
+ : 'grid-rows-[0fr]',
+ ].join(' ')}
+ >
+
+ {isSuccess && history[0] && (
+
+
+
+
+
+
+
+ {t('swap.result_heading')}
+
+
+
+ {t('swap.result_detail', {
+ fromAmount: formatAmount(history[0].fromAmount),
+ from: history[0].fromSymbol,
+ toAmount: formatAmount(history[0].toAmount),
+ to: history[0].toSymbol,
+ })}
+
+
+ )}
+
+
+
+
+ {/* ── History panel ──────────────────────────────────────────────────── */}
+
0 ? 'opacity-100' : 'opacity-0 pointer-events-none',
+ ].join(' ')}
+ >
+
+
+ {t('swap.history_heading')}
+
+
+ {history.map((record) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/problems/problem2/logic.test.ts b/src/problems/problem2/logic.test.ts
new file mode 100644
index 0000000000..8b29746727
--- /dev/null
+++ b/src/problems/problem2/logic.test.ts
@@ -0,0 +1,257 @@
+import { describe, expect, it } from 'vitest'
+import {
+ bankersRound,
+ formatAmount,
+ computeRate,
+ computeToAmount,
+ normalizeTokenPrices,
+} from './logic'
+import type { TokenInfo, TokenPrice } from './types'
+
+// ── bankersRound ──────────────────────────────────────────────────────────────
+describe('bankersRound', () => {
+ it('rounds a typical value to the specified decimal places', () => {
+ expect(bankersRound(1.2345, 2)).toBe(1.23)
+ })
+
+ it('rounds an exactly halfway value down when the floor digit is even', () => {
+ // floor digit is 2 (even) → round down: 2.5 → 2
+ expect(bankersRound(2.5, 0)).toBe(2)
+ })
+
+ it('rounds an exactly halfway value up when the floor digit is odd', () => {
+ // floor digit is 3 (odd) → round up: 3.5 → 4
+ expect(bankersRound(3.5, 0)).toBe(4)
+ })
+
+ it('rounds a halfway value at decimal precision with even floor digit down', () => {
+ // 2.45 at 1dp: floor digit at that position is 4 (even) → 2.4
+ expect(bankersRound(2.45, 1)).toBe(2.4)
+ })
+
+ it('rounds a halfway value at decimal precision with odd floor digit up', () => {
+ // 2.35 at 1dp: floor digit at that position is 3 (odd) → 2.4
+ expect(bankersRound(2.35, 1)).toBe(2.4)
+ })
+
+ it('returns zero unchanged', () => {
+ expect(bankersRound(0, 4)).toBe(0)
+ })
+
+ it('handles zero decimal places', () => {
+ expect(bankersRound(7.3, 0)).toBe(7)
+ expect(bankersRound(7.7, 0)).toBe(8)
+ })
+
+ it('handles a large number with high decimal precision', () => {
+ expect(bankersRound(123456.123456, 2)).toBe(123456.12)
+ })
+
+ it('handles a very small number near zero', () => {
+ expect(bankersRound(0.000001234, 8)).toBe(0.00000123)
+ })
+})
+
+// ── formatAmount ──────────────────────────────────────────────────────────────
+describe('formatAmount', () => {
+ it('formats a typical medium-range value with 6 decimal places', () => {
+ expect(formatAmount(12.3456789)).toBe('12.345679')
+ })
+
+ it('returns 0 for a zero value', () => {
+ expect(formatAmount(0)).toBe('0')
+ })
+
+ it('formats a large value (above 100 000) with 2 decimal places', () => {
+ expect(formatAmount(150000.5678)).toBe('150,000.57')
+ })
+
+ it('formats a value in the thousands range with 4 decimal places', () => {
+ expect(formatAmount(1234.56789)).toBe('1,234.5679')
+ })
+
+ it('formats a small value below 1 with 8 decimal places', () => {
+ expect(formatAmount(0.123456789)).toBe('0.12345679')
+ })
+
+ it('formats a very small value below 0.0001 with extended precision', () => {
+ const result = formatAmount(0.00000123)
+ // should preserve at least 5 significant digits past the leading zeros
+ expect(result).toBe('0.00000123')
+ })
+
+ it('adds a thousands separator for large values', () => {
+ expect(formatAmount(1000000)).toBe('1,000,000')
+ })
+
+ it('applies banker\'s rounding, not plain round-half-up', () => {
+ // 2.5 rounds to 2 (even), not 3
+ expect(bankersRound(2.5, 0)).toBe(2)
+ // 3.5 rounds to 4 (even), not 3
+ expect(bankersRound(3.5, 0)).toBe(4)
+ })
+})
+
+// ── computeRate ───────────────────────────────────────────────────────────────
+const makeToken = (symbol: string, price: number): TokenInfo => ({
+ symbol,
+ price,
+ imageUrl: `https://example.com/${symbol}.svg`,
+})
+
+describe('computeRate', () => {
+ it('returns the correct rate between two tokens', () => {
+ const eth = makeToken('ETH', 2000)
+ const usdt = makeToken('USDT', 1)
+ // 1 ETH = 2000 USDT
+ expect(computeRate(eth, usdt)).toBe(2000)
+ })
+
+ it('returns the inverse rate when token order is reversed', () => {
+ const eth = makeToken('ETH', 2000)
+ const usdt = makeToken('USDT', 1)
+ // 1 USDT = 0.0005 ETH
+ expect(computeRate(usdt, eth)).toBe(0.0005)
+ })
+
+ it('returns 1 when both tokens have the same price', () => {
+ const a = makeToken('A', 500)
+ const b = makeToken('B', 500)
+ expect(computeRate(a, b)).toBe(1)
+ })
+
+ it('returns null when the from token is undefined', () => {
+ const usdt = makeToken('USDT', 1)
+ expect(computeRate(undefined, usdt)).toBeNull()
+ })
+
+ it('returns null when the to token is undefined', () => {
+ const eth = makeToken('ETH', 2000)
+ expect(computeRate(eth, undefined)).toBeNull()
+ })
+
+ it('returns null when both tokens are undefined', () => {
+ expect(computeRate(undefined, undefined)).toBeNull()
+ })
+
+ it('returns null when the from token has a zero price', () => {
+ const broken = makeToken('BROKEN', 0)
+ const usdt = makeToken('USDT', 1)
+ expect(computeRate(broken, usdt)).toBeNull()
+ })
+
+ it('returns null when the to token has a zero price', () => {
+ const eth = makeToken('ETH', 2000)
+ const broken = makeToken('BROKEN', 0)
+ expect(computeRate(eth, broken)).toBeNull()
+ })
+})
+
+// ── computeToAmount ───────────────────────────────────────────────────────────
+describe('computeToAmount', () => {
+ it('returns the correctly formatted output for a typical conversion', () => {
+ // 10 ETH at rate 2000 = 20 000 USDT → large value, 4 dp
+ const result = computeToAmount('10', 2000)
+ expect(result).toBe('20,000')
+ })
+
+ it('returns an empty string when fromAmount is empty', () => {
+ expect(computeToAmount('', 2000)).toBe('')
+ })
+
+ it('returns an empty string when rate is null', () => {
+ expect(computeToAmount('10', null)).toBe('')
+ })
+
+ it('returns an empty string when fromAmount is zero', () => {
+ expect(computeToAmount('0', 2000)).toBe('')
+ })
+
+ it('returns an empty string when fromAmount is negative', () => {
+ expect(computeToAmount('-5', 2000)).toBe('')
+ })
+
+ it('returns an empty string when fromAmount is not a number', () => {
+ expect(computeToAmount('abc', 2000)).toBe('')
+ })
+
+ it('handles a fractional fromAmount', () => {
+ // 0.5 ETH at rate 2000 = 1000 USDT
+ expect(computeToAmount('0.5', 2000)).toBe('1,000')
+ })
+
+ it('handles a rate that produces a very small result', () => {
+ // 1 USDT at rate 0.0005 = 0.0005 ETH → small value, 8 dp
+ const result = computeToAmount('1', 0.0005)
+ expect(result).toBe('0.0005')
+ })
+
+ it('handles a minimum fromAmount of 0.000001', () => {
+ const result = computeToAmount('0.000001', 1)
+ expect(result).not.toBe('')
+ })
+})
+
+// ── normalizeTokenPrices ──────────────────────────────────────────────────────
+describe('normalizeTokenPrices', () => {
+ const makeRaw = (
+ currency: string,
+ price: number,
+ date = '2024-01-01T00:00:00Z',
+ ): TokenPrice => ({ currency, price, date })
+
+ it('returns a token list sorted alphabetically by symbol', () => {
+ const raw = [makeRaw('USDT', 1), makeRaw('ETH', 2000), makeRaw('BTC', 50000)]
+ const result = normalizeTokenPrices(raw)
+ const symbols = result.map((t) => t.symbol)
+ expect(symbols).toEqual(['BTC', 'ETH', 'USDT'])
+ })
+
+ it('filters out entries with a zero price', () => {
+ const raw = [makeRaw('ETH', 2000), makeRaw('BROKEN', 0)]
+ const result = normalizeTokenPrices(raw)
+ expect(result.map((t) => t.symbol)).toEqual(['ETH'])
+ })
+
+ it('filters out entries with a negative price', () => {
+ const raw = [makeRaw('ETH', 2000), makeRaw('NEG', -1)]
+ const result = normalizeTokenPrices(raw)
+ expect(result.map((t) => t.symbol)).toEqual(['ETH'])
+ })
+
+ it('deduplicates by keeping the entry with the most recent date', () => {
+ const raw = [
+ makeRaw('ETH', 1800, '2024-01-01T00:00:00Z'),
+ makeRaw('ETH', 2000, '2024-06-01T00:00:00Z'),
+ makeRaw('ETH', 1900, '2024-03-01T00:00:00Z'),
+ ]
+ const result = normalizeTokenPrices(raw)
+ expect(result).toHaveLength(1)
+ expect(result[0].price).toBe(2000)
+ })
+
+ it('returns an empty array when input is empty', () => {
+ expect(normalizeTokenPrices([])).toEqual([])
+ })
+
+ it('returns an empty array when all entries have invalid prices', () => {
+ const raw = [makeRaw('A', 0), makeRaw('B', -5)]
+ expect(normalizeTokenPrices(raw)).toEqual([])
+ })
+
+ it('builds the correct image URL for each token', () => {
+ const raw = [makeRaw('ETH', 2000)]
+ const result = normalizeTokenPrices(raw)
+ expect(result[0].imageUrl).toBe(
+ 'https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens/ETH.svg',
+ )
+ })
+
+ it('handles a single valid entry without error', () => {
+ const raw = [makeRaw('BTC', 50000)]
+ const result = normalizeTokenPrices(raw)
+ expect(result).toHaveLength(1)
+ expect(result[0].symbol).toBe('BTC')
+ expect(result[0].price).toBe(50000)
+ })
+})
diff --git a/src/problems/problem2/logic.ts b/src/problems/problem2/logic.ts
new file mode 100644
index 0000000000..55dadcca0f
--- /dev/null
+++ b/src/problems/problem2/logic.ts
@@ -0,0 +1,127 @@
+import { t } from '../../lib/i18n'
+import type { TokenInfo, TokenPrice } from './types'
+
+const TOKEN_IMAGE_BASE =
+ 'https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens'
+
+export const buildTokenImageUrl = (symbol: string): string => {
+ return `${TOKEN_IMAGE_BASE}/${symbol}.svg`
+}
+
+export const normalizeTokenPrices = (raw: TokenPrice[]): TokenInfo[] => {
+ const map = new Map()
+
+ for (const entry of raw) {
+ if (!entry.price || entry.price <= 0) {
+ continue
+ }
+ const existing = map.get(entry.currency)
+ if (!existing || new Date(entry.date) > new Date(existing.date)) {
+ map.set(entry.currency, entry)
+ }
+ }
+
+ const result: TokenInfo[] = []
+ for (const entry of map.values()) {
+ result.push({
+ symbol: entry.currency,
+ price: entry.price,
+ imageUrl: buildTokenImageUrl(entry.currency),
+ })
+ }
+
+ result.sort((a, b) => a.symbol.localeCompare(b.symbol))
+
+ return result
+}
+
+export const computeRate = (
+ from: TokenInfo | undefined,
+ to: TokenInfo | undefined,
+): number | null => {
+ if (!from || !to) {
+ return null
+ }
+ if (from.price <= 0 || to.price <= 0) {
+ return null
+ }
+ return from.price / to.price
+}
+
+// ── Banker's rounding (round half to even) ────────────────────────────────
+// Standard in financial systems: avoids systematic bias from always rounding 0.5 up.
+// e.g. 2.5 → 2, 3.5 → 4, 2.45 at 1dp → 2.4, 2.55 at 1dp → 2.6
+export const bankersRound = (value: number, decimals: number): number => {
+ const factor = 10 ** decimals
+ const shifted = value * factor
+ const floor = Math.floor(shifted)
+ const remainder = shifted - floor
+
+ // Floating point noise guard: treat values within epsilon of 0.5 as exactly halfway
+ if (Math.abs(remainder - 0.5) < 1e-9) {
+ return (floor % 2 === 0 ? floor : floor + 1) / factor
+ }
+ return Math.round(shifted) / factor
+}
+
+// Choose decimal places by order of magnitude — more precision for smaller values
+const decimalsByMagnitude = (abs: number): number => {
+ if (abs >= 100_000) {
+ return 2
+ }
+ if (abs >= 1_000) {
+ return 4
+ }
+ if (abs >= 1) {
+ return 6
+ }
+ if (abs >= 0.0001) {
+ return 8
+ }
+ // Very small: derive from position of first significant digit + 5 guard digits
+ return Math.max(8, -Math.floor(Math.log10(abs)) + 5)
+}
+
+export const formatAmount = (value: number): string => {
+ if (value === 0) {
+ return '0'
+ }
+ const abs = Math.abs(value)
+ const decimals = decimalsByMagnitude(abs)
+ const rounded = bankersRound(value, decimals)
+ return rounded.toLocaleString('en-US', {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: decimals,
+ })
+}
+
+export const computeToAmount = (
+ fromAmount: string,
+ rate: number | null,
+): string => {
+ if (!fromAmount || rate === null) {
+ return ''
+ }
+ const parsed = Number(fromAmount)
+ if (isNaN(parsed) || parsed <= 0) {
+ return ''
+ }
+ return formatAmount(parsed * rate)
+}
+
+export const fetchTokenPrices = async (): Promise => {
+ let res: Response
+ try {
+ res = await fetch('https://interview.switcheo.com/prices.json')
+ } catch {
+ throw new Error(t('swap.error_network'))
+ }
+ if (!res.ok) {
+ throw new Error(t('swap.error_http', { status: res.status }))
+ }
+ return res.json() as Promise
+}
+
+export const simulateSwap = (delayMs = 1500): Promise => {
+ return new Promise((resolve) => setTimeout(resolve, delayMs))
+}
diff --git a/src/problems/problem2/types.ts b/src/problems/problem2/types.ts
new file mode 100644
index 0000000000..c51797b164
--- /dev/null
+++ b/src/problems/problem2/types.ts
@@ -0,0 +1,27 @@
+export type TokenPrice = {
+ currency: string
+ date: string
+ price: number
+}
+
+export type TokenInfo = {
+ symbol: string
+ price: number
+ imageUrl: string
+}
+
+export type SwapFormValues = {
+ fromToken: string
+ fromAmount: string
+ toToken: string
+}
+
+export type SwapRecord = {
+ id: string
+ fromSymbol: string
+ fromAmount: number
+ toSymbol: string
+ toAmount: number
+ rate: number
+ timestamp: Date
+}
diff --git a/src/problems/problem2/useSwapForm.ts b/src/problems/problem2/useSwapForm.ts
new file mode 100644
index 0000000000..fd283637ba
--- /dev/null
+++ b/src/problems/problem2/useSwapForm.ts
@@ -0,0 +1,83 @@
+import { useForm } from 'react-hook-form';
+import { computeRate, computeToAmount } from './logic';
+import { t } from '../../lib/i18n';
+import type { TokenInfo, SwapFormValues } from './types';
+
+export const useSwapForm = (tokens: TokenInfo[]) => {
+ // onTouched: first error shown on blur, re-validates on change after that
+ // prevents showing errors mid-typing on untouched fields
+ const form = useForm({ mode: 'onTouched' });
+
+ // eslint-disable-next-line react-hooks/incompatible-library
+ const [watchedFrom, watchedAmount, watchedTo] = form.watch([
+ 'fromToken',
+ 'fromAmount',
+ 'toToken',
+ ]);
+
+ const fromToken = tokens.find((tk) => tk.symbol === watchedFrom);
+ const toToken = tokens.find((tk) => tk.symbol === watchedTo);
+
+ const rate = computeRate(fromToken, toToken);
+ const toAmount = computeToAmount(watchedAmount, rate);
+
+ // ── Registered fields with validation rules ────────────────────
+ const fromTokenField = form.register('fromToken', {
+ required: t('validation.required'),
+ });
+
+ const fromAmountField = form.register('fromAmount', {
+ required: t('validation.required'),
+ validate: {
+ number: (v) => !isNaN(Number(v)) || t('validation.amount_number'),
+ positive: (v) => Number(v) > 0 || t('validation.amount_positive'),
+ },
+ });
+
+ const toTokenField = form.register('toToken', {
+ required: t('validation.required'),
+ validate: (v) => v !== watchedFrom || t('validation.same_token'),
+ });
+
+ // ── Actions ────────────────────────────────────────────────────
+ const flipTokens = () => {
+ // Only swap the token selectors — fromAmount is user input, toAmount is derived
+ // shouldValidate only fires on fields already touched to avoid premature errors
+ form.setValue('fromToken', watchedTo, {
+ shouldValidate: form.getFieldState('fromToken').isTouched,
+ });
+ form.setValue('toToken', watchedFrom, {
+ shouldValidate: form.getFieldState('toToken').isTouched,
+ });
+ };
+
+ const resetForm = () => {
+ form.reset();
+ };
+
+ return {
+ // Registered fields (spread directly onto inputs)
+ fromTokenField,
+ fromAmountField,
+ toTokenField,
+
+ // Form state
+ errors: form.formState.errors,
+ isValid: form.formState.isValid,
+ handleSubmit: form.handleSubmit,
+ setValue: form.setValue,
+
+ // Derived
+ rate,
+ toAmount,
+ fromToken,
+ toToken,
+ watchedFrom,
+ watchedTo,
+ watchedAmount,
+
+ // Actions
+ flipTokens,
+ resetForm,
+ };
+};
diff --git a/src/problems/problem3/index.tsx b/src/problems/problem3/index.tsx
new file mode 100644
index 0000000000..57862f442c
--- /dev/null
+++ b/src/problems/problem3/index.tsx
@@ -0,0 +1,127 @@
+import Task from '../../components/Task'
+import WalletPage from './wallet_page'
+import refactoredPreview from './problem3_refactored.png'
+
+const ORIGINAL_CODE = `interface WalletBalance {
+ currency: string;
+ amount: number;
+}
+interface FormattedWalletBalance {
+ currency: string;
+ amount: number;
+ formatted: string;
+}
+
+interface Props extends BoxProps {}
+
+const WalletPage: React.FC = (props: Props) => {
+ const { children, ...rest } = props;
+ const balances = useWalletBalances();
+ const prices = usePrices();
+
+ const getPriority = (blockchain: any): number => {
+ switch (blockchain) {
+ case 'Osmosis': return 100
+ case 'Ethereum': return 50
+ case 'Arbitrum': return 30
+ case 'Zilliqa': return 20
+ case 'Neo': return 20
+ default: return -99
+ }
+ }
+
+ const sortedBalances = useMemo(() => {
+ return balances.filter((balance: WalletBalance) => {
+ const balancePriority = getPriority(balance.blockchain);
+ if (lhsPriority > -99) { // ❌ bug: lhsPriority is undefined
+ if (balance.amount <= 0) {
+ return true;
+ }
+ }
+ return false
+ }).sort((lhs: WalletBalance, rhs: WalletBalance) => {
+ const leftPriority = getPriority(lhs.blockchain);
+ const rightPriority = getPriority(rhs.blockchain);
+ if (leftPriority > rightPriority) {
+ return -1;
+ } else if (rightPriority > leftPriority) {
+ return 1;
+ }
+ // ❌ missing: no return for equal priorities (undefined)
+ });
+ }, [balances, prices]); // ❌ prices not used inside — spurious dependency
+
+ const formattedBalances = sortedBalances.map((balance: WalletBalance) => {
+ return {
+ ...balance,
+ formatted: balance.amount.toFixed() // ❌ toFixed() → no decimal precision
+ }
+ }) // ❌ formattedBalances computed but never used in rows
+
+ const rows = sortedBalances.map((balance: FormattedWalletBalance, index: number) => {
+ // ❌ iterating sortedBalances (WalletBalance), typed as FormattedWalletBalance
+ const usdValue = prices[balance.currency] * balance.amount;
+ return (
+
+ )
+ })
+
+ return (
+
+ {rows}
+
+ )
+}`
+
+export default function Problem3() {
+ return (
+
+ {/* ── Task statement ─────────────────────────────────────────────── */}
+
+
+ List out the computational inefficiencies and anti-patterns found in
+ the code block below.
+
+
+
+ This code block uses:
+
+ ReactJS with TypeScript.
+ Functional components.
+ React Hooks.
+
+
+
+ You should also provide a refactored version of the code, but more
+ points are awarded to accurately stating the issues and explaining
+ correctly how to improve them.
+
+
+
+ {ORIGINAL_CODE}
+
+
+
+ {/* ── Solution preview ───────────────────────────────────────────── */}
+
+
+ Solution Preview
+
+
+
+
+ {/* ── Refactored output ──────────────────────────────────────────── */}
+
+
+ )
+}
diff --git a/src/problems/problem3/logic.ts b/src/problems/problem3/logic.ts
new file mode 100644
index 0000000000..5fb150e169
--- /dev/null
+++ b/src/problems/problem3/logic.ts
@@ -0,0 +1,51 @@
+// ── Types ─────────────────────────────────────────────────────────────────────
+
+export type Blockchain = 'Osmosis' | 'Ethereum' | 'Arbitrum' | 'Zilliqa' | 'Neo'
+
+export interface BoxProps {
+ [key: string]: unknown
+}
+
+export interface WalletBalance {
+ currency: string
+ amount: number
+ blockchain: Blockchain
+}
+
+export interface FormattedWalletBalance extends WalletBalance {
+ formatted: string
+ usdValue: number
+}
+
+// ── Pure logic ────────────────────────────────────────────────────────────────
+
+export const getPriority = (blockchain: Blockchain): number => {
+ switch (blockchain) {
+ case 'Osmosis': return 100
+ case 'Ethereum': return 50
+ case 'Arbitrum': return 30
+ case 'Zilliqa': return 20
+ case 'Neo': return 20
+ default: return -99
+ }
+}
+
+// ── Mock hooks ────────────────────────────────────────────────────────────────
+
+export const useWalletBalances = (): WalletBalance[] => {
+ return [
+ { currency: 'OSMO', amount: 120.5, blockchain: 'Osmosis' },
+ { currency: 'ETH', amount: 2.75, blockchain: 'Ethereum' },
+ { currency: 'ARB', amount: 500, blockchain: 'Arbitrum' },
+ { currency: 'ZIL', amount: 0, blockchain: 'Zilliqa' },
+ { currency: 'NEO', amount: -5, blockchain: 'Neo' },
+ ]
+}
+
+export const usePrices = (): Record => {
+ return {
+ OSMO: 0.42,
+ ETH: 3200,
+ ARB: 1.15,
+ }
+}
diff --git a/src/problems/problem3/problem3_refactored.png b/src/problems/problem3/problem3_refactored.png
new file mode 100644
index 0000000000..e7cc24b157
Binary files /dev/null and b/src/problems/problem3/problem3_refactored.png differ
diff --git a/src/problems/problem3/wallet_page.tsx b/src/problems/problem3/wallet_page.tsx
new file mode 100644
index 0000000000..b7f7f0d325
--- /dev/null
+++ b/src/problems/problem3/wallet_page.tsx
@@ -0,0 +1,244 @@
+// ── ORIGINAL CODE + ISSUES ───────────────────────────────────────────────────
+//
+// [N1] WalletBalance is missing the `blockchain` field — TypeScript cannot
+// catch accesses to balance.blockchain anywhere in this component.
+//
+// [N2] FormattedWalletBalance duplicates WalletBalance fields manually instead
+// of extending the interface — any change to WalletBalance must be
+// mirrored here by hand.
+//
+/*
+
+interface WalletBalance {
+ currency: string;
+ amount: number;
+}
+
+interface FormattedWalletBalance { // [N2]
+ currency: string;
+ amount: number;
+ formatted: string;
+}
+
+interface Props extends BoxProps {}
+
+const WalletPage: React.FC = (props: Props) => {
+ const { children, ...rest } = props;
+ // [N3] children is destructured but never rendered — silently dropped.
+
+ const balances = useWalletBalances();
+ const prices = usePrices();
+
+ // [N4] getPriority is a pure function with no dependency on props or state.
+ // Defining it inside the component causes it to be redeclared on every
+ // render. It should live outside the component entirely.
+ // [N5] blockchain typed as `any` — loses all type safety and IDE support.
+ // [N6] Switch on raw string literals — a typo silently hits the default
+ // case. A union type or enum makes invalid values a compile-time error.
+ const getPriority = (blockchain: any): number => {
+ switch (blockchain) {
+ case 'Osmosis':
+ return 100
+ case 'Ethereum':
+ return 50
+ case 'Arbitrum':
+ return 30
+ case 'Zilliqa':
+ return 20
+ case 'Neo':
+ return 20
+ default:
+ return -99
+ }
+ }
+
+ const sortedBalances = useMemo(() => {
+ return balances.filter((balance: WalletBalance) => {
+ const balancePriority = getPriority(balance.blockchain);
+ // [N7] lhsPriority is never declared — balancePriority is computed above
+ // but lhsPriority is referenced here. ReferenceError at runtime;
+ // the filter never behaves as intended.
+ if (lhsPriority > -99) {
+ // [N8] Filter logic is inverted — this keeps balances with amount <= 0
+ // (empty wallets) and discards wallets that have funds.
+ if (balance.amount <= 0) {
+ return true;
+ }
+ }
+ return false;
+ }).sort((lhs: WalletBalance, rhs: WalletBalance) => {
+ const leftPriority = getPriority(lhs.blockchain);
+ const rightPriority = getPriority(rhs.blockchain);
+ if (leftPriority > rightPriority) {
+ return -1;
+ } else if (rightPriority > leftPriority) {
+ return 1;
+ }
+ // [N9] No return when priorities are equal — implicit undefined.
+ // Array.sort requires a number; undefined produces non-deterministic
+ // ordering across JS engines.
+ });
+ // [N10] prices is in the dependency array but is never read inside the memo —
+ // any price update triggers an unnecessary re-filter + re-sort.
+ }, [balances, prices]);
+
+ // [N11] formattedBalances is computed here but rows maps over sortedBalances
+ // instead — this entire .map() is dead code, its result is never used.
+ // [N12] toFixed() with no argument defaults to 0 decimal places ("42" instead
+ // of "42.00") — incorrect for currency display.
+ const formattedBalances = sortedBalances.map((balance: WalletBalance) => {
+ return {
+ ...balance,
+ formatted: balance.amount.toFixed() // [N12]
+ }
+ })
+
+ // [N13] rows maps sortedBalances (WalletBalance[]) but types each element as
+ // FormattedWalletBalance — balance.formatted is undefined at runtime
+ // because WalletBalance has no formatted field (see N11).
+ // [N14] index used as key — breaks React reconciliation when list order
+ // changes, causing incorrect component reuse and stale state.
+ // [N15] prices[balance.currency] may be undefined (currency missing from the
+ // price map) — undefined × number = NaN with no guard or fallback.
+ // [N16] classes is never declared in this component — ReferenceError at
+ // runtime. WalletRow should manage its own row styling.
+ const rows = sortedBalances.map((balance: FormattedWalletBalance, index: number) => {
+ const usdValue = prices[balance.currency] * balance.amount; // [N15]
+ return (
+
+ )
+ })
+
+ return (
+
+ {rows}
+ // [N3] children never rendered
+
+ )
+}
+
+*/
+
+// ── REFACTORED ────────────────────────────────────────────────────────────────
+
+import React, { useMemo } from 'react'
+
+import type { BoxProps, FormattedWalletBalance, WalletBalance } from './logic'
+import { getPriority, usePrices, useWalletBalances } from './logic'
+
+// ── WalletRow ─────────────────────────────────────────────────────────────────
+
+interface WalletRowProps {
+ index: number
+ currency: string
+ amount: number
+ usdValue: number
+ formattedAmount: string
+}
+
+const WalletRow: React.FC = (props: WalletRowProps) => {
+ const { index, currency, formattedAmount, usdValue } = props
+
+ return (
+
+ {index}
+ {currency}
+ {formattedAmount}
+ ${usdValue.toFixed(2)}
+
+ )
+}
+
+// ── WalletPage ────────────────────────────────────────────────────────────────
+
+interface Props extends BoxProps {
+ children?: React.ReactNode
+}
+
+const WalletPage: React.FC = (props: Props) => {
+ const { children, ...rest } = props
+ const balances = useWalletBalances()
+ const prices = usePrices()
+
+ const formattedBalances = useMemo((): FormattedWalletBalance[] => {
+ return balances
+ .reduce((acc: FormattedWalletBalance[], balance: WalletBalance) => {
+ const priority = getPriority(balance.blockchain)
+
+ if (priority <= -99) {
+ return acc
+ }
+
+ if (balance.amount <= 0) {
+ return acc
+ }
+
+ const usdPrice = prices[balance.currency]
+
+ let usdValue = 0
+ if (usdPrice !== undefined) {
+ usdValue = usdPrice * balance.amount
+ }
+
+ acc.push({
+ ...balance,
+ formatted: balance.amount.toFixed(2),
+ usdValue,
+ })
+
+ return acc
+ }, [])
+ .sort((lhs: FormattedWalletBalance, rhs: FormattedWalletBalance) => {
+ const leftPriority = getPriority(lhs.blockchain)
+ const rightPriority = getPriority(rhs.blockchain)
+
+ if (leftPriority > rightPriority) {
+ return -1
+ }
+
+ if (rightPriority > leftPriority) {
+ return 1
+ }
+
+ return 0
+ })
+ }, [balances, prices])
+
+ return (
+ }>
+
+
+
+ #
+ Currency
+ Amount
+ USD Value
+
+
+
+ {formattedBalances.map((balance: FormattedWalletBalance, index: number) => {
+ return (
+
+ )
+ })}
+
+
+ {children}
+
+ )
+}
+
+export default WalletPage
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000000..7f42e5f7cd
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000000..1ffef600d9
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000000..d3c52ea64c
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "module": "esnext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/video_demo.mov b/video_demo.mov
new file mode 100644
index 0000000000..734f93fb1b
Binary files /dev/null and b/video_demo.mov differ
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000000..37b6f5a064
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+import react, { reactCompilerPreset } from '@vitejs/plugin-react'
+import babel from '@rolldown/plugin-babel'
+import tailwindcss from '@tailwindcss/vite'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [
+ tailwindcss(),
+ react(),
+ babel({ presets: [reactCompilerPreset()] }),
+ ],
+})