feat: init a414062c
Steve · 2026-04-04 14:25 37 file(s) · +2020 −0
.gitignore (added) +24 −0
1 +
# Logs
2 +
logs
3 +
*.log
4 +
npm-debug.log*
5 +
yarn-debug.log*
6 +
yarn-error.log*
7 +
pnpm-debug.log*
8 +
lerna-debug.log*
9 +
10 +
node_modules
11 +
dist
12 +
dist-ssr
13 +
*.local
14 +
15 +
# Editor directories and files
16 +
.vscode/*
17 +
!.vscode/extensions.json
18 +
.idea
19 +
.DS_Store
20 +
*.suo
21 +
*.ntvs*
22 +
*.njsproj
23 +
*.sln
24 +
*.sw?
README.md (added) +73 −0
1 +
# React + TypeScript + Vite
2 +
3 +
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 +
5 +
Currently, two official plugins are available:
6 +
7 +
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8 +
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9 +
10 +
## React Compiler
11 +
12 +
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13 +
14 +
## Expanding the ESLint configuration
15 +
16 +
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17 +
18 +
```js
19 +
export default defineConfig([
20 +
  globalIgnores(['dist']),
21 +
  {
22 +
    files: ['**/*.{ts,tsx}'],
23 +
    extends: [
24 +
      // Other configs...
25 +
26 +
      // Remove tseslint.configs.recommended and replace with this
27 +
      tseslint.configs.recommendedTypeChecked,
28 +
      // Alternatively, use this for stricter rules
29 +
      tseslint.configs.strictTypeChecked,
30 +
      // Optionally, add this for stylistic rules
31 +
      tseslint.configs.stylisticTypeChecked,
32 +
33 +
      // Other configs...
34 +
    ],
35 +
    languageOptions: {
36 +
      parserOptions: {
37 +
        project: ['./tsconfig.node.json', './tsconfig.app.json'],
38 +
        tsconfigRootDir: import.meta.dirname,
39 +
      },
40 +
      // other options...
41 +
    },
42 +
  },
43 +
])
44 +
```
45 +
46 +
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47 +
48 +
```js
49 +
// eslint.config.js
50 +
import reactX from 'eslint-plugin-react-x'
51 +
import reactDom from 'eslint-plugin-react-dom'
52 +
53 +
export default defineConfig([
54 +
  globalIgnores(['dist']),
55 +
  {
56 +
    files: ['**/*.{ts,tsx}'],
57 +
    extends: [
58 +
      // Other configs...
59 +
      // Enable lint rules for React
60 +
      reactX.configs['recommended-typescript'],
61 +
      // Enable lint rules for React DOM
62 +
      reactDom.configs.recommended,
63 +
    ],
64 +
    languageOptions: {
65 +
      parserOptions: {
66 +
        project: ['./tsconfig.node.json', './tsconfig.app.json'],
67 +
        tsconfigRootDir: import.meta.dirname,
68 +
      },
69 +
      // other options...
70 +
    },
71 +
  },
72 +
])
73 +
```
bun.lock (added) +434 −0
1 +
{
2 +
  "lockfileVersion": 1,
3 +
  "configVersion": 1,
4 +
  "workspaces": {
5 +
    "": {
6 +
      "name": "scope",
7 +
      "dependencies": {
8 +
        "react": "^19.2.4",
9 +
        "react-dom": "^19.2.4",
10 +
      },
11 +
      "devDependencies": {
12 +
        "@eslint/js": "^9.39.4",
13 +
        "@types/node": "^24.12.0",
14 +
        "@types/react": "^19.2.14",
15 +
        "@types/react-dom": "^19.2.3",
16 +
        "@vitejs/plugin-react": "^6.0.1",
17 +
        "eslint": "^9.39.4",
18 +
        "eslint-plugin-react-hooks": "^7.0.1",
19 +
        "eslint-plugin-react-refresh": "^0.5.2",
20 +
        "globals": "^17.4.0",
21 +
        "typescript": "~5.9.3",
22 +
        "typescript-eslint": "^8.57.0",
23 +
        "vite": "^8.0.1",
24 +
      },
25 +
    },
26 +
  },
27 +
  "packages": {
28 +
    "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
29 +
30 +
    "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
31 +
32 +
    "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
33 +
34 +
    "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
35 +
36 +
    "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
37 +
38 +
    "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
39 +
40 +
    "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
41 +
42 +
    "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
43 +
44 +
    "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
45 +
46 +
    "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
47 +
48 +
    "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
49 +
50 +
    "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
51 +
52 +
    "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
53 +
54 +
    "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
55 +
56 +
    "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
57 +
58 +
    "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
59 +
60 +
    "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
61 +
62 +
    "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
63 +
64 +
    "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
65 +
66 +
    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
67 +
68 +
    "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
69 +
70 +
    "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
71 +
72 +
    "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
73 +
74 +
    "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
75 +
76 +
    "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
77 +
78 +
    "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
79 +
80 +
    "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
81 +
82 +
    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
83 +
84 +
    "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
85 +
86 +
    "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
87 +
88 +
    "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
89 +
90 +
    "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
91 +
92 +
    "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
93 +
94 +
    "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
95 +
96 +
    "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
97 +
98 +
    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
99 +
100 +
    "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
101 +
102 +
    "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
103 +
104 +
    "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
105 +
106 +
    "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="],
107 +
108 +
    "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="],
109 +
110 +
    "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="],
111 +
112 +
    "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="],
113 +
114 +
    "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="],
115 +
116 +
    "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="],
117 +
118 +
    "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="],
119 +
120 +
    "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="],
121 +
122 +
    "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="],
123 +
124 +
    "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="],
125 +
126 +
    "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="],
127 +
128 +
    "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="],
129 +
130 +
    "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="],
131 +
132 +
    "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="],
133 +
134 +
    "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="],
135 +
136 +
    "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
137 +
138 +
    "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
139 +
140 +
    "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
141 +
142 +
    "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
143 +
144 +
    "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="],
145 +
146 +
    "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
147 +
148 +
    "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
149 +
150 +
    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="],
151 +
152 +
    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="],
153 +
154 +
    "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="],
155 +
156 +
    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
157 +
158 +
    "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="],
159 +
160 +
    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="],
161 +
162 +
    "@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
163 +
164 +
    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="],
165 +
166 +
    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
167 +
168 +
    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
169 +
170 +
    "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
171 +
172 +
    "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
173 +
174 +
    "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
175 +
176 +
    "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
177 +
178 +
    "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
179 +
180 +
    "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
181 +
182 +
    "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
183 +
184 +
    "baseline-browser-mapping": ["baseline-browser-mapping@2.10.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA=="],
185 +
186 +
    "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="],
187 +
188 +
    "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
189 +
190 +
    "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
191 +
192 +
    "caniuse-lite": ["caniuse-lite@1.0.30001785", "", {}, "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ=="],
193 +
194 +
    "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
195 +
196 +
    "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
197 +
198 +
    "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
199 +
200 +
    "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
201 +
202 +
    "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
203 +
204 +
    "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
205 +
206 +
    "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
207 +
208 +
    "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
209 +
210 +
    "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
211 +
212 +
    "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
213 +
214 +
    "electron-to-chromium": ["electron-to-chromium@1.5.331", "", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="],
215 +
216 +
    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
217 +
218 +
    "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
219 +
220 +
    "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
221 +
222 +
    "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
223 +
224 +
    "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="],
225 +
226 +
    "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
227 +
228 +
    "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
229 +
230 +
    "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
231 +
232 +
    "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
233 +
234 +
    "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
235 +
236 +
    "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
237 +
238 +
    "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
239 +
240 +
    "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
241 +
242 +
    "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
243 +
244 +
    "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
245 +
246 +
    "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
247 +
248 +
    "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
249 +
250 +
    "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
251 +
252 +
    "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
253 +
254 +
    "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
255 +
256 +
    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
257 +
258 +
    "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
259 +
260 +
    "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
261 +
262 +
    "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="],
263 +
264 +
    "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
265 +
266 +
    "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
267 +
268 +
    "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
269 +
270 +
    "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
271 +
272 +
    "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
273 +
274 +
    "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
275 +
276 +
    "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
277 +
278 +
    "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
279 +
280 +
    "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
281 +
282 +
    "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
283 +
284 +
    "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
285 +
286 +
    "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
287 +
288 +
    "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
289 +
290 +
    "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
291 +
292 +
    "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
293 +
294 +
    "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
295 +
296 +
    "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
297 +
298 +
    "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
299 +
300 +
    "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
301 +
302 +
    "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
303 +
304 +
    "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
305 +
306 +
    "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
307 +
308 +
    "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
309 +
310 +
    "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
311 +
312 +
    "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
313 +
314 +
    "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
315 +
316 +
    "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
317 +
318 +
    "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
319 +
320 +
    "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
321 +
322 +
    "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
323 +
324 +
    "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
325 +
326 +
    "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
327 +
328 +
    "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
329 +
330 +
    "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
331 +
332 +
    "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
333 +
334 +
    "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
335 +
336 +
    "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
337 +
338 +
    "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
339 +
340 +
    "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
341 +
342 +
    "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
343 +
344 +
    "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
345 +
346 +
    "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
347 +
348 +
    "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
349 +
350 +
    "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
351 +
352 +
    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
353 +
354 +
    "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
355 +
356 +
    "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
357 +
358 +
    "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
359 +
360 +
    "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
361 +
362 +
    "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
363 +
364 +
    "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
365 +
366 +
    "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
367 +
368 +
    "rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
369 +
370 +
    "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
371 +
372 +
    "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
373 +
374 +
    "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
375 +
376 +
    "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
377 +
378 +
    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
379 +
380 +
    "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
381 +
382 +
    "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
383 +
384 +
    "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
385 +
386 +
    "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
387 +
388 +
    "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
389 +
390 +
    "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
391 +
392 +
    "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
393 +
394 +
    "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="],
395 +
396 +
    "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
397 +
398 +
    "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
399 +
400 +
    "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
401 +
402 +
    "vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.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" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
403 +
404 +
    "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
405 +
406 +
    "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
407 +
408 +
    "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
409 +
410 +
    "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
411 +
412 +
    "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
413 +
414 +
    "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
415 +
416 +
    "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
417 +
418 +
    "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
419 +
420 +
    "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
421 +
422 +
    "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
423 +
424 +
    "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
425 +
426 +
    "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
427 +
428 +
    "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
429 +
430 +
    "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
431 +
432 +
    "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
433 +
  }
434 +
}
eslint.config.js (added) +23 −0
1 +
import js from '@eslint/js'
2 +
import globals from 'globals'
3 +
import reactHooks from 'eslint-plugin-react-hooks'
4 +
import reactRefresh from 'eslint-plugin-react-refresh'
5 +
import tseslint from 'typescript-eslint'
6 +
import { defineConfig, globalIgnores } from 'eslint/config'
7 +
8 +
export default defineConfig([
9 +
  globalIgnores(['dist']),
10 +
  {
11 +
    files: ['**/*.{ts,tsx}'],
12 +
    extends: [
13 +
      js.configs.recommended,
14 +
      tseslint.configs.recommended,
15 +
      reactHooks.configs.flat.recommended,
16 +
      reactRefresh.configs.vite,
17 +
    ],
18 +
    languageOptions: {
19 +
      ecmaVersion: 2020,
20 +
      globals: globals.browser,
21 +
    },
22 +
  },
23 +
])
index.html (added) +35 −0
1 +
<!doctype html>
2 +
<html lang="en">
3 +
  <head>
4 +
    <meta charset="UTF-8" />
5 +
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 +
    <meta name="theme-color" content="#121113" />
7 +
    <title>Scope</title>
8 +
    <meta name="description" content="Minimal video scope" />
9 +
10 +
    <!-- Open Graph -->
11 +
    <meta property="og:type" content="website" />
12 +
    <meta property="og:url" content="https://scope.darkmatter.build" />
13 +
    <meta property="og:title" content="Scope" />
14 +
    <meta property="og:description" content="Minimal video scope" />
15 +
    <meta property="og:image" content="https://scope.darkmatter.build/og.png" />
16 +
    <meta property="og:site_name" content="Scope" />
17 +
18 +
    <!-- Twitter -->
19 +
    <meta name="twitter:card" content="summary_large_image" />
20 +
    <meta name="twitter:url" content="https://scope.darkmatter.build" />
21 +
    <meta name="twitter:title" content="Scope" />
22 +
    <meta name="twitter:description" content="Minimal video scope" />
23 +
    <meta name="twitter:image" content="https://scope.darkmatter.build/og.png" />
24 +
25 +
    <!-- Favicons -->
26 +
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
27 +
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
28 +
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
29 +
    <link rel="manifest" href="/site.webmanifest">
30 +
  </head>
31 +
  <body>
32 +
    <div id="root"></div>
33 +
    <script type="module" src="/src/main.tsx"></script>
34 +
  </body>
35 +
</html>
package.json (added) +30 −0
1 +
{
2 +
  "name": "scope",
3 +
  "private": true,
4 +
  "version": "0.0.0",
5 +
  "type": "module",
6 +
  "scripts": {
7 +
    "dev": "vite",
8 +
    "build": "tsc -b && vite build",
9 +
    "lint": "eslint .",
10 +
    "preview": "vite preview"
11 +
  },
12 +
  "dependencies": {
13 +
    "react": "^19.2.4",
14 +
    "react-dom": "^19.2.4"
15 +
  },
16 +
  "devDependencies": {
17 +
    "@eslint/js": "^9.39.4",
18 +
    "@types/node": "^24.12.0",
19 +
    "@types/react": "^19.2.14",
20 +
    "@types/react-dom": "^19.2.3",
21 +
    "@vitejs/plugin-react": "^6.0.1",
22 +
    "eslint": "^9.39.4",
23 +
    "eslint-plugin-react-hooks": "^7.0.1",
24 +
    "eslint-plugin-react-refresh": "^0.5.2",
25 +
    "globals": "^17.4.0",
26 +
    "typescript": "~5.9.3",
27 +
    "typescript-eslint": "^8.57.0",
28 +
    "vite": "^8.0.1"
29 +
  }
30 +
}
public/CommitMono-400-Italic.otf (added) +0 −0

Binary file — no preview.

public/CommitMono-400-Regular.otf (added) +0 −0

Binary file — no preview.

public/CommitMono-700-Italic.otf (added) +0 −0

Binary file — no preview.

public/CommitMono-700-Regular.otf (added) +0 −0

Binary file — no preview.

public/android-chrome-192x192.png (added) +0 −0

Binary file — no preview.

public/android-chrome-512x512.png (added) +0 −0

Binary file — no preview.

public/apple-touch-icon.png (added) +0 −0

Binary file — no preview.

public/favicon-16x16.png (added) +0 −0

Binary file — no preview.

public/favicon-32x32.png (added) +0 −0

Binary file — no preview.

public/favicon.ico (added) +0 −0

Binary file — no preview.

public/icon.png (added) +0 −0

Binary file — no preview.

public/og.png (added) +0 −0

Binary file — no preview.

public/site.webmanifest (added) +1 −0
1 +
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
src/App.css (added) +311 −0
1 +
/* Layout */
2 +
3 +
.app {
4 +
  width: 100vw;
5 +
  height: 100vh;
6 +
  position: relative;
7 +
  overflow: hidden;
8 +
  background: #121113;
9 +
}
10 +
11 +
/* Camera */
12 +
13 +
.camera-view {
14 +
  width: 100%;
15 +
  height: 100%;
16 +
  display: flex;
17 +
  align-items: center;
18 +
  justify-content: center;
19 +
}
20 +
21 +
.camera-canvas {
22 +
  width: 100%;
23 +
  height: 100%;
24 +
  object-fit: contain;
25 +
}
26 +
27 +
.camera-error {
28 +
  width: 100%;
29 +
  height: 100%;
30 +
  display: flex;
31 +
  align-items: center;
32 +
  justify-content: center;
33 +
}
34 +
35 +
.camera-error p {
36 +
  color: #ffffff;
37 +
  border-left: 2px solid #ffffff;
38 +
  padding-left: 0.5rem;
39 +
  font-size: 13px;
40 +
  opacity: 0.8;
41 +
}
42 +
43 +
/* Control Panel */
44 +
45 +
.control-panel {
46 +
  position: fixed;
47 +
  top: 0;
48 +
  right: 0;
49 +
  height: 100vh;
50 +
  width: 320px;
51 +
  transform: translateX(100%);
52 +
  transition: transform 0.3s ease;
53 +
  z-index: 10;
54 +
  display: flex;
55 +
}
56 +
57 +
.control-panel.open {
58 +
  transform: translateX(0);
59 +
}
60 +
61 +
.panel-toggle {
62 +
  position: absolute;
63 +
  left: -32px;
64 +
  top: 50%;
65 +
  transform: translateY(-50%);
66 +
  width: 32px;
67 +
  height: 64px;
68 +
  background: #121113;
69 +
  color: #ffffff;
70 +
  border: 1px solid #333;
71 +
  border-right: none;
72 +
  cursor: pointer;
73 +
  font-size: 20px;
74 +
  display: flex;
75 +
  align-items: center;
76 +
  justify-content: center;
77 +
  border-radius: 0;
78 +
  padding: 0;
79 +
}
80 +
81 +
.panel-toggle:hover {
82 +
  opacity: 0.7;
83 +
}
84 +
85 +
.panel-content {
86 +
  flex: 1;
87 +
  background: #121113;
88 +
  border-left: 1px solid #333;
89 +
  overflow-y: auto;
90 +
  padding: 1.5rem 1rem;
91 +
  display: flex;
92 +
  flex-direction: column;
93 +
  gap: 1.5rem;
94 +
  scrollbar-width: none;
95 +
}
96 +
97 +
.panel-content::-webkit-scrollbar {
98 +
  display: none;
99 +
}
100 +
101 +
.panel-header {
102 +
  display: flex;
103 +
  justify-content: space-between;
104 +
  align-items: center;
105 +
  padding-bottom: 0.75rem;
106 +
  border-bottom: 1px solid #333;
107 +
}
108 +
109 +
.panel-title {
110 +
  font-size: 16px;
111 +
  font-weight: 700;
112 +
  text-transform: uppercase;
113 +
}
114 +
115 +
.reset-btn {
116 +
  background: #121113;
117 +
  color: #ffffff;
118 +
  border: 1px solid white;
119 +
  padding: 0.25rem 0.5rem;
120 +
  font-size: 12px;
121 +
  cursor: pointer;
122 +
  border-radius: 0;
123 +
}
124 +
125 +
.reset-btn:hover {
126 +
  opacity: 0.7;
127 +
}
128 +
129 +
/* Randomize */
130 +
131 +
.randomize-section {
132 +
  display: flex;
133 +
  gap: 0.5rem;
134 +
}
135 +
136 +
.randomize-btn {
137 +
  flex: 1;
138 +
  background: #121113;
139 +
  color: #ffffff;
140 +
  border: 1px solid #555;
141 +
  padding: 0.4rem 0;
142 +
  font-size: 12px;
143 +
  font-weight: 700;
144 +
  cursor: pointer;
145 +
  border-radius: 0;
146 +
}
147 +
148 +
.randomize-btn:hover {
149 +
  border-color: #ffffff;
150 +
}
151 +
152 +
.fluid-toggle {
153 +
  background: #121113;
154 +
  color: #ffffff;
155 +
  border: 1px solid #555;
156 +
  padding: 0.4rem 0.75rem;
157 +
  font-size: 12px;
158 +
  font-weight: 700;
159 +
  cursor: pointer;
160 +
  border-radius: 0;
161 +
  opacity: 0.5;
162 +
  transition: opacity 0.15s, border-color 0.15s;
163 +
}
164 +
165 +
.fluid-toggle:hover {
166 +
  opacity: 0.7;
167 +
}
168 +
169 +
.fluid-toggle.active {
170 +
  opacity: 1;
171 +
  border-color: #ffffff;
172 +
}
173 +
174 +
.panel-header-actions {
175 +
  display: flex;
176 +
  gap: 0.5rem;
177 +
  align-items: center;
178 +
}
179 +
180 +
/* Sections */
181 +
182 +
.panel-section {
183 +
  display: flex;
184 +
  flex-direction: column;
185 +
  gap: 0.75rem;
186 +
}
187 +
188 +
.section-label {
189 +
  font-size: 12px;
190 +
  opacity: 0.5;
191 +
  text-transform: uppercase;
192 +
  font-weight: 700;
193 +
}
194 +
195 +
/* Slider */
196 +
197 +
.slider-group {
198 +
  display: flex;
199 +
  flex-direction: column;
200 +
  gap: 0.5rem;
201 +
}
202 +
203 +
.slider-row {
204 +
  display: flex;
205 +
  flex-direction: column;
206 +
  gap: 0.2rem;
207 +
}
208 +
209 +
.slider-header {
210 +
  display: flex;
211 +
  justify-content: space-between;
212 +
  align-items: center;
213 +
}
214 +
215 +
.slider-header label {
216 +
  font-size: 12px;
217 +
  opacity: 0.7;
218 +
}
219 +
220 +
.slider-value {
221 +
  font-size: 12px;
222 +
  opacity: 0.5;
223 +
}
224 +
225 +
/* Range Input Styling */
226 +
227 +
input[type="range"] {
228 +
  -webkit-appearance: none;
229 +
  appearance: none;
230 +
  width: 100%;
231 +
  height: 2px;
232 +
  background: #555;
233 +
  outline: none;
234 +
  border: none;
235 +
  border-radius: 0;
236 +
}
237 +
238 +
input[type="range"]::-webkit-slider-thumb {
239 +
  -webkit-appearance: none;
240 +
  appearance: none;
241 +
  width: 12px;
242 +
  height: 12px;
243 +
  background: #ffffff;
244 +
  border: none;
245 +
  border-radius: 0;
246 +
  cursor: pointer;
247 +
}
248 +
249 +
input[type="range"]::-moz-range-thumb {
250 +
  width: 12px;
251 +
  height: 12px;
252 +
  background: #ffffff;
253 +
  border: none;
254 +
  border-radius: 0;
255 +
  cursor: pointer;
256 +
}
257 +
258 +
input[type="range"]::-webkit-slider-thumb:hover {
259 +
  opacity: 0.7;
260 +
}
261 +
262 +
input[type="range"]::-moz-range-thumb:hover {
263 +
  opacity: 0.7;
264 +
}
265 +
266 +
/* Curves Editor */
267 +
268 +
.curves-editor {
269 +
  display: flex;
270 +
  flex-direction: column;
271 +
  gap: 0.5rem;
272 +
}
273 +
274 +
.curves-tabs {
275 +
  display: flex;
276 +
  gap: 0;
277 +
}
278 +
279 +
.curves-tab {
280 +
  flex: 1;
281 +
  background: #121113;
282 +
  color: #ffffff;
283 +
  border: 1px solid #333;
284 +
  padding: 0.3rem 0;
285 +
  font-size: 12px;
286 +
  cursor: pointer;
287 +
  border-radius: 0;
288 +
  opacity: 0.5;
289 +
}
290 +
291 +
.curves-tab + .curves-tab {
292 +
  border-left: none;
293 +
}
294 +
295 +
.curves-tab.active {
296 +
  opacity: 1;
297 +
  border-color: #ffffff;
298 +
}
299 +
300 +
.curves-tab:hover {
301 +
  opacity: 0.7;
302 +
}
303 +
304 +
.curves-canvas {
305 +
  cursor: crosshair;
306 +
  border: 1px solid #333;
307 +
  display: block;
308 +
  width: 100% !important;
309 +
  height: auto !important;
310 +
  aspect-ratio: 1;
311 +
}
src/App.tsx (added) +46 −0
1 +
import { useReducer } from "react";
2 +
import type { BasicFilters, CurveChannel, CurvePoint, FilterState } from "./lib/types";
3 +
import { DEFAULT_FILTER_STATE } from "./lib/types";
4 +
import { CameraView } from "./components/CameraView";
5 +
import { ControlPanel } from "./components/ControlPanel";
6 +
import "./App.css";
7 +
8 +
type Action =
9 +
  | { type: "SET_BASIC"; key: keyof BasicFilters; value: number }
10 +
  | { type: "SET_CURVE"; channel: CurveChannel; points: CurvePoint[] }
11 +
  | { type: "SET_ALL"; state: FilterState }
12 +
  | { type: "RESET" };
13 +
14 +
function reducer(state: FilterState, action: Action): FilterState {
15 +
  switch (action.type) {
16 +
    case "SET_BASIC":
17 +
      return { ...state, basic: { ...state.basic, [action.key]: action.value } };
18 +
    case "SET_CURVE":
19 +
      return { ...state, curves: { ...state.curves, [action.channel]: action.points } };
20 +
    case "SET_ALL":
21 +
      return action.state;
22 +
    case "RESET":
23 +
      return { ...DEFAULT_FILTER_STATE };
24 +
    default:
25 +
      return state;
26 +
  }
27 +
}
28 +
29 +
function App() {
30 +
  const [filterState, dispatch] = useReducer(reducer, DEFAULT_FILTER_STATE);
31 +
32 +
  return (
33 +
    <div className="app">
34 +
      <CameraView filterState={filterState} />
35 +
      <ControlPanel
36 +
        filterState={filterState}
37 +
        onBasicChange={(key, value) => dispatch({ type: "SET_BASIC", key, value })}
38 +
        onCurveChange={(channel, points) => dispatch({ type: "SET_CURVE", channel, points })}
39 +
        onSetAll={(state: FilterState) => dispatch({ type: "SET_ALL", state })}
40 +
        onReset={() => dispatch({ type: "RESET" })}
41 +
      />
42 +
    </div>
43 +
  );
44 +
}
45 +
46 +
export default App;
src/components/CameraView.tsx (added) +83 −0
1 +
import { useRef, useMemo, useEffect, useState } from "react";
2 +
import { useCamera } from "../hooks/useCamera";
3 +
import { useAnimationLoop } from "../hooks/useAnimationLoop";
4 +
import { buildCSSFilter, buildLUT } from "../lib/filters";
5 +
import type { FilterState } from "../lib/types";
6 +
7 +
interface CameraViewProps {
8 +
  filterState: FilterState;
9 +
}
10 +
11 +
export function CameraView({ filterState }: CameraViewProps) {
12 +
  const { videoRef, error, isReady } = useCamera();
13 +
  const canvasRef = useRef<HTMLCanvasElement>(null);
14 +
  const [videoDimensions, setVideoDimensions] = useState({ w: 1280, h: 720 });
15 +
16 +
  useEffect(() => {
17 +
    const video = videoRef.current;
18 +
    if (!video) return;
19 +
20 +
    function onMeta() {
21 +
      setVideoDimensions({ w: video!.videoWidth, h: video!.videoHeight });
22 +
    }
23 +
    video.addEventListener("loadedmetadata", onMeta);
24 +
    return () => video.removeEventListener("loadedmetadata", onMeta);
25 +
  }, [videoRef]);
26 +
27 +
  const cssFilter = useMemo(() => buildCSSFilter(filterState.basic), [filterState.basic]);
28 +
  const lut = useMemo(
29 +
    () => buildLUT(filterState.basic, filterState.curves),
30 +
    [filterState.basic, filterState.curves],
31 +
  );
32 +
33 +
  useAnimationLoop(() => {
34 +
    const video = videoRef.current;
35 +
    const canvas = canvasRef.current;
36 +
    if (!video || !canvas || !isReady || video.readyState < 2) return;
37 +
38 +
    const ctx = canvas.getContext("2d", { willReadFrequently: !lut.isIdentity });
39 +
    if (!ctx) return;
40 +
41 +
    canvas.width = videoDimensions.w;
42 +
    canvas.height = videoDimensions.h;
43 +
44 +
    ctx.filter = cssFilter;
45 +
    ctx.drawImage(video, 0, 0, videoDimensions.w, videoDimensions.h);
46 +
47 +
    if (!lut.isIdentity) {
48 +
      ctx.filter = "none";
49 +
      const imageData = ctx.getImageData(0, 0, videoDimensions.w, videoDimensions.h);
50 +
      const d = imageData.data;
51 +
      const lr = lut.r;
52 +
      const lg = lut.g;
53 +
      const lb = lut.b;
54 +
      for (let i = 0; i < d.length; i += 4) {
55 +
        d[i] = lr[d[i]];
56 +
        d[i + 1] = lg[d[i + 1]];
57 +
        d[i + 2] = lb[d[i + 2]];
58 +
      }
59 +
      ctx.putImageData(imageData, 0, 0);
60 +
    }
61 +
  });
62 +
63 +
  if (error) {
64 +
    return (
65 +
      <div className="camera-error">
66 +
        <p>{error}</p>
67 +
      </div>
68 +
    );
69 +
  }
70 +
71 +
  return (
72 +
    <div className="camera-view">
73 +
      <video
74 +
        ref={videoRef}
75 +
        autoPlay
76 +
        playsInline
77 +
        muted
78 +
        style={{ position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" }}
79 +
      />
80 +
      <canvas ref={canvasRef} className="camera-canvas" />
81 +
    </div>
82 +
  );
83 +
}
src/components/ControlPanel.tsx (added) +218 −0
1 +
import { useState, useEffect, useRef, useCallback } from "react";
2 +
import type { BasicFilters, CurveChannel, CurvePoint, FilterState } from "../lib/types";
3 +
import { DEFAULT_BASIC, DEFAULT_CURVES } from "../lib/types";
4 +
import { SliderGroup } from "./SliderGroup";
5 +
import { CurvesEditor } from "./CurvesEditor";
6 +
7 +
const SLIDER_RANGES: Record<keyof BasicFilters, [number, number]> = {
8 +
  brightness: [0, 200],
9 +
  contrast: [0, 200],
10 +
  exposure: [-100, 100],
11 +
  saturation: [0, 200],
12 +
  temperature: [-100, 100],
13 +
  tint: [-100, 100],
14 +
  highlights: [-100, 100],
15 +
  shadows: [-100, 100],
16 +
};
17 +
18 +
function randBetween(min: number, max: number) {
19 +
  return Math.round(min + Math.random() * (max - min));
20 +
}
21 +
22 +
function randomCurve(): CurvePoint[] {
23 +
  const numMid = 1 + Math.floor(Math.random() * 3);
24 +
  const points: CurvePoint[] = [{ x: 0, y: randBetween(0, 60) }];
25 +
  for (let i = 0; i < numMid; i++) {
26 +
    const x = Math.round(((i + 1) / (numMid + 1)) * 255);
27 +
    points.push({ x, y: randBetween(30, 225) });
28 +
  }
29 +
  points.push({ x: 255, y: randBetween(195, 255) });
30 +
  return points;
31 +
}
32 +
33 +
function generateRandomState(): FilterState {
34 +
  const basic = {} as BasicFilters;
35 +
  for (const key of Object.keys(SLIDER_RANGES) as (keyof BasicFilters)[]) {
36 +
    const [min, max] = SLIDER_RANGES[key];
37 +
    basic[key] = randBetween(min, max);
38 +
  }
39 +
  return {
40 +
    basic,
41 +
    curves: {
42 +
      rgb: randomCurve(),
43 +
      r: randomCurve(),
44 +
      g: randomCurve(),
45 +
      b: randomCurve(),
46 +
    },
47 +
  };
48 +
}
49 +
50 +
function lerpNum(a: number, b: number, t: number) {
51 +
  return a + (b - a) * t;
52 +
}
53 +
54 +
function lerpBasic(a: BasicFilters, b: BasicFilters, t: number): BasicFilters {
55 +
  const result = {} as BasicFilters;
56 +
  for (const key of Object.keys(a) as (keyof BasicFilters)[]) {
57 +
    result[key] = Math.round(lerpNum(a[key], b[key], t));
58 +
  }
59 +
  return result;
60 +
}
61 +
62 +
function lerpCurve(a: CurvePoint[], b: CurvePoint[], t: number): CurvePoint[] {
63 +
  // Resample both curves to a common set of x points for smooth interpolation
64 +
  const xs = new Set<number>();
65 +
  a.forEach((p) => xs.add(p.x));
66 +
  b.forEach((p) => xs.add(p.x));
67 +
  const sortedX = Array.from(xs).sort((a, b) => a - b);
68 +
69 +
  return sortedX.map((x) => ({
70 +
    x,
71 +
    y: Math.round(lerpNum(sampleCurve(a, x), sampleCurve(b, x), t)),
72 +
  }));
73 +
}
74 +
75 +
function sampleCurve(points: CurvePoint[], x: number): number {
76 +
  if (x <= points[0].x) return points[0].y;
77 +
  if (x >= points[points.length - 1].x) return points[points.length - 1].y;
78 +
  for (let i = 0; i < points.length - 1; i++) {
79 +
    if (x >= points[i].x && x <= points[i + 1].x) {
80 +
      const t = (x - points[i].x) / (points[i + 1].x - points[i].x);
81 +
      return lerpNum(points[i].y, points[i + 1].y, t);
82 +
    }
83 +
  }
84 +
  return points[points.length - 1].y;
85 +
}
86 +
87 +
function lerpState(a: FilterState, b: FilterState, t: number): FilterState {
88 +
  return {
89 +
    basic: lerpBasic(a.basic, b.basic, t),
90 +
    curves: {
91 +
      rgb: lerpCurve(a.curves.rgb, b.curves.rgb, t),
92 +
      r: lerpCurve(a.curves.r, b.curves.r, t),
93 +
      g: lerpCurve(a.curves.g, b.curves.g, t),
94 +
      b: lerpCurve(a.curves.b, b.curves.b, t),
95 +
    },
96 +
  };
97 +
}
98 +
99 +
interface ControlPanelProps {
100 +
  filterState: FilterState;
101 +
  onBasicChange: (key: keyof BasicFilters, value: number) => void;
102 +
  onCurveChange: (channel: CurveChannel, points: CurvePoint[]) => void;
103 +
  onSetAll: (state: FilterState) => void;
104 +
  onReset: () => void;
105 +
}
106 +
107 +
export function ControlPanel({ filterState, onBasicChange, onCurveChange, onSetAll, onReset }: ControlPanelProps) {
108 +
  const [open, setOpen] = useState(false);
109 +
  const [fluid, setFluid] = useState(false);
110 +
111 +
  const fluidRef = useRef(false);
112 +
  const rafRef = useRef<number>(0);
113 +
  const fromRef = useRef<FilterState>(filterState);
114 +
  const toRef = useRef<FilterState>(generateRandomState());
115 +
  const progressRef = useRef(0);
116 +
  const onSetAllRef = useRef(onSetAll);
117 +
  onSetAllRef.current = onSetAll;
118 +
119 +
  const startFluid = useCallback(() => {
120 +
    fluidRef.current = true;
121 +
    fromRef.current = filterState;
122 +
    toRef.current = generateRandomState();
123 +
    progressRef.current = 0;
124 +
125 +
    const LERP_SPEED = 0.008;
126 +
127 +
    const tick = () => {
128 +
      if (!fluidRef.current) return;
129 +
      progressRef.current += LERP_SPEED;
130 +
      if (progressRef.current >= 1) {
131 +
        fromRef.current = toRef.current;
132 +
        toRef.current = generateRandomState();
133 +
        progressRef.current = 0;
134 +
      }
135 +
      // Smooth easing
136 +
      const t = progressRef.current * progressRef.current * (3 - 2 * progressRef.current);
137 +
      const interpolated = lerpState(fromRef.current, toRef.current, t);
138 +
      onSetAllRef.current(interpolated);
139 +
      rafRef.current = requestAnimationFrame(tick);
140 +
    };
141 +
    rafRef.current = requestAnimationFrame(tick);
142 +
  }, [filterState]);
143 +
144 +
  const stopFluid = useCallback(() => {
145 +
    fluidRef.current = false;
146 +
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
147 +
  }, []);
148 +
149 +
  useEffect(() => {
150 +
    return () => {
151 +
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
152 +
    };
153 +
  }, []);
154 +
155 +
  const handleFluidToggle = () => {
156 +
    if (fluid) {
157 +
      stopFluid();
158 +
      setFluid(false);
159 +
    } else {
160 +
      setFluid(true);
161 +
      startFluid();
162 +
    }
163 +
  };
164 +
165 +
  const handleRandomize = () => {
166 +
    if (fluid) {
167 +
      stopFluid();
168 +
      setFluid(false);
169 +
    }
170 +
    onSetAll(generateRandomState());
171 +
  };
172 +
173 +
  const hasChanges =
174 +
    JSON.stringify(filterState.basic) !== JSON.stringify(DEFAULT_BASIC) ||
175 +
    JSON.stringify(filterState.curves) !== JSON.stringify(DEFAULT_CURVES);
176 +
177 +
  return (
178 +
    <div className={`control-panel ${open ? "open" : ""}`}>
179 +
      <button className="panel-toggle" onClick={() => setOpen(!open)}>
180 +
        {open ? "\u203A" : "\u2039"}
181 +
      </button>
182 +
      <div className="panel-content">
183 +
        <div className="panel-header">
184 +
          <span className="panel-title">CONTROLS</span>
185 +
          <div className="panel-header-actions">
186 +
            {hasChanges && (
187 +
              <button className="reset-btn" onClick={onReset}>
188 +
                RESET
189 +
              </button>
190 +
            )}
191 +
          </div>
192 +
        </div>
193 +
194 +
        <div className="randomize-section">
195 +
          <button className="randomize-btn" onClick={handleRandomize}>
196 +
            RANDOMIZE
197 +
          </button>
198 +
          <button
199 +
            className={`fluid-toggle ${fluid ? "active" : ""}`}
200 +
            onClick={handleFluidToggle}
201 +
          >
202 +
            FLUID
203 +
          </button>
204 +
        </div>
205 +
206 +
        <div className="panel-section">
207 +
          <span className="section-label">ADJUSTMENTS</span>
208 +
          <SliderGroup filters={filterState.basic} onChange={onBasicChange} />
209 +
        </div>
210 +
211 +
        <div className="panel-section">
212 +
          <span className="section-label">CURVES</span>
213 +
          <CurvesEditor curves={filterState.curves} onChange={onCurveChange} />
214 +
        </div>
215 +
      </div>
216 +
    </div>
217 +
  );
218 +
}
src/components/CurvesEditor.tsx (added) +200 −0
1 +
import { useRef, useEffect, useCallback, useState } from "react";
2 +
import type { CurvePoint, CurveChannel, CurvesState } from "../lib/types";
3 +
import { interpolateSpline } from "../lib/curves";
4 +
5 +
interface CurvesEditorProps {
6 +
  curves: CurvesState;
7 +
  onChange: (channel: CurveChannel, points: CurvePoint[]) => void;
8 +
}
9 +
10 +
const SIZE = 256;
11 +
const HANDLE_RADIUS = 6;
12 +
const CHANNELS: CurveChannel[] = ["rgb", "r", "g", "b"];
13 +
const CHANNEL_LABELS: Record<CurveChannel, string> = {
14 +
  rgb: "RGB",
15 +
  r: "R",
16 +
  g: "G",
17 +
  b: "B",
18 +
};
19 +
20 +
export function CurvesEditor({ curves, onChange }: CurvesEditorProps) {
21 +
  const canvasRef = useRef<HTMLCanvasElement>(null);
22 +
  const [activeChannel, setActiveChannel] = useState<CurveChannel>("rgb");
23 +
  const draggingRef = useRef<number | null>(null);
24 +
25 +
  const points = curves[activeChannel];
26 +
27 +
  const draw = useCallback(() => {
28 +
    const canvas = canvasRef.current;
29 +
    if (!canvas) return;
30 +
    const ctx = canvas.getContext("2d");
31 +
    if (!ctx) return;
32 +
33 +
    const dpr = window.devicePixelRatio || 1;
34 +
    const w = SIZE;
35 +
    const h = SIZE;
36 +
37 +
    canvas.width = w * dpr;
38 +
    canvas.height = h * dpr;
39 +
    ctx.scale(dpr, dpr);
40 +
41 +
    // Background
42 +
    ctx.fillStyle = "#1e1c1f";
43 +
    ctx.fillRect(0, 0, w, h);
44 +
45 +
    // Grid
46 +
    ctx.strokeStyle = "#333";
47 +
    ctx.lineWidth = 0.5;
48 +
    for (let i = 1; i < 4; i++) {
49 +
      const pos = (i / 4) * w;
50 +
      ctx.beginPath();
51 +
      ctx.moveTo(pos, 0);
52 +
      ctx.lineTo(pos, h);
53 +
      ctx.stroke();
54 +
      ctx.beginPath();
55 +
      ctx.moveTo(0, pos);
56 +
      ctx.lineTo(w, pos);
57 +
      ctx.stroke();
58 +
    }
59 +
60 +
    // Diagonal reference
61 +
    ctx.strokeStyle = "#555";
62 +
    ctx.lineWidth = 1;
63 +
    ctx.setLineDash([4, 4]);
64 +
    ctx.beginPath();
65 +
    ctx.moveTo(0, h);
66 +
    ctx.lineTo(w, 0);
67 +
    ctx.stroke();
68 +
    ctx.setLineDash([]);
69 +
70 +
    // Curve
71 +
    const lut = interpolateSpline(points);
72 +
    ctx.strokeStyle = "#ffffff";
73 +
    ctx.lineWidth = 1.5;
74 +
    ctx.beginPath();
75 +
    for (let x = 0; x < 256; x++) {
76 +
      const px = (x / 255) * w;
77 +
      const py = h - (lut[x] / 255) * h;
78 +
      if (x === 0) ctx.moveTo(px, py);
79 +
      else ctx.lineTo(px, py);
80 +
    }
81 +
    ctx.stroke();
82 +
83 +
    // Control points
84 +
    for (const point of points) {
85 +
      const px = (point.x / 255) * w;
86 +
      const py = h - (point.y / 255) * h;
87 +
      ctx.fillStyle = "#121113";
88 +
      ctx.strokeStyle = "#ffffff";
89 +
      ctx.lineWidth = 1.5;
90 +
      ctx.beginPath();
91 +
      ctx.arc(px, py, HANDLE_RADIUS, 0, Math.PI * 2);
92 +
      ctx.fill();
93 +
      ctx.stroke();
94 +
    }
95 +
  }, [points]);
96 +
97 +
  useEffect(() => {
98 +
    draw();
99 +
  }, [draw]);
100 +
101 +
  function canvasToPoint(e: React.PointerEvent): { x: number; y: number } {
102 +
    const canvas = canvasRef.current!;
103 +
    const rect = canvas.getBoundingClientRect();
104 +
    const x = ((e.clientX - rect.left) / rect.width) * 255;
105 +
    const y = (1 - (e.clientY - rect.top) / rect.height) * 255;
106 +
    return { x: Math.round(x), y: Math.round(Math.max(0, Math.min(255, y))) };
107 +
  }
108 +
109 +
  function findNearestPoint(cx: number, cy: number): number | null {
110 +
    const threshold = 15;
111 +
    let bestIdx: number | null = null;
112 +
    let bestDist = Infinity;
113 +
    for (let i = 0; i < points.length; i++) {
114 +
      const dx = points[i].x - cx;
115 +
      const dy = points[i].y - cy;
116 +
      const dist = Math.sqrt(dx * dx + dy * dy);
117 +
      if (dist < threshold && dist < bestDist) {
118 +
        bestDist = dist;
119 +
        bestIdx = i;
120 +
      }
121 +
    }
122 +
    return bestIdx;
123 +
  }
124 +
125 +
  function handlePointerDown(e: React.PointerEvent) {
126 +
    const { x, y } = canvasToPoint(e);
127 +
    const idx = findNearestPoint(x, y);
128 +
129 +
    if (idx !== null) {
130 +
      draggingRef.current = idx;
131 +
    } else {
132 +
      // Add new point
133 +
      const newPoints = [...points, { x, y }].sort((a, b) => a.x - b.x);
134 +
      onChange(activeChannel, newPoints);
135 +
      const newIdx = newPoints.findIndex((p) => p.x === x && p.y === y);
136 +
      draggingRef.current = newIdx;
137 +
    }
138 +
139 +
    (e.target as Element).setPointerCapture(e.pointerId);
140 +
  }
141 +
142 +
  function handlePointerMove(e: React.PointerEvent) {
143 +
    if (draggingRef.current === null) return;
144 +
    const { y } = canvasToPoint(e);
145 +
    const idx = draggingRef.current;
146 +
147 +
    const updated = [...points];
148 +
    // Endpoints can only move vertically
149 +
    if (idx === 0 || idx === points.length - 1) {
150 +
      updated[idx] = { ...updated[idx], y };
151 +
    } else {
152 +
      const rect = canvasRef.current!.getBoundingClientRect();
153 +
      const rawX = ((e.clientX - rect.left) / rect.width) * 255;
154 +
      const x = Math.round(Math.max(updated[idx - 1].x + 1, Math.min(updated[idx + 1].x - 1, rawX)));
155 +
      updated[idx] = { x, y };
156 +
    }
157 +
    onChange(activeChannel, updated);
158 +
  }
159 +
160 +
  function handlePointerUp() {
161 +
    draggingRef.current = null;
162 +
  }
163 +
164 +
  function handleDoubleClick(e: React.MouseEvent) {
165 +
    const canvas = canvasRef.current!;
166 +
    const rect = canvas.getBoundingClientRect();
167 +
    const cx = ((e.clientX - rect.left) / rect.width) * 255;
168 +
    const cy = (1 - (e.clientY - rect.top) / rect.height) * 255;
169 +
    const idx = findNearestPoint(cx, cy);
170 +
    if (idx !== null && idx !== 0 && idx !== points.length - 1) {
171 +
      const updated = points.filter((_, i) => i !== idx);
172 +
      onChange(activeChannel, updated);
173 +
    }
174 +
  }
175 +
176 +
  return (
177 +
    <div className="curves-editor">
178 +
      <div className="curves-tabs">
179 +
        {CHANNELS.map((ch) => (
180 +
          <button
181 +
            key={ch}
182 +
            className={`curves-tab ${activeChannel === ch ? "active" : ""}`}
183 +
            onClick={() => setActiveChannel(ch)}
184 +
          >
185 +
            {CHANNEL_LABELS[ch]}
186 +
          </button>
187 +
        ))}
188 +
      </div>
189 +
      <canvas
190 +
        ref={canvasRef}
191 +
        className="curves-canvas"
192 +
        style={{ width: SIZE, height: SIZE }}
193 +
        onPointerDown={handlePointerDown}
194 +
        onPointerMove={handlePointerMove}
195 +
        onPointerUp={handlePointerUp}
196 +
        onDoubleClick={handleDoubleClick}
197 +
      />
198 +
    </div>
199 +
  );
200 +
}
src/components/Slider.tsx (added) +27 −0
1 +
interface SliderProps {
2 +
  label: string;
3 +
  value: number;
4 +
  min: number;
5 +
  max: number;
6 +
  step?: number;
7 +
  onChange: (value: number) => void;
8 +
}
9 +
10 +
export function Slider({ label, value, min, max, step = 1, onChange }: SliderProps) {
11 +
  return (
12 +
    <div className="slider-row">
13 +
      <div className="slider-header">
14 +
        <label>{label}</label>
15 +
        <span className="slider-value">{value}</span>
16 +
      </div>
17 +
      <input
18 +
        type="range"
19 +
        min={min}
20 +
        max={max}
21 +
        step={step}
22 +
        value={value}
23 +
        onChange={(e) => onChange(Number(e.target.value))}
24 +
      />
25 +
    </div>
26 +
  );
27 +
}
src/components/SliderGroup.tsx (added) +40 −0
1 +
import type { BasicFilters } from "../lib/types";
2 +
import { Slider } from "./Slider";
3 +
4 +
interface SliderGroupProps {
5 +
  filters: BasicFilters;
6 +
  onChange: (key: keyof BasicFilters, value: number) => void;
7 +
}
8 +
9 +
const SLIDER_CONFIG: {
10 +
  key: keyof BasicFilters;
11 +
  label: string;
12 +
  min: number;
13 +
  max: number;
14 +
}[] = [
15 +
  { key: "brightness", label: "BRIGHTNESS", min: 0, max: 200 },
16 +
  { key: "contrast", label: "CONTRAST", min: 0, max: 200 },
17 +
  { key: "exposure", label: "EXPOSURE", min: -100, max: 100 },
18 +
  { key: "saturation", label: "SATURATION", min: 0, max: 200 },
19 +
  { key: "temperature", label: "TEMPERATURE", min: -100, max: 100 },
20 +
  { key: "tint", label: "TINT", min: -100, max: 100 },
21 +
  { key: "highlights", label: "HIGHLIGHTS", min: -100, max: 100 },
22 +
  { key: "shadows", label: "SHADOWS", min: -100, max: 100 },
23 +
];
24 +
25 +
export function SliderGroup({ filters, onChange }: SliderGroupProps) {
26 +
  return (
27 +
    <div className="slider-group">
28 +
      {SLIDER_CONFIG.map(({ key, label, min, max }) => (
29 +
        <Slider
30 +
          key={key}
31 +
          label={label}
32 +
          value={filters[key]}
33 +
          min={min}
34 +
          max={max}
35 +
          onChange={(v) => onChange(key, v)}
36 +
        />
37 +
      ))}
38 +
    </div>
39 +
  );
40 +
}
src/hooks/useAnimationLoop.ts (added) +18 −0
1 +
import { useEffect, useRef, useCallback } from "react";
2 +
3 +
export function useAnimationLoop(callback: () => void) {
4 +
  const callbackRef = useRef(callback);
5 +
  const rafRef = useRef<number>(0);
6 +
7 +
  callbackRef.current = callback;
8 +
9 +
  const loop = useCallback(() => {
10 +
    callbackRef.current();
11 +
    rafRef.current = requestAnimationFrame(loop);
12 +
  }, []);
13 +
14 +
  useEffect(() => {
15 +
    rafRef.current = requestAnimationFrame(loop);
16 +
    return () => cancelAnimationFrame(rafRef.current);
17 +
  }, [loop]);
18 +
}
src/hooks/useCamera.ts (added) +49 −0
1 +
import { useEffect, useRef, useState } from "react";
2 +
3 +
export function useCamera() {
4 +
  const videoRef = useRef<HTMLVideoElement>(null);
5 +
  const [error, setError] = useState<string | null>(null);
6 +
  const [isReady, setIsReady] = useState(false);
7 +
8 +
  useEffect(() => {
9 +
    let stream: MediaStream | null = null;
10 +
11 +
    async function init() {
12 +
      try {
13 +
        stream = await navigator.mediaDevices.getUserMedia({
14 +
          video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } },
15 +
          audio: false,
16 +
        });
17 +
        if (videoRef.current) {
18 +
          videoRef.current.srcObject = stream;
19 +
          videoRef.current.onloadedmetadata = () => {
20 +
            videoRef.current!.play();
21 +
            setIsReady(true);
22 +
          };
23 +
        }
24 +
      } catch (err) {
25 +
        if (err instanceof DOMException) {
26 +
          if (err.name === "NotAllowedError") {
27 +
            setError("Camera access denied. Please allow camera permissions.");
28 +
          } else if (err.name === "NotFoundError") {
29 +
            setError("No camera found. Please connect a camera.");
30 +
          } else {
31 +
            setError(`Camera error: ${err.message}`);
32 +
          }
33 +
        } else {
34 +
          setError("Failed to access camera.");
35 +
        }
36 +
      }
37 +
    }
38 +
39 +
    init();
40 +
41 +
    return () => {
42 +
      if (stream) {
43 +
        stream.getTracks().forEach((t) => t.stop());
44 +
      }
45 +
    };
46 +
  }, []);
47 +
48 +
  return { videoRef, error, isReady };
49 +
}
src/index.css (added) +46 −0
1 +
@font-face {
2 +
  font-family: "Commit Mono";
3 +
  src: url("/CommitMono-400-Regular.otf") format("opentype");
4 +
  font-weight: 400;
5 +
  font-style: normal;
6 +
}
7 +
8 +
@font-face {
9 +
  font-family: "Commit Mono";
10 +
  src: url("/CommitMono-700-Regular.otf") format("opentype");
11 +
  font-weight: 700;
12 +
  font-style: normal;
13 +
}
14 +
15 +
* {
16 +
  padding: 0;
17 +
  margin: 0;
18 +
  box-sizing: border-box;
19 +
  font-family: "Commit Mono", monospace, sans-serif;
20 +
  scrollbar-width: none;
21 +
  -ms-overflow-style: none;
22 +
}
23 +
24 +
html {
25 +
  background: #121113;
26 +
  color: #ffffff;
27 +
  font-size: 14px;
28 +
  line-height: 1.6;
29 +
}
30 +
31 +
html::-webkit-scrollbar {
32 +
  display: none;
33 +
}
34 +
35 +
body {
36 +
  margin: 0;
37 +
  overflow: hidden;
38 +
  width: 100vw;
39 +
  height: 100vh;
40 +
}
41 +
42 +
#root {
43 +
  width: 100%;
44 +
  height: 100%;
45 +
  position: relative;
46 +
}
src/lib/curves.ts (added) +112 −0
1 +
import type { CurvePoint } from "./types";
2 +
3 +
export function interpolateSpline(points: CurvePoint[]): Uint8Array {
4 +
  const lut = new Uint8Array(256);
5 +
6 +
  if (points.length < 2) {
7 +
    for (let i = 0; i < 256; i++) lut[i] = i;
8 +
    return lut;
9 +
  }
10 +
11 +
  const sorted = [...points].sort((a, b) => a.x - b.x);
12 +
  const n = sorted.length;
13 +
14 +
  if (n === 2) {
15 +
    const [p0, p1] = sorted;
16 +
    for (let i = 0; i < 256; i++) {
17 +
      if (i <= p0.x) {
18 +
        lut[i] = clamp(p0.y);
19 +
      } else if (i >= p1.x) {
20 +
        lut[i] = clamp(p1.y);
21 +
      } else {
22 +
        const t = (i - p0.x) / (p1.x - p0.x);
23 +
        lut[i] = clamp(p0.y + t * (p1.y - p0.y));
24 +
      }
25 +
    }
26 +
    return lut;
27 +
  }
28 +
29 +
  // Monotone cubic Hermite interpolation (Fritsch-Carlson)
30 +
  const xs = sorted.map((p) => p.x);
31 +
  const ys = sorted.map((p) => p.y);
32 +
  const dx: number[] = [];
33 +
  const dy: number[] = [];
34 +
  const m: number[] = [];
35 +
  const ms: number[] = [];
36 +
37 +
  for (let i = 0; i < n - 1; i++) {
38 +
    dx[i] = xs[i + 1] - xs[i];
39 +
    dy[i] = ys[i + 1] - ys[i];
40 +
    ms[i] = dx[i] === 0 ? 0 : dy[i] / dx[i];
41 +
  }
42 +
43 +
  m[0] = ms[0];
44 +
  for (let i = 1; i < n - 1; i++) {
45 +
    if (ms[i - 1] * ms[i] <= 0) {
46 +
      m[i] = 0;
47 +
    } else {
48 +
      m[i] = (ms[i - 1] + ms[i]) / 2;
49 +
    }
50 +
  }
51 +
  m[n - 1] = ms[n - 2];
52 +
53 +
  // Fritsch-Carlson monotonicity
54 +
  for (let i = 0; i < n - 1; i++) {
55 +
    if (ms[i] === 0) {
56 +
      m[i] = 0;
57 +
      m[i + 1] = 0;
58 +
    } else {
59 +
      const alpha = m[i] / ms[i];
60 +
      const beta = m[i + 1] / ms[i];
61 +
      const tau = alpha * alpha + beta * beta;
62 +
      if (tau > 9) {
63 +
        const s = 3 / Math.sqrt(tau);
64 +
        m[i] = s * alpha * ms[i];
65 +
        m[i + 1] = s * beta * ms[i];
66 +
      }
67 +
    }
68 +
  }
69 +
70 +
  for (let x = 0; x < 256; x++) {
71 +
    if (x <= xs[0]) {
72 +
      lut[x] = clamp(ys[0]);
73 +
      continue;
74 +
    }
75 +
    if (x >= xs[n - 1]) {
76 +
      lut[x] = clamp(ys[n - 1]);
77 +
      continue;
78 +
    }
79 +
80 +
    let seg = 0;
81 +
    for (let i = 0; i < n - 1; i++) {
82 +
      if (x >= xs[i] && x < xs[i + 1]) {
83 +
        seg = i;
84 +
        break;
85 +
      }
86 +
    }
87 +
88 +
    const h = dx[seg];
89 +
    const t = (x - xs[seg]) / h;
90 +
    const t2 = t * t;
91 +
    const t3 = t2 * t;
92 +
93 +
    const h00 = 2 * t3 - 3 * t2 + 1;
94 +
    const h10 = t3 - 2 * t2 + t;
95 +
    const h01 = -2 * t3 + 3 * t2;
96 +
    const h11 = t3 - t2;
97 +
98 +
    const val = h00 * ys[seg] + h10 * h * m[seg] + h01 * ys[seg + 1] + h11 * h * m[seg + 1];
99 +
    lut[x] = clamp(val);
100 +
  }
101 +
102 +
  return lut;
103 +
}
104 +
105 +
function clamp(v: number): number {
106 +
  return Math.max(0, Math.min(255, Math.round(v)));
107 +
}
108 +
109 +
export function isIdentityCurve(points: CurvePoint[]): boolean {
110 +
  if (points.length !== 2) return false;
111 +
  return points[0].x === 0 && points[0].y === 0 && points[1].x === 255 && points[1].y === 255;
112 +
}
src/lib/filters.ts (added) +110 −0
1 +
import type { BasicFilters, CurvesState } from "./types";
2 +
import { interpolateSpline, isIdentityCurve } from "./curves";
3 +
4 +
export function buildCSSFilter(basic: BasicFilters): string {
5 +
  const parts: string[] = [];
6 +
7 +
  // Brightness: slider 0-200, CSS brightness(0-2)
8 +
  parts.push(`brightness(${basic.brightness / 100})`);
9 +
10 +
  // Contrast: slider 0-200, CSS contrast(0-2)
11 +
  parts.push(`contrast(${basic.contrast / 100})`);
12 +
13 +
  // Saturation: slider 0-200, CSS saturate(0-2)
14 +
  parts.push(`saturate(${basic.saturation / 100})`);
15 +
16 +
  // Exposure: -100 to 100, maps to exponential brightness multiplier
17 +
  if (basic.exposure !== 0) {
18 +
    const exposureMult = Math.pow(2, basic.exposure / 100);
19 +
    parts.push(`brightness(${exposureMult})`);
20 +
  }
21 +
22 +
  // Temperature: -100 (cool) to 100 (warm)
23 +
  if (basic.temperature !== 0) {
24 +
    const absTemp = Math.abs(basic.temperature) / 100;
25 +
    const sepia = absTemp * 0.3;
26 +
    parts.push(`sepia(${sepia})`);
27 +
    if (basic.temperature < 0) {
28 +
      parts.push(`hue-rotate(180deg)`);
29 +
    }
30 +
    parts.push(`saturate(${1 + absTemp * 0.2})`);
31 +
  }
32 +
33 +
  // Tint: -100 to 100, maps to hue-rotate -30 to 30 degrees
34 +
  if (basic.tint !== 0) {
35 +
    const deg = (basic.tint / 100) * 30;
36 +
    parts.push(`hue-rotate(${deg}deg)`);
37 +
  }
38 +
39 +
  return parts.join(" ");
40 +
}
41 +
42 +
export interface LUTResult {
43 +
  r: Uint8Array;
44 +
  g: Uint8Array;
45 +
  b: Uint8Array;
46 +
  isIdentity: boolean;
47 +
}
48 +
49 +
export function buildLUT(basic: BasicFilters, curves: CurvesState): LUTResult {
50 +
  const curvesIdentity =
51 +
    isIdentityCurve(curves.rgb) &&
52 +
    isIdentityCurve(curves.r) &&
53 +
    isIdentityCurve(curves.g) &&
54 +
    isIdentityCurve(curves.b);
55 +
56 +
  const noHighlightsShadows = basic.highlights === 0 && basic.shadows === 0;
57 +
58 +
  if (curvesIdentity && noHighlightsShadows) {
59 +
    const identity = new Uint8Array(256);
60 +
    for (let i = 0; i < 256; i++) identity[i] = i;
61 +
    return { r: identity, g: identity, b: identity, isIdentity: true };
62 +
  }
63 +
64 +
  // Start with highlight/shadow adjustments
65 +
  const baseLut = new Uint8Array(256);
66 +
  for (let i = 0; i < 256; i++) {
67 +
    let val = i;
68 +
69 +
    // Shadows: gamma on lower half (values < 128)
70 +
    if (basic.shadows !== 0) {
71 +
      const shadowGamma = 1 - basic.shadows / 200;
72 +
      if (i < 128) {
73 +
        const t = i / 128;
74 +
        const adjusted = Math.pow(t, shadowGamma) * 128;
75 +
        val = adjusted + (val - i);
76 +
      }
77 +
    }
78 +
79 +
    // Highlights: gamma on upper half (values > 128)
80 +
    if (basic.highlights !== 0) {
81 +
      const highlightGamma = 1 - basic.highlights / 200;
82 +
      if (val > 128) {
83 +
        const t = (val - 128) / 127;
84 +
        val = 128 + Math.pow(t, highlightGamma) * 127;
85 +
      }
86 +
    }
87 +
88 +
    baseLut[i] = Math.max(0, Math.min(255, Math.round(val)));
89 +
  }
90 +
91 +
  // Apply curves on top
92 +
  const rgbLut = interpolateSpline(curves.rgb);
93 +
  const rCurve = interpolateSpline(curves.r);
94 +
  const gCurve = interpolateSpline(curves.g);
95 +
  const bCurve = interpolateSpline(curves.b);
96 +
97 +
  const r = new Uint8Array(256);
98 +
  const g = new Uint8Array(256);
99 +
  const b = new Uint8Array(256);
100 +
101 +
  for (let i = 0; i < 256; i++) {
102 +
    const base = baseLut[i];
103 +
    const afterRgb = rgbLut[base];
104 +
    r[i] = rCurve[afterRgb];
105 +
    g[i] = gCurve[afterRgb];
106 +
    b[i] = bCurve[afterRgb];
107 +
  }
108 +
109 +
  return { r, g, b, isIdentity: false };
110 +
}
src/lib/types.ts (added) +62 −0
1 +
export interface BasicFilters {
2 +
  brightness: number;
3 +
  contrast: number;
4 +
  exposure: number;
5 +
  saturation: number;
6 +
  temperature: number;
7 +
  tint: number;
8 +
  highlights: number;
9 +
  shadows: number;
10 +
}
11 +
12 +
export interface CurvePoint {
13 +
  x: number;
14 +
  y: number;
15 +
}
16 +
17 +
export type CurveChannel = "rgb" | "r" | "g" | "b";
18 +
19 +
export interface CurvesState {
20 +
  rgb: CurvePoint[];
21 +
  r: CurvePoint[];
22 +
  g: CurvePoint[];
23 +
  b: CurvePoint[];
24 +
}
25 +
26 +
export interface FilterState {
27 +
  basic: BasicFilters;
28 +
  curves: CurvesState;
29 +
}
30 +
31 +
export const DEFAULT_BASIC: BasicFilters = {
32 +
  brightness: 100,
33 +
  contrast: 100,
34 +
  exposure: 0,
35 +
  saturation: 100,
36 +
  temperature: 0,
37 +
  tint: 0,
38 +
  highlights: 0,
39 +
  shadows: 0,
40 +
};
41 +
42 +
export const DEFAULT_CURVE: CurvePoint[] = [
43 +
  { x: 0, y: 0 },
44 +
  { x: 255, y: 255 },
45 +
];
46 +
47 +
export const DEFAULT_CURVES: CurvesState = {
48 +
  rgb: [...DEFAULT_CURVE],
49 +
  r: [...DEFAULT_CURVE],
50 +
  g: [...DEFAULT_CURVE],
51 +
  b: [...DEFAULT_CURVE],
52 +
};
53 +
54 +
export const DEFAULT_FILTER_STATE: FilterState = {
55 +
  basic: { ...DEFAULT_BASIC },
56 +
  curves: {
57 +
    rgb: [...DEFAULT_CURVE],
58 +
    r: [...DEFAULT_CURVE],
59 +
    g: [...DEFAULT_CURVE],
60 +
    b: [...DEFAULT_CURVE],
61 +
  },
62 +
};
src/main.tsx (added) +10 −0
1 +
import { StrictMode } from 'react'
2 +
import { createRoot } from 'react-dom/client'
3 +
import './index.css'
4 +
import App from './App.tsx'
5 +
6 +
createRoot(document.getElementById('root')!).render(
7 +
  <StrictMode>
8 +
    <App />
9 +
  </StrictMode>,
10 +
)
tsconfig.app.json (added) +28 −0
1 +
{
2 +
  "compilerOptions": {
3 +
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 +
    "target": "ES2023",
5 +
    "useDefineForClassFields": true,
6 +
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
7 +
    "module": "ESNext",
8 +
    "types": ["vite/client"],
9 +
    "skipLibCheck": true,
10 +
11 +
    /* Bundler mode */
12 +
    "moduleResolution": "bundler",
13 +
    "allowImportingTsExtensions": true,
14 +
    "verbatimModuleSyntax": true,
15 +
    "moduleDetection": "force",
16 +
    "noEmit": true,
17 +
    "jsx": "react-jsx",
18 +
19 +
    /* Linting */
20 +
    "strict": true,
21 +
    "noUnusedLocals": true,
22 +
    "noUnusedParameters": true,
23 +
    "erasableSyntaxOnly": true,
24 +
    "noFallthroughCasesInSwitch": true,
25 +
    "noUncheckedSideEffectImports": true
26 +
  },
27 +
  "include": ["src"]
28 +
}
tsconfig.json (added) +7 −0
1 +
{
2 +
  "files": [],
3 +
  "references": [
4 +
    { "path": "./tsconfig.app.json" },
5 +
    { "path": "./tsconfig.node.json" }
6 +
  ]
7 +
}
tsconfig.node.json (added) +26 −0
1 +
{
2 +
  "compilerOptions": {
3 +
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 +
    "target": "ES2023",
5 +
    "lib": ["ES2023"],
6 +
    "module": "ESNext",
7 +
    "types": ["node"],
8 +
    "skipLibCheck": true,
9 +
10 +
    /* Bundler mode */
11 +
    "moduleResolution": "bundler",
12 +
    "allowImportingTsExtensions": true,
13 +
    "verbatimModuleSyntax": true,
14 +
    "moduleDetection": "force",
15 +
    "noEmit": true,
16 +
17 +
    /* Linting */
18 +
    "strict": true,
19 +
    "noUnusedLocals": true,
20 +
    "noUnusedParameters": true,
21 +
    "erasableSyntaxOnly": true,
22 +
    "noFallthroughCasesInSwitch": true,
23 +
    "noUncheckedSideEffectImports": true
24 +
  },
25 +
  "include": ["vite.config.ts"]
26 +
}
vite.config.ts (added) +7 −0
1 +
import { defineConfig } from 'vite'
2 +
import react from '@vitejs/plugin-react'
3 +
4 +
// https://vite.dev/config/
5 +
export default defineConfig({
6 +
  plugins: [react()],
7 +
})