wip
@@ -1,3 +1,3 @@
 | 
			
		||||
# Ignore artifacts:
 | 
			
		||||
dist
 | 
			
		||||
node_modules
 | 
			
		||||
build
 | 
			
		||||
coverage
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
FROM oven/bun:1.1.45-alpine
 | 
			
		||||
 | 
			
		||||
WORKDIR /frontend
 | 
			
		||||
 | 
			
		||||
COPY ./frontend/package.json ./
 | 
			
		||||
COPY ./frontend/bun.lockb ./
 | 
			
		||||
 | 
			
		||||
RUN bun install
 | 
			
		||||
 | 
			
		||||
COPY ./frontend/public ./public
 | 
			
		||||
COPY ./frontend/src ./src
 | 
			
		||||
 | 
			
		||||
COPY ./frontend/eslint.config.js ./
 | 
			
		||||
COPY ./frontend/index.html ./
 | 
			
		||||
COPY ./frontend/tsconfig.json ./
 | 
			
		||||
COPY ./frontend/tsconfig.app.json ./
 | 
			
		||||
COPY ./frontend/tsconfig.node.json ./
 | 
			
		||||
COPY ./frontend/vite.config.ts ./
 | 
			
		||||
COPY ./frontend/postcss.config.cjs ./
 | 
			
		||||
 | 
			
		||||
EXPOSE 5173
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["bun", "run", "dev"]
 | 
			
		||||
							
								
								
									
										54
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,54 @@
 | 
			
		||||
# React + TypeScript + Vite
 | 
			
		||||
 | 
			
		||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
 | 
			
		||||
 | 
			
		||||
Currently, two official plugins are available:
 | 
			
		||||
 | 
			
		||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
 | 
			
		||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
 | 
			
		||||
 | 
			
		||||
## Expanding the ESLint configuration
 | 
			
		||||
 | 
			
		||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
export default tseslint.config({
 | 
			
		||||
  extends: [
 | 
			
		||||
    // Remove ...tseslint.configs.recommended and replace with this
 | 
			
		||||
    ...tseslint.configs.recommendedTypeChecked,
 | 
			
		||||
    // Alternatively, use this for stricter rules
 | 
			
		||||
    ...tseslint.configs.strictTypeChecked,
 | 
			
		||||
    // Optionally, add this for stylistic rules
 | 
			
		||||
    ...tseslint.configs.stylisticTypeChecked,
 | 
			
		||||
  ],
 | 
			
		||||
  languageOptions: {
 | 
			
		||||
    // other options...
 | 
			
		||||
    parserOptions: {
 | 
			
		||||
      project: ['./tsconfig.node.json', './tsconfig.app.json'],
 | 
			
		||||
      tsconfigRootDir: import.meta.dirname,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
// eslint.config.js
 | 
			
		||||
import reactX from 'eslint-plugin-react-x'
 | 
			
		||||
import reactDom from 'eslint-plugin-react-dom'
 | 
			
		||||
 | 
			
		||||
export default tseslint.config({
 | 
			
		||||
  plugins: {
 | 
			
		||||
    // Add the react-x and react-dom plugins
 | 
			
		||||
    'react-x': reactX,
 | 
			
		||||
    'react-dom': reactDom,
 | 
			
		||||
  },
 | 
			
		||||
  rules: {
 | 
			
		||||
    // other rules...
 | 
			
		||||
    // Enable its recommended typescript rules
 | 
			
		||||
    ...reactX.configs['recommended-typescript'].rules,
 | 
			
		||||
    ...reactDom.configs.recommended.rules,
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										727
									
								
								frontend/bun.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,727 @@
 | 
			
		||||
{
 | 
			
		||||
  "lockfileVersion": 1,
 | 
			
		||||
  "workspaces": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "tinyauth-shadcn",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@radix-ui/react-label": "^2.1.4",
 | 
			
		||||
        "@radix-ui/react-select": "^2.2.2",
 | 
			
		||||
        "@radix-ui/react-separator": "^1.1.4",
 | 
			
		||||
        "@radix-ui/react-slot": "^1.2.0",
 | 
			
		||||
        "@tailwindcss/vite": "^4.1.4",
 | 
			
		||||
        "class-variance-authority": "^0.7.1",
 | 
			
		||||
        "clsx": "^2.1.1",
 | 
			
		||||
        "i18next": "^25.0.2",
 | 
			
		||||
        "i18next-browser-languagedetector": "^8.0.5",
 | 
			
		||||
        "i18next-resources-to-backend": "^1.2.1",
 | 
			
		||||
        "input-otp": "^1.4.2",
 | 
			
		||||
        "lucide-react": "^0.503.0",
 | 
			
		||||
        "react": "^19.0.0",
 | 
			
		||||
        "react-dom": "^19.0.0",
 | 
			
		||||
        "react-i18next": "^15.5.1",
 | 
			
		||||
        "react-router": "^7.5.3",
 | 
			
		||||
        "tailwind-merge": "^3.2.0",
 | 
			
		||||
        "tailwindcss": "^4.1.4",
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@eslint/js": "^9.22.0",
 | 
			
		||||
        "@types/node": "^22.15.3",
 | 
			
		||||
        "@types/react": "^19.0.10",
 | 
			
		||||
        "@types/react-dom": "^19.0.4",
 | 
			
		||||
        "@vitejs/plugin-react": "^4.3.4",
 | 
			
		||||
        "eslint": "^9.22.0",
 | 
			
		||||
        "eslint-plugin-react-hooks": "^5.2.0",
 | 
			
		||||
        "eslint-plugin-react-refresh": "^0.4.19",
 | 
			
		||||
        "globals": "^16.0.0",
 | 
			
		||||
        "prettier": "3.5.3",
 | 
			
		||||
        "tw-animate-css": "^1.2.8",
 | 
			
		||||
        "typescript": "~5.7.2",
 | 
			
		||||
        "typescript-eslint": "^8.26.1",
 | 
			
		||||
        "vite": "^6.3.1",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "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-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helpers": ["@babel/helpers@7.27.0", "", { "dependencies": { "@babel/template": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/runtime": ["@babel/runtime@7.27.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/android-arm": ["@esbuild/android-arm@0.25.3", "", { "os": "android", "cpu": "arm" }, "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.3", "", { "os": "android", "cpu": "arm64" }, "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/android-x64": ["@esbuild/android-x64@0.25.3", "", { "os": "android", "cpu": "x64" }, "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.3", "", { "os": "linux", "cpu": "arm" }, "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.3", "", { "os": "linux", "cpu": "none" }, "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.3", "", { "os": "linux", "cpu": "x64" }, "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.3", "", { "os": "none", "cpu": "arm64" }, "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.3", "", { "os": "none", "cpu": "x64" }, "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.6.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "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.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/js": ["@eslint/js@9.25.1", "", {}, "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.8", "", { "dependencies": { "@eslint/core": "^0.13.0", "levn": "^0.4.1" } }, "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA=="],
 | 
			
		||||
 | 
			
		||||
    "@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="],
 | 
			
		||||
 | 
			
		||||
    "@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="],
 | 
			
		||||
 | 
			
		||||
    "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="],
 | 
			
		||||
 | 
			
		||||
    "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-label": ["@radix-ui/react-label@2.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wy3dqizZnZVV4ja0FNnUhIWNwWdoldXrneEyUcVtLYDAt8ovGS4ridtMAOGgXBBIfggL4BOveVWsjXDORdGEQg=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.4", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-select": ["@radix-ui/react-select@2.2.2", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.4", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.4", "@radix-ui/react-portal": "1.1.6", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HjkVHtBkuq+r3zUAZ/CvNWUGKPfuicGDbgtZgiQuFmNcV5F+Tgy24ep2nsAW2nFgvhGPJVqeBZa6KyVN0EyrBA=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2fTm6PSiUm8YPq9W0E4reYuv01EE3aFSzt8edBiXqPHshF8N9+Kymt/k0/R+F3dkY5lQyB/zPtrP82phskLi7w=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.0", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg=="],
 | 
			
		||||
 | 
			
		||||
    "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.1", "", { "os": "android", "cpu": "arm" }, "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.1", "", { "os": "android", "cpu": "arm64" }, "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.1", "", { "os": "linux", "cpu": "arm" }, "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.1", "", { "os": "linux", "cpu": "none" }, "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.1", "", { "os": "linux", "cpu": "none" }, "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.1", "", { "os": "linux", "cpu": "none" }, "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.1", "", { "os": "linux", "cpu": "x64" }, "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/node": ["@tailwindcss/node@4.1.4", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.4" } }, "sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.4", "@tailwindcss/oxide-darwin-arm64": "4.1.4", "@tailwindcss/oxide-darwin-x64": "4.1.4", "@tailwindcss/oxide-freebsd-x64": "4.1.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.4", "@tailwindcss/oxide-linux-arm64-musl": "4.1.4", "@tailwindcss/oxide-linux-x64-gnu": "4.1.4", "@tailwindcss/oxide-linux-x64-musl": "4.1.4", "@tailwindcss/oxide-wasm32-wasi": "4.1.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.4", "@tailwindcss/oxide-win32-x64-msvc": "4.1.4" } }, "sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.4", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@emnapi/wasi-threads": "^1.0.1", "@napi-rs/wasm-runtime": "^0.2.8", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/vite": ["@tailwindcss/vite@4.1.4", "", { "dependencies": { "@tailwindcss/node": "4.1.4", "@tailwindcss/oxide": "4.1.4", "tailwindcss": "4.1.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A=="],
 | 
			
		||||
 | 
			
		||||
    "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
 | 
			
		||||
 | 
			
		||||
    "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
 | 
			
		||||
 | 
			
		||||
    "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
 | 
			
		||||
 | 
			
		||||
    "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
 | 
			
		||||
 | 
			
		||||
    "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/node": ["@types/node@22.15.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw=="],
 | 
			
		||||
 | 
			
		||||
    "@types/react": ["@types/react@19.1.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw=="],
 | 
			
		||||
 | 
			
		||||
    "@types/react-dom": ["@types/react-dom@19.1.2", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.31.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.31.1", "@typescript-eslint/type-utils": "8.31.1", "@typescript-eslint/utils": "8.31.1", "@typescript-eslint/visitor-keys": "8.31.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.31.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.31.1", "@typescript-eslint/types": "8.31.1", "@typescript-eslint/typescript-estree": "8.31.1", "@typescript-eslint/visitor-keys": "8.31.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.31.1", "", { "dependencies": { "@typescript-eslint/types": "8.31.1", "@typescript-eslint/visitor-keys": "8.31.1" } }, "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.31.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.31.1", "@typescript-eslint/utils": "8.31.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/types": ["@typescript-eslint/types@8.31.1", "", {}, "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.31.1", "", { "dependencies": { "@typescript-eslint/types": "8.31.1", "@typescript-eslint/visitor-keys": "8.31.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.31.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.31.1", "@typescript-eslint/types": "8.31.1", "@typescript-eslint/typescript-estree": "8.31.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.31.1", "", { "dependencies": { "@typescript-eslint/types": "8.31.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw=="],
 | 
			
		||||
 | 
			
		||||
    "@vitejs/plugin-react": ["@vitejs/plugin-react@4.4.1", "", { "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w=="],
 | 
			
		||||
 | 
			
		||||
    "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
 | 
			
		||||
 | 
			
		||||
    "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
 | 
			
		||||
 | 
			
		||||
    "ajv": ["ajv@6.12.6", "", { "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-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
 | 
			
		||||
 | 
			
		||||
    "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
 | 
			
		||||
    "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
 | 
			
		||||
 | 
			
		||||
    "aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="],
 | 
			
		||||
 | 
			
		||||
    "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 | 
			
		||||
 | 
			
		||||
    "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
 | 
			
		||||
 | 
			
		||||
    "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
 | 
			
		||||
 | 
			
		||||
    "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="],
 | 
			
		||||
 | 
			
		||||
    "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
 | 
			
		||||
 | 
			
		||||
    "caniuse-lite": ["caniuse-lite@1.0.30001715", "", {}, "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw=="],
 | 
			
		||||
 | 
			
		||||
    "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 | 
			
		||||
 | 
			
		||||
    "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
 | 
			
		||||
 | 
			
		||||
    "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
 | 
			
		||||
 | 
			
		||||
    "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
 | 
			
		||||
 | 
			
		||||
    "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
 | 
			
		||||
 | 
			
		||||
    "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
 | 
			
		||||
 | 
			
		||||
    "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
 | 
			
		||||
 | 
			
		||||
    "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
 | 
			
		||||
 | 
			
		||||
    "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
 | 
			
		||||
 | 
			
		||||
    "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
 | 
			
		||||
 | 
			
		||||
    "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
 | 
			
		||||
 | 
			
		||||
    "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
 | 
			
		||||
 | 
			
		||||
    "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
 | 
			
		||||
 | 
			
		||||
    "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
 | 
			
		||||
 | 
			
		||||
    "electron-to-chromium": ["electron-to-chromium@1.5.144", "", {}, "sha512-eJIaMRKeAzxfBSxtjYnoIAw/tdD6VIH6tHBZepZnAbE3Gyqqs5mGN87DvcldPUbVkIljTK8pY0CMcUljP64lfQ=="],
 | 
			
		||||
 | 
			
		||||
    "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
 | 
			
		||||
 | 
			
		||||
    "esbuild": ["esbuild@0.25.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.3", "@esbuild/android-arm": "0.25.3", "@esbuild/android-arm64": "0.25.3", "@esbuild/android-x64": "0.25.3", "@esbuild/darwin-arm64": "0.25.3", "@esbuild/darwin-x64": "0.25.3", "@esbuild/freebsd-arm64": "0.25.3", "@esbuild/freebsd-x64": "0.25.3", "@esbuild/linux-arm": "0.25.3", "@esbuild/linux-arm64": "0.25.3", "@esbuild/linux-ia32": "0.25.3", "@esbuild/linux-loong64": "0.25.3", "@esbuild/linux-mips64el": "0.25.3", "@esbuild/linux-ppc64": "0.25.3", "@esbuild/linux-riscv64": "0.25.3", "@esbuild/linux-s390x": "0.25.3", "@esbuild/linux-x64": "0.25.3", "@esbuild/netbsd-arm64": "0.25.3", "@esbuild/netbsd-x64": "0.25.3", "@esbuild/openbsd-arm64": "0.25.3", "@esbuild/openbsd-x64": "0.25.3", "@esbuild/sunos-x64": "0.25.3", "@esbuild/win32-arm64": "0.25.3", "@esbuild/win32-ia32": "0.25.3", "@esbuild/win32-x64": "0.25.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q=="],
 | 
			
		||||
 | 
			
		||||
    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
 | 
			
		||||
 | 
			
		||||
    "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint": ["eslint@9.25.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.25.1", "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.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.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.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-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
 | 
			
		||||
 | 
			
		||||
    "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
 | 
			
		||||
 | 
			
		||||
    "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
 | 
			
		||||
 | 
			
		||||
    "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
 | 
			
		||||
 | 
			
		||||
    "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
 | 
			
		||||
 | 
			
		||||
    "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
 | 
			
		||||
 | 
			
		||||
    "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
 | 
			
		||||
 | 
			
		||||
    "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
 | 
			
		||||
 | 
			
		||||
    "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
 | 
			
		||||
 | 
			
		||||
    "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
 | 
			
		||||
 | 
			
		||||
    "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
 | 
			
		||||
 | 
			
		||||
    "fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
 | 
			
		||||
 | 
			
		||||
    "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
 | 
			
		||||
 | 
			
		||||
    "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
 | 
			
		||||
 | 
			
		||||
    "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
 | 
			
		||||
 | 
			
		||||
    "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
 | 
			
		||||
 | 
			
		||||
    "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
 | 
			
		||||
 | 
			
		||||
    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
 | 
			
		||||
 | 
			
		||||
    "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
 | 
			
		||||
 | 
			
		||||
    "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
 | 
			
		||||
 | 
			
		||||
    "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
 | 
			
		||||
 | 
			
		||||
    "globals": ["globals@16.0.0", "", {}, "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A=="],
 | 
			
		||||
 | 
			
		||||
    "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
 | 
			
		||||
 | 
			
		||||
    "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
 | 
			
		||||
 | 
			
		||||
    "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
 | 
			
		||||
 | 
			
		||||
    "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
 | 
			
		||||
 | 
			
		||||
    "i18next": ["i18next@25.0.2", "", { "dependencies": { "@babel/runtime": "^7.26.10" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-xWxgK8GAaPYkV9ia2tdgbtdM+qiC+ysVTBPvXhpCORU/+QkeQe3BSI7Crr+c4ZXULN1PfnXG/HY2n7HGx4KKBg=="],
 | 
			
		||||
 | 
			
		||||
    "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.0.5", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-OstebRKqKiQw8xEvQF5aRyUujsCatanj7Q9eo5iiH2gJpoXGZ7483ol3sVBwfqbobTQPNH1J+NAyJ1aCQoEC+w=="],
 | 
			
		||||
 | 
			
		||||
    "i18next-resources-to-backend": ["i18next-resources-to-backend@1.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw=="],
 | 
			
		||||
 | 
			
		||||
    "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
 | 
			
		||||
 | 
			
		||||
    "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
 | 
			
		||||
 | 
			
		||||
    "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
 | 
			
		||||
 | 
			
		||||
    "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
 | 
			
		||||
 | 
			
		||||
    "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
 | 
			
		||||
 | 
			
		||||
    "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
 | 
			
		||||
 | 
			
		||||
    "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
 | 
			
		||||
 | 
			
		||||
    "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
 | 
			
		||||
 | 
			
		||||
    "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
 | 
			
		||||
 | 
			
		||||
    "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
 | 
			
		||||
 | 
			
		||||
    "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
 | 
			
		||||
 | 
			
		||||
    "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
 | 
			
		||||
 | 
			
		||||
    "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
 | 
			
		||||
 | 
			
		||||
    "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
 | 
			
		||||
 | 
			
		||||
    "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
 | 
			
		||||
 | 
			
		||||
    "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
 | 
			
		||||
 | 
			
		||||
    "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
 | 
			
		||||
 | 
			
		||||
    "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.2", "", { "os": "linux", "cpu": "arm" }, "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="],
 | 
			
		||||
 | 
			
		||||
    "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
 | 
			
		||||
 | 
			
		||||
    "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
 | 
			
		||||
 | 
			
		||||
    "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
 | 
			
		||||
 | 
			
		||||
    "lucide-react": ["lucide-react@0.503.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w=="],
 | 
			
		||||
 | 
			
		||||
    "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
 | 
			
		||||
 | 
			
		||||
    "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
 | 
			
		||||
 | 
			
		||||
    "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
 | 
			
		||||
 | 
			
		||||
    "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
 | 
			
		||||
 | 
			
		||||
    "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
 | 
			
		||||
 | 
			
		||||
    "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
 | 
			
		||||
 | 
			
		||||
    "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
 | 
			
		||||
 | 
			
		||||
    "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=="],
 | 
			
		||||
 | 
			
		||||
    "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
 | 
			
		||||
 | 
			
		||||
    "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
 | 
			
		||||
 | 
			
		||||
    "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
 | 
			
		||||
 | 
			
		||||
    "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
 | 
			
		||||
 | 
			
		||||
    "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
 | 
			
		||||
 | 
			
		||||
    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
 | 
			
		||||
 | 
			
		||||
    "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
 | 
			
		||||
 | 
			
		||||
    "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
 | 
			
		||||
 | 
			
		||||
    "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
 | 
			
		||||
 | 
			
		||||
    "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
 | 
			
		||||
 | 
			
		||||
    "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
 | 
			
		||||
 | 
			
		||||
    "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
 | 
			
		||||
 | 
			
		||||
    "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
 | 
			
		||||
 | 
			
		||||
    "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
 | 
			
		||||
 | 
			
		||||
    "react-i18next": ["react-i18next@15.5.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA=="],
 | 
			
		||||
 | 
			
		||||
    "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
 | 
			
		||||
 | 
			
		||||
    "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
 | 
			
		||||
 | 
			
		||||
    "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
 | 
			
		||||
 | 
			
		||||
    "react-router": ["react-router@7.5.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw=="],
 | 
			
		||||
 | 
			
		||||
    "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
 | 
			
		||||
 | 
			
		||||
    "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
 | 
			
		||||
 | 
			
		||||
    "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
 | 
			
		||||
 | 
			
		||||
    "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
 | 
			
		||||
 | 
			
		||||
    "rollup": ["rollup@4.40.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.1", "@rollup/rollup-android-arm64": "4.40.1", "@rollup/rollup-darwin-arm64": "4.40.1", "@rollup/rollup-darwin-x64": "4.40.1", "@rollup/rollup-freebsd-arm64": "4.40.1", "@rollup/rollup-freebsd-x64": "4.40.1", "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", "@rollup/rollup-linux-arm-musleabihf": "4.40.1", "@rollup/rollup-linux-arm64-gnu": "4.40.1", "@rollup/rollup-linux-arm64-musl": "4.40.1", "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", "@rollup/rollup-linux-riscv64-gnu": "4.40.1", "@rollup/rollup-linux-riscv64-musl": "4.40.1", "@rollup/rollup-linux-s390x-gnu": "4.40.1", "@rollup/rollup-linux-x64-gnu": "4.40.1", "@rollup/rollup-linux-x64-musl": "4.40.1", "@rollup/rollup-win32-arm64-msvc": "4.40.1", "@rollup/rollup-win32-ia32-msvc": "4.40.1", "@rollup/rollup-win32-x64-msvc": "4.40.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw=="],
 | 
			
		||||
 | 
			
		||||
    "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
 | 
			
		||||
 | 
			
		||||
    "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
 | 
			
		||||
 | 
			
		||||
    "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 | 
			
		||||
 | 
			
		||||
    "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
 | 
			
		||||
 | 
			
		||||
    "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
 | 
			
		||||
 | 
			
		||||
    "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
 | 
			
		||||
 | 
			
		||||
    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
 | 
			
		||||
 | 
			
		||||
    "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
 | 
			
		||||
 | 
			
		||||
    "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
 | 
			
		||||
 | 
			
		||||
    "tailwind-merge": ["tailwind-merge@3.2.0", "", {}, "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA=="],
 | 
			
		||||
 | 
			
		||||
    "tailwindcss": ["tailwindcss@4.1.4", "", {}, "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A=="],
 | 
			
		||||
 | 
			
		||||
    "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
 | 
			
		||||
 | 
			
		||||
    "tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
 | 
			
		||||
 | 
			
		||||
    "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
 | 
			
		||||
 | 
			
		||||
    "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
 | 
			
		||||
 | 
			
		||||
    "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="],
 | 
			
		||||
 | 
			
		||||
    "tw-animate-css": ["tw-animate-css@1.2.8", "", {}, "sha512-AxSnYRvyFnAiZCUndS3zQZhNfV/B77ZhJ+O7d3K6wfg/jKJY+yv6ahuyXwnyaYA9UdLqnpCwhTRv9pPTBnPR2g=="],
 | 
			
		||||
 | 
			
		||||
    "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
 | 
			
		||||
 | 
			
		||||
    "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
 | 
			
		||||
 | 
			
		||||
    "typescript-eslint": ["typescript-eslint@8.31.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", "@typescript-eslint/utils": "8.31.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA=="],
 | 
			
		||||
 | 
			
		||||
    "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
 | 
			
		||||
 | 
			
		||||
    "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
 | 
			
		||||
 | 
			
		||||
    "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
 | 
			
		||||
 | 
			
		||||
    "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
 | 
			
		||||
 | 
			
		||||
    "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
 | 
			
		||||
 | 
			
		||||
    "vite": ["vite@6.3.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw=="],
 | 
			
		||||
 | 
			
		||||
    "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
 | 
			
		||||
 | 
			
		||||
    "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
 | 
			
		||||
 | 
			
		||||
    "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
 | 
			
		||||
 | 
			
		||||
    "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
 | 
			
		||||
 | 
			
		||||
    "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.9", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
 | 
			
		||||
 | 
			
		||||
    "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 | 
			
		||||
 | 
			
		||||
    "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								frontend/components.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://ui.shadcn.com/schema.json",
 | 
			
		||||
  "style": "new-york",
 | 
			
		||||
  "rsc": false,
 | 
			
		||||
  "tsx": true,
 | 
			
		||||
  "tailwind": {
 | 
			
		||||
    "config": "",
 | 
			
		||||
    "css": "src/index.css",
 | 
			
		||||
    "baseColor": "neutral",
 | 
			
		||||
    "cssVariables": true,
 | 
			
		||||
    "prefix": ""
 | 
			
		||||
  },
 | 
			
		||||
  "aliases": {
 | 
			
		||||
    "components": "@/components",
 | 
			
		||||
    "utils": "@/lib/utils",
 | 
			
		||||
    "ui": "@/components/ui",
 | 
			
		||||
    "lib": "@/lib",
 | 
			
		||||
    "hooks": "@/hooks"
 | 
			
		||||
  },
 | 
			
		||||
  "iconLibrary": "lucide"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,28 +1,28 @@
 | 
			
		||||
import js from "@eslint/js";
 | 
			
		||||
import globals from "globals";
 | 
			
		||||
import reactHooks from "eslint-plugin-react-hooks";
 | 
			
		||||
import reactRefresh from "eslint-plugin-react-refresh";
 | 
			
		||||
import tseslint from "typescript-eslint";
 | 
			
		||||
import js from '@eslint/js'
 | 
			
		||||
import globals from 'globals'
 | 
			
		||||
import reactHooks from 'eslint-plugin-react-hooks'
 | 
			
		||||
import reactRefresh from 'eslint-plugin-react-refresh'
 | 
			
		||||
import tseslint from 'typescript-eslint'
 | 
			
		||||
 | 
			
		||||
export default tseslint.config(
 | 
			
		||||
  { ignores: ["dist"] },
 | 
			
		||||
  { ignores: ['dist'] },
 | 
			
		||||
  {
 | 
			
		||||
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
 | 
			
		||||
    files: ["**/*.{ts,tsx}"],
 | 
			
		||||
    files: ['**/*.{ts,tsx}'],
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      ecmaVersion: 2020,
 | 
			
		||||
      globals: globals.browser,
 | 
			
		||||
    },
 | 
			
		||||
    plugins: {
 | 
			
		||||
      "react-hooks": reactHooks,
 | 
			
		||||
      "react-refresh": reactRefresh,
 | 
			
		||||
      'react-hooks': reactHooks,
 | 
			
		||||
      'react-refresh': reactRefresh,
 | 
			
		||||
    },
 | 
			
		||||
    rules: {
 | 
			
		||||
      ...reactHooks.configs.recommended.rules,
 | 
			
		||||
      "react-refresh/only-export-components": [
 | 
			
		||||
        "warn",
 | 
			
		||||
      'react-refresh/only-export-components': [
 | 
			
		||||
        'warn',
 | 
			
		||||
        { allowConstantExport: true },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,9 @@
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
 | 
			
		||||
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
 | 
			
		||||
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
 | 
			
		||||
    <link rel="manifest" href="/frontend.webmanifest" />
 | 
			
		||||
    <title>Tinyauth</title>
 | 
			
		||||
    <title>Tinyauth (Shadcn)</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
  <body class="dark">
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <script type="module" src="/src/main.tsx"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3907
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "frontend",
 | 
			
		||||
  "name": "tinyauth-shadcn",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
@@ -10,39 +10,39 @@
 | 
			
		||||
    "preview": "vite preview"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@mantine/core": "^7.16.0",
 | 
			
		||||
    "@mantine/form": "^7.16.0",
 | 
			
		||||
    "@mantine/hooks": "^7.16.0",
 | 
			
		||||
    "@mantine/notifications": "^7.16.0",
 | 
			
		||||
    "@tanstack/react-query": "5",
 | 
			
		||||
    "axios": "^1.7.9",
 | 
			
		||||
    "i18next": "^25.0.0",
 | 
			
		||||
    "i18next-browser-languagedetector": "^8.0.4",
 | 
			
		||||
    "i18next-chained-backend": "^4.6.2",
 | 
			
		||||
    "i18next-http-backend": "^3.0.2",
 | 
			
		||||
    "@radix-ui/react-label": "^2.1.4",
 | 
			
		||||
    "@radix-ui/react-select": "^2.2.2",
 | 
			
		||||
    "@radix-ui/react-separator": "^1.1.4",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.2.0",
 | 
			
		||||
    "@tailwindcss/vite": "^4.1.4",
 | 
			
		||||
    "class-variance-authority": "^0.7.1",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "i18next": "^25.0.2",
 | 
			
		||||
    "i18next-browser-languagedetector": "^8.0.5",
 | 
			
		||||
    "i18next-resources-to-backend": "^1.2.1",
 | 
			
		||||
    "react": "^19.1.0",
 | 
			
		||||
    "react-dom": "^19.1.0",
 | 
			
		||||
    "react-i18next": "^15.4.1",
 | 
			
		||||
    "react-markdown": "^10.1.0",
 | 
			
		||||
    "react-router": "^7.5.2",
 | 
			
		||||
    "zod": "^3.24.1"
 | 
			
		||||
    "input-otp": "^1.4.2",
 | 
			
		||||
    "lucide-react": "^0.503.0",
 | 
			
		||||
    "react": "^19.0.0",
 | 
			
		||||
    "react-dom": "^19.0.0",
 | 
			
		||||
    "react-i18next": "^15.5.1",
 | 
			
		||||
    "react-router": "^7.5.3",
 | 
			
		||||
    "tailwind-merge": "^3.2.0",
 | 
			
		||||
    "tailwindcss": "^4.1.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@eslint/js": "^9.17.0",
 | 
			
		||||
    "@types/react": "^19.1.1",
 | 
			
		||||
    "@types/react-dom": "^19.1.2",
 | 
			
		||||
    "@vitejs/plugin-react-swc": "^3.5.0",
 | 
			
		||||
    "eslint": "^9.17.0",
 | 
			
		||||
    "eslint-plugin-react-hooks": "^5.0.0",
 | 
			
		||||
    "eslint-plugin-react-refresh": "^0.4.16",
 | 
			
		||||
    "@eslint/js": "^9.22.0",
 | 
			
		||||
    "@types/node": "^22.15.3",
 | 
			
		||||
    "@types/react": "^19.0.10",
 | 
			
		||||
    "@types/react-dom": "^19.0.4",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.3.4",
 | 
			
		||||
    "eslint": "^9.22.0",
 | 
			
		||||
    "eslint-plugin-react-hooks": "^5.2.0",
 | 
			
		||||
    "eslint-plugin-react-refresh": "^0.4.19",
 | 
			
		||||
    "globals": "^16.0.0",
 | 
			
		||||
    "postcss": "^8.5.1",
 | 
			
		||||
    "postcss-preset-mantine": "^1.17.0",
 | 
			
		||||
    "postcss-simple-vars": "^7.0.1",
 | 
			
		||||
    "prettier": "3.5.3",
 | 
			
		||||
    "typescript": "~5.8.3",
 | 
			
		||||
    "typescript-eslint": "^8.18.2",
 | 
			
		||||
    "vite": "^6.0.5"
 | 
			
		||||
    "tw-animate-css": "^1.2.8",
 | 
			
		||||
    "typescript": "~5.7.2",
 | 
			
		||||
    "typescript-eslint": "^8.26.1",
 | 
			
		||||
    "vite": "^6.3.1"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    "postcss-preset-mantine": {},
 | 
			
		||||
    "postcss-simple-vars": {
 | 
			
		||||
      variables: {
 | 
			
		||||
        "mantine-breakpoint-xs": "36em",
 | 
			
		||||
        "mantine-breakpoint-sm": "48em",
 | 
			
		||||
        "mantine-breakpoint-md": "62em",
 | 
			
		||||
        "mantine-breakpoint-lg": "75em",
 | 
			
		||||
        "mantine-breakpoint-xl": "88em",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 15 KiB  | 
| 
		 Before Width: | Height: | Size: 46 KiB  | 
| 
		 Before Width: | Height: | Size: 13 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								frontend/public/background.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 588 KiB  | 
| 
		 Before Width: | Height: | Size: 602 B  | 
| 
		 Before Width: | Height: | Size: 1.4 KiB  | 
| 
		 Before Width: | Height: | Size: 15 KiB  | 
| 
		 Before Width: | Height: | Size: 35 KiB  | 
@@ -1 +0,0 @@
 | 
			
		||||
{"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"}
 | 
			
		||||
@@ -1,17 +1,5 @@
 | 
			
		||||
import { Navigate } from "react-router";
 | 
			
		||||
import { useUserContext } from "./context/user-context";
 | 
			
		||||
import { LogoutPage } from "./pages/logout-page";
 | 
			
		||||
 | 
			
		||||
export const App = () => {
 | 
			
		||||
  const queryString = window.location.search;
 | 
			
		||||
  const params = new URLSearchParams(queryString);
 | 
			
		||||
  const redirectUri = params.get("redirect_uri");
 | 
			
		||||
 | 
			
		||||
  const { isLoggedIn } = useUserContext();
 | 
			
		||||
 | 
			
		||||
  if (!isLoggedIn) {
 | 
			
		||||
    return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <LogoutPage />;
 | 
			
		||||
  return <Navigate to="/login" />;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,57 +0,0 @@
 | 
			
		||||
import { TextInput, PasswordInput, Button, Anchor, Group, Text } from "@mantine/core";
 | 
			
		||||
import { useForm, zodResolver } from "@mantine/form";
 | 
			
		||||
import { LoginFormValues, loginSchema } from "../../schemas/login-schema";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
interface LoginFormProps {
 | 
			
		||||
  isPending: boolean;
 | 
			
		||||
  onSubmit: (values: LoginFormValues) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const LoginForm = (props: LoginFormProps) => {
 | 
			
		||||
  const { isPending, onSubmit } = props;
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const form = useForm({
 | 
			
		||||
    mode: "uncontrolled",
 | 
			
		||||
    initialValues: {
 | 
			
		||||
      username: "",
 | 
			
		||||
      password: "",
 | 
			
		||||
    },
 | 
			
		||||
    validate: zodResolver(loginSchema),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form onSubmit={form.onSubmit(onSubmit)}>
 | 
			
		||||
      <TextInput
 | 
			
		||||
        label={t("loginUsername")}
 | 
			
		||||
        placeholder="Username"
 | 
			
		||||
        disabled={isPending}
 | 
			
		||||
        required
 | 
			
		||||
        withAsterisk={false}
 | 
			
		||||
        key={form.key("username")}
 | 
			
		||||
        {...form.getInputProps("username")}
 | 
			
		||||
      />
 | 
			
		||||
      <Group justify="space-between" mb={5} mt="md">
 | 
			
		||||
        <Text component="label" htmlFor=".password-input" size="sm" fw={500}>
 | 
			
		||||
        {t("loginPassword")}
 | 
			
		||||
        </Text>
 | 
			
		||||
 | 
			
		||||
        <Anchor href="#" onClick={() => window.location.replace("/forgot-password")} pt={2} fw={500} fz="xs">
 | 
			
		||||
          {t('forgotPasswordTitle')}
 | 
			
		||||
        </Anchor>
 | 
			
		||||
      </Group>
 | 
			
		||||
      <PasswordInput
 | 
			
		||||
        className="password-input"
 | 
			
		||||
        placeholder="Password"
 | 
			
		||||
        required
 | 
			
		||||
        disabled={isPending}
 | 
			
		||||
        key={form.key("password")}
 | 
			
		||||
        {...form.getInputProps("password")}
 | 
			
		||||
      />
 | 
			
		||||
      <Button fullWidth mt="xl" type="submit" loading={isPending}>
 | 
			
		||||
        {t("loginSubmit")}
 | 
			
		||||
      </Button>
 | 
			
		||||
    </form>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										31
									
								
								frontend/src/components/auth/oauth-button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
			
		||||
import { Loader2 } from "lucide-react";
 | 
			
		||||
import { Button } from "../ui/button";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  title: string;
 | 
			
		||||
  icon: React.ReactNode;
 | 
			
		||||
  onClick?: () => void;
 | 
			
		||||
  loading?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const OAuthButton = (props: Props) => {
 | 
			
		||||
  const { title, icon, onClick, loading } = props;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Button
 | 
			
		||||
      onClick={onClick}
 | 
			
		||||
      className="rounded-full basis-1/3"
 | 
			
		||||
      variant="outline"
 | 
			
		||||
    >
 | 
			
		||||
      {loading ? (
 | 
			
		||||
        <Loader2 className="animate-spin" />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          {icon}
 | 
			
		||||
          {title}
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </Button>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
import { Grid, Button } from "@mantine/core";
 | 
			
		||||
import { GithubIcon } from "../../icons/github";
 | 
			
		||||
import { GoogleIcon } from "../../icons/google";
 | 
			
		||||
import { OAuthIcon } from "../../icons/oauth";
 | 
			
		||||
 | 
			
		||||
interface OAuthButtonsProps {
 | 
			
		||||
  oauthProviders: string[];
 | 
			
		||||
  isPending: boolean;
 | 
			
		||||
  mutate: (provider: string) => void;
 | 
			
		||||
  genericName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const OAuthButtons = (props: OAuthButtonsProps) => {
 | 
			
		||||
  const { oauthProviders, isPending, genericName, mutate } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid mb="md" mt="md" align="center" justify="center">
 | 
			
		||||
      {oauthProviders.includes("google") && (
 | 
			
		||||
        <Grid.Col span="content">
 | 
			
		||||
          <Button
 | 
			
		||||
            radius="xl"
 | 
			
		||||
            leftSection={<GoogleIcon style={{ width: 14, height: 14 }} />}
 | 
			
		||||
            variant="default"
 | 
			
		||||
            onClick={() => mutate("google")}
 | 
			
		||||
            loading={isPending}
 | 
			
		||||
          >
 | 
			
		||||
            Google
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Grid.Col>
 | 
			
		||||
      )}
 | 
			
		||||
      {oauthProviders.includes("github") && (
 | 
			
		||||
        <Grid.Col span="content">
 | 
			
		||||
          <Button
 | 
			
		||||
            radius="xl"
 | 
			
		||||
            leftSection={<GithubIcon style={{ width: 14, height: 14 }} />}
 | 
			
		||||
            variant="default"
 | 
			
		||||
            onClick={() => mutate("github")}
 | 
			
		||||
            loading={isPending}
 | 
			
		||||
          >
 | 
			
		||||
            Github
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Grid.Col>
 | 
			
		||||
      )}
 | 
			
		||||
      {oauthProviders.includes("generic") && (
 | 
			
		||||
        <Grid.Col span="content">
 | 
			
		||||
          <Button
 | 
			
		||||
            radius="xl"
 | 
			
		||||
            leftSection={<OAuthIcon style={{ width: 14, height: 14 }} />}
 | 
			
		||||
            variant="default"
 | 
			
		||||
            onClick={() => mutate("generic")}
 | 
			
		||||
            loading={isPending}
 | 
			
		||||
          >
 | 
			
		||||
            {genericName}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Grid.Col>
 | 
			
		||||
      )}
 | 
			
		||||
    </Grid>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
import { Button, PinInput } from "@mantine/core";
 | 
			
		||||
import { useForm, zodResolver } from "@mantine/form";
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
 | 
			
		||||
const schema = z.object({
 | 
			
		||||
  code: z.string(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
type FormValues = z.infer<typeof schema>;
 | 
			
		||||
 | 
			
		||||
interface TotpFormProps {
 | 
			
		||||
  onSubmit: (values: FormValues) => void;
 | 
			
		||||
  isPending: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TotpForm = (props: TotpFormProps) => {
 | 
			
		||||
  const { onSubmit, isPending } = props;
 | 
			
		||||
 | 
			
		||||
  const form = useForm({
 | 
			
		||||
    mode: "uncontrolled",
 | 
			
		||||
    initialValues: {
 | 
			
		||||
      code: "",
 | 
			
		||||
    },
 | 
			
		||||
    validate: zodResolver(schema),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form onSubmit={form.onSubmit(onSubmit)}>
 | 
			
		||||
      <PinInput
 | 
			
		||||
        length={6}
 | 
			
		||||
        type={"number"}
 | 
			
		||||
        placeholder=""
 | 
			
		||||
        {...form.getInputProps("code")}
 | 
			
		||||
      />
 | 
			
		||||
      <Button type="submit" mt="xl" loading={isPending} fullWidth>
 | 
			
		||||
        Verify
 | 
			
		||||
      </Button>
 | 
			
		||||
    </form>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import type { SVGProps } from "react";
 | 
			
		||||
 | 
			
		||||
export function OAuthIcon(props: SVGProps<SVGSVGElement>) {
 | 
			
		||||
export function GenericIcon(props: SVGProps<SVGSVGElement>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
							
								
								
									
										30
									
								
								frontend/src/components/icons/google.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,30 @@
 | 
			
		||||
import type { SVGProps } from "react";
 | 
			
		||||
 | 
			
		||||
export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      width={256}
 | 
			
		||||
      height={262}
 | 
			
		||||
      viewBox="0 0 256 262"
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill="#4285f4"
 | 
			
		||||
        d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
 | 
			
		||||
      ></path>
 | 
			
		||||
      <path
 | 
			
		||||
        fill="#34a853"
 | 
			
		||||
        d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
 | 
			
		||||
      ></path>
 | 
			
		||||
      <path
 | 
			
		||||
        fill="#fbbc05"
 | 
			
		||||
        d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
 | 
			
		||||
      ></path>
 | 
			
		||||
      <path
 | 
			
		||||
        fill="#eb4335"
 | 
			
		||||
        d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
 | 
			
		||||
      ></path>
 | 
			
		||||
    </svg>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
import { ComboboxItem, Select } from "@mantine/core";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import i18n from "../../lib/i18n/i18n";
 | 
			
		||||
import {
 | 
			
		||||
  SupportedLanguage,
 | 
			
		||||
  getLanguageName,
 | 
			
		||||
  languages,
 | 
			
		||||
} from "../../lib/i18n/locales";
 | 
			
		||||
 | 
			
		||||
export const LanguageSelector = () => {
 | 
			
		||||
  const [language, setLanguage] = useState<ComboboxItem>({
 | 
			
		||||
    value: i18n.language,
 | 
			
		||||
    label: getLanguageName(i18n.language as SupportedLanguage),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const languageOptions = Object.entries(languages).map(([code, name]) => ({
 | 
			
		||||
    value: code,
 | 
			
		||||
    label: name,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  const handleLanguageChange = (option: string) => {
 | 
			
		||||
    i18n.changeLanguage(option as SupportedLanguage);
 | 
			
		||||
    setLanguage({
 | 
			
		||||
      value: option,
 | 
			
		||||
      label: getLanguageName(option as SupportedLanguage),
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Select
 | 
			
		||||
      data={languageOptions}
 | 
			
		||||
      value={language ? language.value : null}
 | 
			
		||||
      onChange={(_value, option) => handleLanguageChange(option.value)}
 | 
			
		||||
      allowDeselect={false}
 | 
			
		||||
      pos="absolute"
 | 
			
		||||
      right={10}
 | 
			
		||||
      top={10}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										35
									
								
								frontend/src/components/language/language.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,35 @@
 | 
			
		||||
import { languages, SupportedLanguage } from "@/lib/i18n/locales";
 | 
			
		||||
import {
 | 
			
		||||
  Select,
 | 
			
		||||
  SelectContent,
 | 
			
		||||
  SelectItem,
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
} from "../ui/select";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import i18n from "@/lib/i18n/i18n";
 | 
			
		||||
 | 
			
		||||
export const LanguageSelector = () => {
 | 
			
		||||
  const [language, setLanguage] = useState<SupportedLanguage>(
 | 
			
		||||
    i18n.language as SupportedLanguage,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleSelect = (option: string) => {
 | 
			
		||||
    setLanguage(option as SupportedLanguage);
 | 
			
		||||
    i18n.changeLanguage(option as SupportedLanguage);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Select onValueChange={handleSelect} value={language}>
 | 
			
		||||
      <SelectTrigger className="absolute top-5 right-5">
 | 
			
		||||
        <SelectValue placeholder="Select language" />
 | 
			
		||||
      </SelectTrigger>
 | 
			
		||||
      <SelectContent>
 | 
			
		||||
        {Object.entries(languages).map(([key, value]) => (
 | 
			
		||||
          <SelectItem key={key} value={key}>
 | 
			
		||||
            {value}
 | 
			
		||||
          </SelectItem>
 | 
			
		||||
        ))}
 | 
			
		||||
      </SelectContent>
 | 
			
		||||
    </Select>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										10
									
								
								frontend/src/components/layout/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
			
		||||
import { LanguageSelector } from "../language/language";
 | 
			
		||||
 | 
			
		||||
export const Layout = ({ children }: { children: React.ReactNode }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col justify-center items-center min-h-svh bg-[url(/background.jpg)] bg-cover">
 | 
			
		||||
      <LanguageSelector />
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
import { Center, Flex } from "@mantine/core";
 | 
			
		||||
import { ReactNode } from "react";
 | 
			
		||||
import { LanguageSelector } from "../language-selector/language-selector";
 | 
			
		||||
 | 
			
		||||
export const Layout = ({ children }: { children: ReactNode }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <LanguageSelector />
 | 
			
		||||
      <Center style={{ minHeight: "100vh" }}>
 | 
			
		||||
        <Flex direction="column" flex="1" maw={340}>
 | 
			
		||||
          {children}
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </Center>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										61
									
								
								frontend/src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,61 @@
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
import { Slot } from "@radix-ui/react-slot";
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const buttonVariants = cva(
 | 
			
		||||
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default:
 | 
			
		||||
          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
 | 
			
		||||
        destructive:
 | 
			
		||||
          "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
 | 
			
		||||
        outline:
 | 
			
		||||
          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
 | 
			
		||||
        secondary:
 | 
			
		||||
          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
 | 
			
		||||
        ghost:
 | 
			
		||||
          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
 | 
			
		||||
        link: "text-primary underline-offset-4 hover:underline",
 | 
			
		||||
        warning:
 | 
			
		||||
          "bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40 dark:bg-amber-600",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-9 px-4 py-2 has-[>svg]:px-3",
 | 
			
		||||
        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
 | 
			
		||||
        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
 | 
			
		||||
        icon: "size-9",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
function Button({
 | 
			
		||||
  className,
 | 
			
		||||
  variant,
 | 
			
		||||
  size,
 | 
			
		||||
  asChild = false,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"button"> &
 | 
			
		||||
  VariantProps<typeof buttonVariants> & {
 | 
			
		||||
    asChild?: boolean;
 | 
			
		||||
  }) {
 | 
			
		||||
  const Comp = asChild ? Slot : "button";
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Comp
 | 
			
		||||
      data-slot="button"
 | 
			
		||||
      className={cn(buttonVariants({ variant, size, className }))}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Button, buttonVariants };
 | 
			
		||||
							
								
								
									
										92
									
								
								frontend/src/components/ui/card.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,92 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-header"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-title"
 | 
			
		||||
      className={cn("leading-none font-semibold", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-description"
 | 
			
		||||
      className={cn("text-muted-foreground text-sm", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-action"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-content"
 | 
			
		||||
      className={cn("px-6", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="card-footer"
 | 
			
		||||
      className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardFooter,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
  CardAction,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardContent,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										75
									
								
								frontend/src/components/ui/input-otp.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,75 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import { OTPInput, OTPInputContext } from "input-otp"
 | 
			
		||||
import { MinusIcon } from "lucide-react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function InputOTP({
 | 
			
		||||
  className,
 | 
			
		||||
  containerClassName,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof OTPInput> & {
 | 
			
		||||
  containerClassName?: string
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <OTPInput
 | 
			
		||||
      data-slot="input-otp"
 | 
			
		||||
      containerClassName={cn(
 | 
			
		||||
        "flex items-center gap-2 has-disabled:opacity-50",
 | 
			
		||||
        containerClassName
 | 
			
		||||
      )}
 | 
			
		||||
      className={cn("disabled:cursor-not-allowed", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="input-otp-group"
 | 
			
		||||
      className={cn("flex items-center", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function InputOTPSlot({
 | 
			
		||||
  index,
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"div"> & {
 | 
			
		||||
  index: number
 | 
			
		||||
}) {
 | 
			
		||||
  const inputOTPContext = React.useContext(OTPInputContext)
 | 
			
		||||
  const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      data-slot="input-otp-slot"
 | 
			
		||||
      data-active={isActive}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {char}
 | 
			
		||||
      {hasFakeCaret && (
 | 
			
		||||
        <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
 | 
			
		||||
          <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div data-slot="input-otp-separator" role="separator" {...props}>
 | 
			
		||||
      <MinusIcon />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
 | 
			
		||||
							
								
								
									
										21
									
								
								frontend/src/components/ui/input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <input
 | 
			
		||||
      type={type}
 | 
			
		||||
      data-slot="input"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
 | 
			
		||||
        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
 | 
			
		||||
        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Input }
 | 
			
		||||
							
								
								
									
										22
									
								
								frontend/src/components/ui/label.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,22 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as LabelPrimitive from "@radix-ui/react-label"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Label({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <LabelPrimitive.Root
 | 
			
		||||
      data-slot="label"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Label }
 | 
			
		||||
							
								
								
									
										183
									
								
								frontend/src/components/ui/select.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,183 @@
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
import * as SelectPrimitive from "@radix-ui/react-select";
 | 
			
		||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
function Select({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
 | 
			
		||||
  return <SelectPrimitive.Root data-slot="select" {...props} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SelectGroup({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
 | 
			
		||||
  return <SelectPrimitive.Group data-slot="select-group" {...props} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SelectValue({
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
 | 
			
		||||
  return <SelectPrimitive.Value data-slot="select-value" {...props} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SelectTrigger({
 | 
			
		||||
  className,
 | 
			
		||||
  size = "default",
 | 
			
		||||
  children,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
 | 
			
		||||
  size?: "sm" | "default";
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectPrimitive.Trigger
 | 
			
		||||
      data-slot="select-trigger"
 | 
			
		||||
      data-size={size}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-secondary dark:hover:bg-secondary/80 flex w-fit items-center justify-between gap-2 rounded-md border bg-primary px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <SelectPrimitive.Icon asChild>
 | 
			
		||||
        <ChevronDownIcon className="size-4" />
 | 
			
		||||
      </SelectPrimitive.Icon>
 | 
			
		||||
    </SelectPrimitive.Trigger>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SelectContent({
 | 
			
		||||
  className,
 | 
			
		||||
  children,
 | 
			
		||||
  position = "popper",
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectPrimitive.Portal>
 | 
			
		||||
      <SelectPrimitive.Content
 | 
			
		||||
        data-slot="select-content"
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
 | 
			
		||||
          position === "popper" &&
 | 
			
		||||
            "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
 | 
			
		||||
          className,
 | 
			
		||||
        )}
 | 
			
		||||
        position={position}
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
        <SelectScrollUpButton />
 | 
			
		||||
        <SelectPrimitive.Viewport
 | 
			
		||||
          className={cn(
 | 
			
		||||
            "p-1",
 | 
			
		||||
            position === "popper" &&
 | 
			
		||||
              "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
 | 
			
		||||
          )}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
        </SelectPrimitive.Viewport>
 | 
			
		||||
        <SelectScrollDownButton />
 | 
			
		||||
      </SelectPrimitive.Content>
 | 
			
		||||
    </SelectPrimitive.Portal>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SelectLabel({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectPrimitive.Label
 | 
			
		||||
      data-slot="select-label"
 | 
			
		||||
      className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SelectItem({
 | 
			
		||||
  className,
 | 
			
		||||
  children,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectPrimitive.Item
 | 
			
		||||
      data-slot="select-item"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <span className="absolute right-2 flex size-3.5 items-center justify-center">
 | 
			
		||||
        <SelectPrimitive.ItemIndicator>
 | 
			
		||||
          <CheckIcon className="size-4" />
 | 
			
		||||
        </SelectPrimitive.ItemIndicator>
 | 
			
		||||
      </span>
 | 
			
		||||
      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
 | 
			
		||||
    </SelectPrimitive.Item>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SelectSeparator({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectPrimitive.Separator
 | 
			
		||||
      data-slot="select-separator"
 | 
			
		||||
      className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SelectScrollUpButton({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectPrimitive.ScrollUpButton
 | 
			
		||||
      data-slot="select-scroll-up-button"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "flex cursor-default items-center justify-center py-1",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <ChevronUpIcon className="size-4" />
 | 
			
		||||
    </SelectPrimitive.ScrollUpButton>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SelectScrollDownButton({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectPrimitive.ScrollDownButton
 | 
			
		||||
      data-slot="select-scroll-down-button"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "flex cursor-default items-center justify-center py-1",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <ChevronDownIcon className="size-4" />
 | 
			
		||||
    </SelectPrimitive.ScrollDownButton>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Select,
 | 
			
		||||
  SelectContent,
 | 
			
		||||
  SelectGroup,
 | 
			
		||||
  SelectItem,
 | 
			
		||||
  SelectLabel,
 | 
			
		||||
  SelectScrollDownButton,
 | 
			
		||||
  SelectScrollUpButton,
 | 
			
		||||
  SelectSeparator,
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										28
									
								
								frontend/src/components/ui/separator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,28 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Separator({
 | 
			
		||||
  className,
 | 
			
		||||
  orientation = "horizontal",
 | 
			
		||||
  decorative = true,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SeparatorPrimitive.Root
 | 
			
		||||
      data-slot="separator-root"
 | 
			
		||||
      decorative={decorative}
 | 
			
		||||
      orientation={orientation}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Separator }
 | 
			
		||||
@@ -1,42 +0,0 @@
 | 
			
		||||
import { useSuspenseQuery } from "@tanstack/react-query";
 | 
			
		||||
import React, { createContext, useContext } from "react";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { AppContextSchemaType } from "../schemas/app-context-schema";
 | 
			
		||||
 | 
			
		||||
const AppContext = createContext<AppContextSchemaType | null>(null);
 | 
			
		||||
 | 
			
		||||
export const AppContextProvider = ({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
}) => {
 | 
			
		||||
  const {
 | 
			
		||||
    data: userContext,
 | 
			
		||||
    isLoading,
 | 
			
		||||
    error,
 | 
			
		||||
  } = useSuspenseQuery({
 | 
			
		||||
    queryKey: ["appContext"],
 | 
			
		||||
    queryFn: async () => {
 | 
			
		||||
      const res = await axios.get("/api/app");
 | 
			
		||||
      return res.data;
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (error && !isLoading) {
 | 
			
		||||
    throw error;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AppContext.Provider value={userContext}>{children}</AppContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useAppContext = () => {
 | 
			
		||||
  const context = useContext(AppContext);
 | 
			
		||||
 | 
			
		||||
  if (context === null) {
 | 
			
		||||
    throw new Error("useAppContext must be used within an AppContextProvider");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return context;
 | 
			
		||||
};
 | 
			
		||||
@@ -1,42 +0,0 @@
 | 
			
		||||
import { useSuspenseQuery } from "@tanstack/react-query";
 | 
			
		||||
import React, { createContext, useContext } from "react";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { UserContextSchemaType } from "../schemas/user-context-schema";
 | 
			
		||||
 | 
			
		||||
const UserContext = createContext<UserContextSchemaType | null>(null);
 | 
			
		||||
 | 
			
		||||
export const UserContextProvider = ({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
}) => {
 | 
			
		||||
  const {
 | 
			
		||||
    data: userContext,
 | 
			
		||||
    isLoading,
 | 
			
		||||
    error,
 | 
			
		||||
  } = useSuspenseQuery({
 | 
			
		||||
    queryKey: ["userContext"],
 | 
			
		||||
    queryFn: async () => {
 | 
			
		||||
      const res = await axios.get("/api/user");
 | 
			
		||||
      return res.data;
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (error && !isLoading) {
 | 
			
		||||
    throw error;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <UserContext.Provider value={userContext}>{children}</UserContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useUserContext = () => {
 | 
			
		||||
  const context = useContext(UserContext);
 | 
			
		||||
 | 
			
		||||
  if (context === null) {
 | 
			
		||||
    throw new Error("useUserContext must be used within a UserContextProvider");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return context;
 | 
			
		||||
};
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
import type { SVGProps } from "react";
 | 
			
		||||
 | 
			
		||||
export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      width={48}
 | 
			
		||||
      height={48}
 | 
			
		||||
      viewBox="0 0 48 48"
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill="#ffc107"
 | 
			
		||||
        d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8c-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4C12.955 4 4 12.955 4 24s8.955 20 20 20s20-8.955 20-20c0-1.341-.138-2.65-.389-3.917"
 | 
			
		||||
      ></path>
 | 
			
		||||
      <path
 | 
			
		||||
        fill="#ff3d00"
 | 
			
		||||
        d="m6.306 14.691l6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4C16.318 4 9.656 8.337 6.306 14.691"
 | 
			
		||||
      ></path>
 | 
			
		||||
      <path
 | 
			
		||||
        fill="#4caf50"
 | 
			
		||||
        d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.9 11.9 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44"
 | 
			
		||||
      ></path>
 | 
			
		||||
      <path
 | 
			
		||||
        fill="#1976d2"
 | 
			
		||||
        d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002l6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917"
 | 
			
		||||
      ></path>
 | 
			
		||||
    </svg>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										120
									
								
								frontend/src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,120 @@
 | 
			
		||||
@import "tailwindcss";
 | 
			
		||||
@import "tw-animate-css";
 | 
			
		||||
 | 
			
		||||
@custom-variant dark (&:is(.dark *));
 | 
			
		||||
 | 
			
		||||
@theme inline {
 | 
			
		||||
  --radius-sm: calc(var(--radius) - 4px);
 | 
			
		||||
  --radius-md: calc(var(--radius) - 2px);
 | 
			
		||||
  --radius-lg: var(--radius);
 | 
			
		||||
  --radius-xl: calc(var(--radius) + 4px);
 | 
			
		||||
  --color-background: var(--background);
 | 
			
		||||
  --color-foreground: var(--foreground);
 | 
			
		||||
  --color-card: var(--card);
 | 
			
		||||
  --color-card-foreground: var(--card-foreground);
 | 
			
		||||
  --color-popover: var(--popover);
 | 
			
		||||
  --color-popover-foreground: var(--popover-foreground);
 | 
			
		||||
  --color-primary: var(--primary);
 | 
			
		||||
  --color-primary-foreground: var(--primary-foreground);
 | 
			
		||||
  --color-secondary: var(--secondary);
 | 
			
		||||
  --color-secondary-foreground: var(--secondary-foreground);
 | 
			
		||||
  --color-muted: var(--muted);
 | 
			
		||||
  --color-muted-foreground: var(--muted-foreground);
 | 
			
		||||
  --color-accent: var(--accent);
 | 
			
		||||
  --color-accent-foreground: var(--accent-foreground);
 | 
			
		||||
  --color-destructive: var(--destructive);
 | 
			
		||||
  --color-border: var(--border);
 | 
			
		||||
  --color-input: var(--input);
 | 
			
		||||
  --color-ring: var(--ring);
 | 
			
		||||
  --color-chart-1: var(--chart-1);
 | 
			
		||||
  --color-chart-2: var(--chart-2);
 | 
			
		||||
  --color-chart-3: var(--chart-3);
 | 
			
		||||
  --color-chart-4: var(--chart-4);
 | 
			
		||||
  --color-chart-5: var(--chart-5);
 | 
			
		||||
  --color-sidebar: var(--sidebar);
 | 
			
		||||
  --color-sidebar-foreground: var(--sidebar-foreground);
 | 
			
		||||
  --color-sidebar-primary: var(--sidebar-primary);
 | 
			
		||||
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
 | 
			
		||||
  --color-sidebar-accent: var(--sidebar-accent);
 | 
			
		||||
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
 | 
			
		||||
  --color-sidebar-border: var(--sidebar-border);
 | 
			
		||||
  --color-sidebar-ring: var(--sidebar-ring);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  --radius: 0.625rem;
 | 
			
		||||
  --background: oklch(1 0 0);
 | 
			
		||||
  --foreground: oklch(0.145 0 0);
 | 
			
		||||
  --card: oklch(1 0 0);
 | 
			
		||||
  --card-foreground: oklch(0.145 0 0);
 | 
			
		||||
  --popover: oklch(1 0 0);
 | 
			
		||||
  --popover-foreground: oklch(0.145 0 0);
 | 
			
		||||
  --primary: oklch(0.205 0 0);
 | 
			
		||||
  --primary-foreground: oklch(0.985 0 0);
 | 
			
		||||
  --secondary: oklch(0.97 0 0);
 | 
			
		||||
  --secondary-foreground: oklch(0.205 0 0);
 | 
			
		||||
  --muted: oklch(0.97 0 0);
 | 
			
		||||
  --muted-foreground: oklch(0.556 0 0);
 | 
			
		||||
  --accent: oklch(0.97 0 0);
 | 
			
		||||
  --accent-foreground: oklch(0.205 0 0);
 | 
			
		||||
  --destructive: oklch(0.577 0.245 27.325);
 | 
			
		||||
  --border: oklch(0.922 0 0);
 | 
			
		||||
  --input: oklch(0.922 0 0);
 | 
			
		||||
  --ring: oklch(0.708 0 0);
 | 
			
		||||
  --chart-1: oklch(0.646 0.222 41.116);
 | 
			
		||||
  --chart-2: oklch(0.6 0.118 184.704);
 | 
			
		||||
  --chart-3: oklch(0.398 0.07 227.392);
 | 
			
		||||
  --chart-4: oklch(0.828 0.189 84.429);
 | 
			
		||||
  --chart-5: oklch(0.769 0.188 70.08);
 | 
			
		||||
  --sidebar: oklch(0.985 0 0);
 | 
			
		||||
  --sidebar-foreground: oklch(0.145 0 0);
 | 
			
		||||
  --sidebar-primary: oklch(0.205 0 0);
 | 
			
		||||
  --sidebar-primary-foreground: oklch(0.985 0 0);
 | 
			
		||||
  --sidebar-accent: oklch(0.97 0 0);
 | 
			
		||||
  --sidebar-accent-foreground: oklch(0.205 0 0);
 | 
			
		||||
  --sidebar-border: oklch(0.922 0 0);
 | 
			
		||||
  --sidebar-ring: oklch(0.708 0 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark {
 | 
			
		||||
  --background: oklch(0.145 0 0);
 | 
			
		||||
  --foreground: oklch(0.985 0 0);
 | 
			
		||||
  --card: oklch(0.205 0 0);
 | 
			
		||||
  --card-foreground: oklch(0.985 0 0);
 | 
			
		||||
  --popover: oklch(0.205 0 0);
 | 
			
		||||
  --popover-foreground: oklch(0.985 0 0);
 | 
			
		||||
  --primary: oklch(0.922 0 0);
 | 
			
		||||
  --primary-foreground: oklch(0.205 0 0);
 | 
			
		||||
  --secondary: oklch(0.269 0 0);
 | 
			
		||||
  --secondary-foreground: oklch(0.985 0 0);
 | 
			
		||||
  --muted: oklch(0.269 0 0);
 | 
			
		||||
  --muted-foreground: oklch(0.708 0 0);
 | 
			
		||||
  --accent: oklch(0.269 0 0);
 | 
			
		||||
  --accent-foreground: oklch(0.985 0 0);
 | 
			
		||||
  --destructive: oklch(0.704 0.191 22.216);
 | 
			
		||||
  --border: oklch(1 0 0 / 10%);
 | 
			
		||||
  --input: oklch(1 0 0 / 15%);
 | 
			
		||||
  --ring: oklch(0.556 0 0);
 | 
			
		||||
  --chart-1: oklch(0.488 0.243 264.376);
 | 
			
		||||
  --chart-2: oklch(0.696 0.17 162.48);
 | 
			
		||||
  --chart-3: oklch(0.769 0.188 70.08);
 | 
			
		||||
  --chart-4: oklch(0.627 0.265 303.9);
 | 
			
		||||
  --chart-5: oklch(0.645 0.246 16.439);
 | 
			
		||||
  --sidebar: oklch(0.205 0 0);
 | 
			
		||||
  --sidebar-foreground: oklch(0.985 0 0);
 | 
			
		||||
  --sidebar-primary: oklch(0.488 0.243 264.376);
 | 
			
		||||
  --sidebar-primary-foreground: oklch(0.985 0 0);
 | 
			
		||||
  --sidebar-accent: oklch(0.269 0 0);
 | 
			
		||||
  --sidebar-accent-foreground: oklch(0.985 0 0);
 | 
			
		||||
  --sidebar-border: oklch(1 0 0 / 10%);
 | 
			
		||||
  --sidebar-ring: oklch(0.556 0 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  * {
 | 
			
		||||
    @apply border-border outline-ring/50;
 | 
			
		||||
  }
 | 
			
		||||
  body {
 | 
			
		||||
    @apply bg-background text-foreground;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,28 +1,14 @@
 | 
			
		||||
import i18n from "i18next";
 | 
			
		||||
import { initReactI18next } from "react-i18next";
 | 
			
		||||
import LanguageDetector from "i18next-browser-languagedetector";
 | 
			
		||||
import ChainedBackend from "i18next-chained-backend";
 | 
			
		||||
import resourcesToBackend from "i18next-resources-to-backend";
 | 
			
		||||
import HttpBackend from "i18next-http-backend";
 | 
			
		||||
 | 
			
		||||
const backends = [
 | 
			
		||||
  HttpBackend,
 | 
			
		||||
  resourcesToBackend(
 | 
			
		||||
    (language: string) => import(`./locales/${language}.json`),
 | 
			
		||||
  ),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
const backendOptions =  [
 | 
			
		||||
  {
 | 
			
		||||
    loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json",
 | 
			
		||||
  },
 | 
			
		||||
  {}
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
i18n
 | 
			
		||||
  .use(ChainedBackend)
 | 
			
		||||
  .use(LanguageDetector)
 | 
			
		||||
  .use(initReactI18next)
 | 
			
		||||
  .use(resourcesToBackend(
 | 
			
		||||
    (language: string) => import(`./locales/${language}.json`),
 | 
			
		||||
  ))
 | 
			
		||||
  .init({
 | 
			
		||||
    fallbackLng: "en",
 | 
			
		||||
    debug: import.meta.env.MODE === "development",
 | 
			
		||||
@@ -32,11 +18,6 @@ i18n
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    load: "currentOnly",
 | 
			
		||||
 | 
			
		||||
    backend: {
 | 
			
		||||
      backends: import.meta.env.MODE !== "development" ? backends : backends.reverse(),
 | 
			
		||||
      backendOptions: import.meta.env.MODE !== "development" ? backendOptions : backendOptions.reverse()
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export default i18n;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
    "loginTitle": "Welcome back, login with",
 | 
			
		||||
    "loginTitleSimple": "Welcome back, please login",
 | 
			
		||||
    "loginDivider": "Or continue with password",
 | 
			
		||||
    "loginUsername": "Username",
 | 
			
		||||
    "loginPassword": "Password",
 | 
			
		||||
@@ -18,7 +19,7 @@
 | 
			
		||||
    "continueInvalidRedirectTitle": "Invalid redirect",
 | 
			
		||||
    "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
 | 
			
		||||
    "continueInsecureRedirectTitle": "Insecure redirect",
 | 
			
		||||
    "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
 | 
			
		||||
    "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code>, are you sure you want to continue?",
 | 
			
		||||
    "continueTitle": "Continue",
 | 
			
		||||
    "continueSubtitle": "Click the button to continue to your app.",
 | 
			
		||||
    "internalErrorTitle": "Internal Server Error",
 | 
			
		||||
@@ -29,8 +30,8 @@
 | 
			
		||||
    "logoutSuccessTitle": "Logged out",
 | 
			
		||||
    "logoutSuccessSubtitle": "You have been logged out",
 | 
			
		||||
    "logoutTitle": "Logout",
 | 
			
		||||
    "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
 | 
			
		||||
    "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
 | 
			
		||||
    "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>, click the button below to logout.",
 | 
			
		||||
    "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider, click the button below to logout.",
 | 
			
		||||
    "notFoundTitle": "Page not found",
 | 
			
		||||
    "notFoundSubtitle": "The page you are looking for does not exist.",
 | 
			
		||||
    "notFoundButton": "Go home",
 | 
			
		||||
@@ -38,13 +39,18 @@
 | 
			
		||||
    "totpFailSubtitle": "Please check your code and try again",
 | 
			
		||||
    "totpSuccessTitle": "Verified",
 | 
			
		||||
    "totpSuccessSubtitle": "Redirecting to your app",
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "totpTitle": "TOTP Verification",
 | 
			
		||||
    "totpSubtitle": "Please enter the code from your authenticator app.",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
 | 
			
		||||
    "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
 | 
			
		||||
    "unauthorizedButton": "Try again",
 | 
			
		||||
    "untrustedRedirectTitle": "Untrusted redirect",
 | 
			
		||||
    "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
 | 
			
		||||
    "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
 | 
			
		||||
    "cancelTitle": "Cancel",
 | 
			
		||||
    "forgotPasswordTitle": "Forgot your password?"
 | 
			
		||||
    "forgotPasswordTitle": "Forgot your password?",
 | 
			
		||||
    "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
 | 
			
		||||
    "errorTitle": "An error occurred",
 | 
			
		||||
    "errorSubtitle": "An error occured while trying to perform this action. Please check the console for more information."
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
    "loginTitle": "Welcome back, login with",
 | 
			
		||||
    "loginTitleSimple": "Welcome back, please login",
 | 
			
		||||
    "loginDivider": "Or continue with password",
 | 
			
		||||
    "loginUsername": "Username",
 | 
			
		||||
    "loginPassword": "Password",
 | 
			
		||||
@@ -18,7 +19,7 @@
 | 
			
		||||
    "continueInvalidRedirectTitle": "Invalid redirect",
 | 
			
		||||
    "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
 | 
			
		||||
    "continueInsecureRedirectTitle": "Insecure redirect",
 | 
			
		||||
    "continueInsecureRedirectSubtitle": "You are trying to redirect from <Code>https</Code> to <Code>http</Code>, are you sure you want to continue?",
 | 
			
		||||
    "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code>, are you sure you want to continue?",
 | 
			
		||||
    "continueTitle": "Continue",
 | 
			
		||||
    "continueSubtitle": "Click the button to continue to your app.",
 | 
			
		||||
    "internalErrorTitle": "Internal Server Error",
 | 
			
		||||
@@ -29,8 +30,8 @@
 | 
			
		||||
    "logoutSuccessTitle": "Logged out",
 | 
			
		||||
    "logoutSuccessSubtitle": "You have been logged out",
 | 
			
		||||
    "logoutTitle": "Logout",
 | 
			
		||||
    "logoutUsernameSubtitle": "You are currently logged in as <Code>{{username}}</Code>, click the button below to logout.",
 | 
			
		||||
    "logoutOauthSubtitle": "You are currently logged in as <Code>{{username}}</Code> using the {{provider}} OAuth provider, click the button below to logout.",
 | 
			
		||||
    "logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>, click the button below to logout.",
 | 
			
		||||
    "logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider, click the button below to logout.",
 | 
			
		||||
    "notFoundTitle": "Page not found",
 | 
			
		||||
    "notFoundSubtitle": "The page you are looking for does not exist.",
 | 
			
		||||
    "notFoundButton": "Go home",
 | 
			
		||||
@@ -39,12 +40,17 @@
 | 
			
		||||
    "totpSuccessTitle": "Verified",
 | 
			
		||||
    "totpSuccessSubtitle": "Redirecting to your app",
 | 
			
		||||
    "totpTitle": "Enter your TOTP code",
 | 
			
		||||
    "totpSubtitle": "Please enter the code from your authenticator app.",
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
 | 
			
		||||
    "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
 | 
			
		||||
    "unauthorizedButton": "Try again",
 | 
			
		||||
    "untrustedRedirectTitle": "Untrusted redirect",
 | 
			
		||||
    "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
 | 
			
		||||
    "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
 | 
			
		||||
    "cancelTitle": "Cancel",
 | 
			
		||||
    "forgotPasswordTitle": "Forgot your password?"
 | 
			
		||||
    "forgotPasswordTitle": "Forgot your password?",
 | 
			
		||||
    "failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
 | 
			
		||||
    "errorTitle": "An error occurred",
 | 
			
		||||
    "errorSubtitle": "An error occured while trying to perform this action. Please check the console for more information."
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								frontend/src/lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
import { clsx, type ClassValue } from "clsx"
 | 
			
		||||
import { twMerge } from "tailwind-merge"
 | 
			
		||||
 | 
			
		||||
export function cn(...inputs: ClassValue[]) {
 | 
			
		||||
  return twMerge(clsx(inputs))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const isValidUrl = (url: string) => {
 | 
			
		||||
  try {
 | 
			
		||||
    new URL(url)
 | 
			
		||||
    return true
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,50 +1,47 @@
 | 
			
		||||
import { StrictMode } from "react";
 | 
			
		||||
import { createRoot } from "react-dom/client";
 | 
			
		||||
import { App } from "./App.tsx";
 | 
			
		||||
import { MantineProvider } from "@mantine/core";
 | 
			
		||||
import { Notifications } from "@mantine/notifications";
 | 
			
		||||
import "@mantine/core/styles.css";
 | 
			
		||||
import "@mantine/notifications/styles.css";
 | 
			
		||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 | 
			
		||||
import { BrowserRouter, Route } from "react-router";
 | 
			
		||||
import { Routes } from "react-router";
 | 
			
		||||
import { UserContextProvider } from "./context/user-context.tsx";
 | 
			
		||||
import "./index.css";
 | 
			
		||||
import { Layout } from "./components/layout/layout.tsx";
 | 
			
		||||
import { createBrowserRouter, RouterProvider } from "react-router";
 | 
			
		||||
import { LoginPage } from "./pages/login-page.tsx";
 | 
			
		||||
import { LogoutPage } from "./pages/logout-page.tsx";
 | 
			
		||||
import { ContinuePage } from "./pages/continue-page.tsx";
 | 
			
		||||
import { App } from "./App.tsx";
 | 
			
		||||
import { ErrorPage } from "./pages/error-page.tsx";
 | 
			
		||||
import { NotFoundPage } from "./pages/not-found-page.tsx";
 | 
			
		||||
import { UnauthorizedPage } from "./pages/unauthorized-page.tsx";
 | 
			
		||||
import { InternalServerError } from "./pages/internal-server-error.tsx";
 | 
			
		||||
import { ContinuePage } from "./pages/continue-page.tsx";
 | 
			
		||||
import { TotpPage } from "./pages/totp-page.tsx";
 | 
			
		||||
import { AppContextProvider } from "./context/app-context.tsx";
 | 
			
		||||
import "./lib/i18n/i18n.ts";
 | 
			
		||||
import { ForgotPasswordPage } from "./pages/forgot-password-page.tsx";
 | 
			
		||||
 | 
			
		||||
const queryClient = new QueryClient();
 | 
			
		||||
const router = createBrowserRouter([
 | 
			
		||||
  {
 | 
			
		||||
    path: "/",
 | 
			
		||||
    element: <App />,
 | 
			
		||||
    errorElement: <ErrorPage />,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/login",
 | 
			
		||||
    element: <LoginPage />,
 | 
			
		||||
    errorElement: <ErrorPage />,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/continue",
 | 
			
		||||
    element: <ContinuePage />,
 | 
			
		||||
    errorElement: <ErrorPage />,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/totp",
 | 
			
		||||
    element: <TotpPage />,
 | 
			
		||||
    errorElement: <ErrorPage />,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "*",
 | 
			
		||||
    element: <NotFoundPage />,
 | 
			
		||||
    errorElement: <ErrorPage />,
 | 
			
		||||
  },
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
createRoot(document.getElementById("root")!).render(
 | 
			
		||||
  <StrictMode>
 | 
			
		||||
    <MantineProvider defaultColorScheme="auto">
 | 
			
		||||
      <QueryClientProvider client={queryClient}>
 | 
			
		||||
        <Notifications />
 | 
			
		||||
        <AppContextProvider>
 | 
			
		||||
          <UserContextProvider>
 | 
			
		||||
            <BrowserRouter>
 | 
			
		||||
              <Routes>
 | 
			
		||||
                <Route path="/" element={<App />} />
 | 
			
		||||
                <Route path="/login" element={<LoginPage />} />
 | 
			
		||||
                <Route path="/totp" element={<TotpPage />} />
 | 
			
		||||
                <Route path="/logout" element={<LogoutPage />} />
 | 
			
		||||
                <Route path="/continue" element={<ContinuePage />} />
 | 
			
		||||
                <Route path="/unauthorized" element={<UnauthorizedPage />} />
 | 
			
		||||
                <Route path="/error" element={<InternalServerError />} />
 | 
			
		||||
                <Route path="/forgot-password" element={<ForgotPasswordPage />} />
 | 
			
		||||
                <Route path="*" element={<NotFoundPage />} />
 | 
			
		||||
              </Routes>
 | 
			
		||||
            </BrowserRouter>
 | 
			
		||||
          </UserContextProvider>
 | 
			
		||||
        </AppContextProvider>
 | 
			
		||||
      </QueryClientProvider>
 | 
			
		||||
    </MantineProvider>
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <RouterProvider router={router} />
 | 
			
		||||
    </Layout>
 | 
			
		||||
  </StrictMode>,
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,134 +1,120 @@
 | 
			
		||||
import { Button, Code, Paper, Text } from "@mantine/core";
 | 
			
		||||
import { notifications } from "@mantine/notifications";
 | 
			
		||||
import { Navigate } from "react-router";
 | 
			
		||||
import { useUserContext } from "../context/user-context";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { ReactNode } from "react";
 | 
			
		||||
import { escapeRegex, isQueryValid } from "../utils/utils";
 | 
			
		||||
import { useAppContext } from "../context/app-context";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardContent,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
} from "@/components/ui/card";
 | 
			
		||||
import { isValidUrl } from "@/lib/utils";
 | 
			
		||||
import { Trans, useTranslation } from "react-i18next";
 | 
			
		||||
import { Navigate, useNavigate } from "react-router";
 | 
			
		||||
 | 
			
		||||
export const ContinuePage = () => {
 | 
			
		||||
  const queryString = window.location.search;
 | 
			
		||||
  const params = new URLSearchParams(queryString);
 | 
			
		||||
  const redirectUri = params.get("redirect_uri") ?? "";
 | 
			
		||||
 | 
			
		||||
  const { isLoggedIn } = useUserContext();
 | 
			
		||||
  const { disableContinue, domain } = useAppContext();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const params = new URLSearchParams(window.location.search);
 | 
			
		||||
 | 
			
		||||
  if (!isLoggedIn) {
 | 
			
		||||
    return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
 | 
			
		||||
  }
 | 
			
		||||
  const redirectURI = params.get("redirect_uri") ?? "";
 | 
			
		||||
 | 
			
		||||
  if (!isQueryValid(redirectUri)) {
 | 
			
		||||
  //psuedo
 | 
			
		||||
  const domain = "127.0.0.1";
 | 
			
		||||
  const disableContinue = false;
 | 
			
		||||
 | 
			
		||||
  if (redirectURI === "") {
 | 
			
		||||
    return <Navigate to="/" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const redirect = () => {
 | 
			
		||||
    notifications.show({
 | 
			
		||||
      title: t("continueRedirectingTitle"),
 | 
			
		||||
      message: t("continueRedirectingSubtitle"),
 | 
			
		||||
      color: "blue",
 | 
			
		||||
    });
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      window.location.href = redirectUri;
 | 
			
		||||
    }, 500);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  let uri;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    uri = new URL(redirectUri);
 | 
			
		||||
  } catch {
 | 
			
		||||
    return (
 | 
			
		||||
      <ContinuePageLayout>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          {t("Invalid redirect")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>{t("The redirect URL is invalid")}</Text>
 | 
			
		||||
      </ContinuePageLayout>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const regex = new RegExp(`^.*${escapeRegex(domain)}$`)
 | 
			
		||||
 | 
			
		||||
  if (!regex.test(uri.hostname)) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ContinuePageLayout>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          {t("untrustedRedirectTitle")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Trans
 | 
			
		||||
            i18nKey="untrustedRedirectSubtitle"
 | 
			
		||||
            t={t}
 | 
			
		||||
            components={{ Code: <Code /> }}
 | 
			
		||||
            values={{ domain: domain }}
 | 
			
		||||
          />
 | 
			
		||||
        <Button fullWidth mt="xl" color="red" onClick={redirect}>
 | 
			
		||||
          {t('continueTitle')}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button fullWidth mt="sm" color="gray" onClick={() => window.location.href = "/"}>
 | 
			
		||||
          {t('cancelTitle')}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </ContinuePageLayout>
 | 
			
		||||
    )
 | 
			
		||||
  if (!isValidUrl(redirectURI)) {
 | 
			
		||||
    return <Navigate to="/" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (disableContinue) {
 | 
			
		||||
    window.location.href = redirectUri;
 | 
			
		||||
    window.location.href = redirectURI;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const url = new URL(redirectURI);
 | 
			
		||||
 | 
			
		||||
  if (!url.hostname.includes(domain)) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ContinuePageLayout>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          {t("continueRedirectingTitle")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>{t("continueRedirectingSubtitle")}</Text>
 | 
			
		||||
      </ContinuePageLayout>
 | 
			
		||||
      <Card className="min-w-xs md:max-w-sm">
 | 
			
		||||
        <CardHeader>
 | 
			
		||||
          <CardTitle className="text-3xl">
 | 
			
		||||
            {t("untrustedRedirectTitle")}
 | 
			
		||||
          </CardTitle>
 | 
			
		||||
          <CardDescription>
 | 
			
		||||
            <Trans
 | 
			
		||||
              i18nKey="untrustedRedirectSubtitle"
 | 
			
		||||
              t={t}
 | 
			
		||||
              components={{
 | 
			
		||||
                code: (
 | 
			
		||||
                  <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold" />
 | 
			
		||||
                ),
 | 
			
		||||
              }}
 | 
			
		||||
              values={{ domain }}
 | 
			
		||||
            />
 | 
			
		||||
          </CardDescription>
 | 
			
		||||
        </CardHeader>
 | 
			
		||||
        <CardContent className="flex flex-col gap-2 items-stretch">
 | 
			
		||||
          <Button
 | 
			
		||||
            onClick={() => window.location.replace(redirectURI)}
 | 
			
		||||
            variant="destructive"
 | 
			
		||||
          >
 | 
			
		||||
            {t("continueTitle")}
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button onClick={() => navigate("/")} variant="outline">
 | 
			
		||||
            {t("cancelTitle")}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </CardContent>
 | 
			
		||||
      </Card>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (window.location.protocol === "https:" && uri.protocol === "http:") {
 | 
			
		||||
  if (url.protocol === "http:" && window.location.protocol === "https:") {
 | 
			
		||||
    return (
 | 
			
		||||
      <ContinuePageLayout>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          {t("continueInsecureRedirectTitle")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>
 | 
			
		||||
          <Trans
 | 
			
		||||
            i18nKey="continueInsecureRedirectSubtitle"
 | 
			
		||||
            t={t}
 | 
			
		||||
            components={{ Code: <Code /> }}
 | 
			
		||||
          />
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Button fullWidth mt="xl" color="yellow" onClick={redirect}>
 | 
			
		||||
      <Card className="min-w-xs md:max-w-sm">
 | 
			
		||||
        <CardHeader>
 | 
			
		||||
          <CardTitle className="text-3xl">
 | 
			
		||||
            {t("continueInsecureRedirectTitle")}
 | 
			
		||||
          </CardTitle>
 | 
			
		||||
          <CardDescription>
 | 
			
		||||
            <Trans
 | 
			
		||||
              i18nKey="continueInsecureRedirectSubtitle"
 | 
			
		||||
              t={t}
 | 
			
		||||
              components={{
 | 
			
		||||
                code: (
 | 
			
		||||
                  <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold" />
 | 
			
		||||
                ),
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </CardDescription>
 | 
			
		||||
        </CardHeader>
 | 
			
		||||
        <CardContent className="flex flex-col gap-2 items-stretch">
 | 
			
		||||
          <Button
 | 
			
		||||
            onClick={() => window.location.replace(redirectURI)}
 | 
			
		||||
            variant="warning"
 | 
			
		||||
          >
 | 
			
		||||
            {t("continueTitle")}
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button onClick={() => navigate("/")} variant="outline">
 | 
			
		||||
            {t("cancelTitle")}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </CardContent>
 | 
			
		||||
      </Card>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Card className="min-w-xs md:max-w-sm">
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <CardTitle className="text-3xl">{t("continueTitle")}</CardTitle>
 | 
			
		||||
        <CardDescription>{t("continueSubtitle")}</CardDescription>
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
      <CardContent className="flex flex-col items-stretch">
 | 
			
		||||
        <Button onClick={() => window.location.replace(redirectURI)}>
 | 
			
		||||
          {t("continueTitle")}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button fullWidth mt="sm" color="gray" onClick={() => window.location.href = "/"}>
 | 
			
		||||
          {t('cancelTitle')}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </ContinuePageLayout>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ContinuePageLayout>
 | 
			
		||||
      <Text size="xl" fw={700}>
 | 
			
		||||
        {t("continueTitle")}
 | 
			
		||||
      </Text>
 | 
			
		||||
      <Text>{t("continueSubtitle")}</Text>
 | 
			
		||||
      <Button fullWidth mt="xl" onClick={redirect}>
 | 
			
		||||
        {t("continueTitle")}
 | 
			
		||||
      </Button>
 | 
			
		||||
    </ContinuePageLayout>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ContinuePageLayout = ({ children }: { children: ReactNode }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
			
		||||
        {children}
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Layout>
 | 
			
		||||
      </CardContent>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								frontend/src/pages/error-page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
			
		||||
import {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
} from "@/components/ui/card";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
export const ErrorPage = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Card className="min-w-xs md:max-w-sm">
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <CardTitle className="text-3xl">{t("errorTitle")}</CardTitle>
 | 
			
		||||
        <CardDescription>{t("errorSubtitle")}</CardDescription>
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
import { Paper, Text, TypographyStylesProvider } from "@mantine/core";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { useAppContext } from "../context/app-context";
 | 
			
		||||
import Markdown from 'react-markdown'
 | 
			
		||||
 | 
			
		||||
export const ForgotPasswordPage = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const { forgotPasswordMessage } = useAppContext();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          {t("forgotPasswordTitle")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <TypographyStylesProvider>
 | 
			
		||||
            <Markdown>
 | 
			
		||||
                {forgotPasswordMessage}
 | 
			
		||||
            </Markdown>
 | 
			
		||||
        </TypographyStylesProvider>
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
import { Button, Paper, Text } from "@mantine/core";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
export const InternalServerError = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          {t("internalErrorTitle")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>{t("internalErrorSubtitle")}</Text>
 | 
			
		||||
        <Button fullWidth mt="xl" onClick={() => window.location.replace("/")}>
 | 
			
		||||
          {t("internalErrorButton")}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,138 +1,100 @@
 | 
			
		||||
import { Paper, Title, Text, Divider } from "@mantine/core";
 | 
			
		||||
import { notifications } from "@mantine/notifications";
 | 
			
		||||
import { useMutation } from "@tanstack/react-query";
 | 
			
		||||
import axios, { type AxiosError } from "axios";
 | 
			
		||||
import { useUserContext } from "../context/user-context";
 | 
			
		||||
import { Navigate } from "react-router";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { OAuthButtons } from "../components/auth/oauth-buttons";
 | 
			
		||||
import { LoginFormValues } from "../schemas/login-schema";
 | 
			
		||||
import { LoginForm } from "../components/auth/login-forn";
 | 
			
		||||
import { isQueryValid } from "../utils/utils";
 | 
			
		||||
import { useAppContext } from "../context/app-context";
 | 
			
		||||
import { OAuthButton } from "@/components/auth/oauth-button";
 | 
			
		||||
import { GenericIcon } from "@/components/icons/generic";
 | 
			
		||||
import { GithubIcon } from "@/components/icons/github";
 | 
			
		||||
import { GoogleIcon } from "@/components/icons/google";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardContent,
 | 
			
		||||
} from "@/components/ui/card";
 | 
			
		||||
import { Input } from "@/components/ui/input";
 | 
			
		||||
import { Label } from "@/components/ui/label";
 | 
			
		||||
import { Separator } from "@/components/ui/separator";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
export const LoginPage = () => {
 | 
			
		||||
  const queryString = window.location.search;
 | 
			
		||||
  const params = new URLSearchParams(queryString);
 | 
			
		||||
  const redirectUri = params.get("redirect_uri") ?? "";
 | 
			
		||||
 | 
			
		||||
  const { isLoggedIn } = useUserContext();
 | 
			
		||||
  const { configuredProviders, title, genericName } = useAppContext();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const configuredProviders = ["google", "github", "generic", "username"];
 | 
			
		||||
  const title = "Tinyauth";
 | 
			
		||||
 | 
			
		||||
  const oauthProviders = configuredProviders.filter(
 | 
			
		||||
    (value) => value !== "username",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (isLoggedIn) {
 | 
			
		||||
    return <Navigate to="/logout" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const loginMutation = useMutation({
 | 
			
		||||
    mutationFn: (login: LoginFormValues) => {
 | 
			
		||||
      return axios.post("/api/login", login);
 | 
			
		||||
    },
 | 
			
		||||
    onError: (data: AxiosError) => {
 | 
			
		||||
      if (data.response) {
 | 
			
		||||
        if (data.response.status === 429) {
 | 
			
		||||
          notifications.show({
 | 
			
		||||
            title: t("loginFailTitle"),
 | 
			
		||||
            message: t("loginFailRateLimit"),
 | 
			
		||||
            color: "red",
 | 
			
		||||
          });
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: t("loginFailTitle"),
 | 
			
		||||
        message: t("loginFailSubtitle"),
 | 
			
		||||
        color: "red",
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess: async (data) => {
 | 
			
		||||
      if (data.data.totpPending) {
 | 
			
		||||
        window.location.replace(`/totp?redirect_uri=${redirectUri}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: t("loginSuccessTitle"),
 | 
			
		||||
        message: t("loginSuccessSubtitle"),
 | 
			
		||||
        color: "green",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        if (!isQueryValid(redirectUri)) {
 | 
			
		||||
          window.location.replace("/");
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        window.location.replace(`/continue?redirect_uri=${redirectUri}`);
 | 
			
		||||
      }, 500);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const loginOAuthMutation = useMutation({
 | 
			
		||||
    mutationFn: (provider: string) => {
 | 
			
		||||
      return axios.get(
 | 
			
		||||
        `/api/oauth/url/${provider}?redirect_uri=${redirectUri}`,
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    onError: () => {
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: t("loginOauthFailTitle"),
 | 
			
		||||
        message: t("loginOauthFailSubtitle"),
 | 
			
		||||
        color: "red",
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess: (data) => {
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: t("loginOauthSuccessTitle"),
 | 
			
		||||
        message: t("loginOauthSuccessSubtitle"),
 | 
			
		||||
        color: "blue",
 | 
			
		||||
      });
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        window.location.href = data.data.url;
 | 
			
		||||
      }, 500);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = (values: LoginFormValues) => {
 | 
			
		||||
    loginMutation.mutate(values);
 | 
			
		||||
  };
 | 
			
		||||
  const oauthConfigured =
 | 
			
		||||
    configuredProviders.filter((provider) => provider !== "username").length >
 | 
			
		||||
    0;
 | 
			
		||||
  const userAuthConfigured = configuredProviders.includes("username");
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Title ta="center">{title}</Title>
 | 
			
		||||
      <Paper shadow="md" p="xl" mt={30} radius="md" withBorder>
 | 
			
		||||
        {oauthProviders.length > 0 && (
 | 
			
		||||
          <>
 | 
			
		||||
            <Text size="lg" fw={500} ta="center">
 | 
			
		||||
              {t("loginTitle")}
 | 
			
		||||
            </Text>
 | 
			
		||||
            <OAuthButtons
 | 
			
		||||
              oauthProviders={oauthProviders}
 | 
			
		||||
              isPending={loginOAuthMutation.isPending}
 | 
			
		||||
              mutate={loginOAuthMutation.mutate}
 | 
			
		||||
              genericName={genericName}
 | 
			
		||||
            />
 | 
			
		||||
            {configuredProviders.includes("username") && (
 | 
			
		||||
              <Divider
 | 
			
		||||
                label={t("loginDivider")}
 | 
			
		||||
                labelPosition="center"
 | 
			
		||||
                my="lg"
 | 
			
		||||
              />
 | 
			
		||||
    <Card className="max-w-xs md:max-w-sm">
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <CardTitle className="text-center text-3xl">{title}</CardTitle>
 | 
			
		||||
        {configuredProviders.length > 0 && (
 | 
			
		||||
          <CardDescription className="text-center">
 | 
			
		||||
            {oauthConfigured ? t("loginTitle") : t("loginTitleSimple")}
 | 
			
		||||
          </CardDescription>
 | 
			
		||||
        )}
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
      <CardContent className="flex flex-col gap-4">
 | 
			
		||||
        {oauthConfigured && (
 | 
			
		||||
          <div className="flex flex-row gap-3 flex-wrap items-center justify-center">
 | 
			
		||||
            {configuredProviders.includes("google") && (
 | 
			
		||||
              <OAuthButton title="Google" icon={<GoogleIcon />} />
 | 
			
		||||
            )}
 | 
			
		||||
          </>
 | 
			
		||||
            {configuredProviders.includes("github") && (
 | 
			
		||||
              <OAuthButton title="Github" icon={<GithubIcon />} />
 | 
			
		||||
            )}
 | 
			
		||||
            {configuredProviders.includes("generic") && (
 | 
			
		||||
              <OAuthButton title="Generic" icon={<GenericIcon />} />
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {configuredProviders.includes("username") && (
 | 
			
		||||
          <LoginForm
 | 
			
		||||
            isPending={loginMutation.isPending}
 | 
			
		||||
            onSubmit={handleSubmit}
 | 
			
		||||
          />
 | 
			
		||||
        {userAuthConfigured && oauthConfigured && (
 | 
			
		||||
          <div className="flex items-center gap-4">
 | 
			
		||||
            <Separator className="flex-1" />
 | 
			
		||||
            <span className="text-sm text-muted-foreground">
 | 
			
		||||
              {t("loginDivider")}
 | 
			
		||||
            </span>
 | 
			
		||||
            <Separator className="flex-1" />
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Layout>
 | 
			
		||||
        {userAuthConfigured && (
 | 
			
		||||
          <div className="flex flex-col gap-4">
 | 
			
		||||
            <div>
 | 
			
		||||
              <Label htmlFor="#username">{t("loginUsername")}</Label>
 | 
			
		||||
              <Input
 | 
			
		||||
                id="username"
 | 
			
		||||
                placeholder={t("loginUsername")}
 | 
			
		||||
                className="mt-2"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
              <Label htmlFor="#password">
 | 
			
		||||
                <div className="flex flex-row min-w-full items-center justify-between">
 | 
			
		||||
                  <span>{t("loginPassword")}</span>
 | 
			
		||||
                  <a
 | 
			
		||||
                    href="/forgot"
 | 
			
		||||
                    className="text-muted-foreground font-normal"
 | 
			
		||||
                  >
 | 
			
		||||
                    {t("forgotPasswordTitle")}
 | 
			
		||||
                  </a>
 | 
			
		||||
                </div>
 | 
			
		||||
              </Label>
 | 
			
		||||
              <Input
 | 
			
		||||
                id="password"
 | 
			
		||||
                placeholder={t("loginPassword")}
 | 
			
		||||
                className="mt-2"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <Button>{t("loginSubmit")}</Button>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {configuredProviders.length == 0 && (
 | 
			
		||||
          <h3 className="text-center text-xl text-red-600">
 | 
			
		||||
            {t("failedToFetchProvidersTitle")}
 | 
			
		||||
          </h3>
 | 
			
		||||
        )}
 | 
			
		||||
      </CardContent>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,84 +0,0 @@
 | 
			
		||||
import { Button, Code, Paper, Text } from "@mantine/core";
 | 
			
		||||
import { notifications } from "@mantine/notifications";
 | 
			
		||||
import { useMutation } from "@tanstack/react-query";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { useUserContext } from "../context/user-context";
 | 
			
		||||
import { Navigate } from "react-router";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { capitalize } from "../utils/utils";
 | 
			
		||||
import { useAppContext } from "../context/app-context";
 | 
			
		||||
import { Trans, useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
export const LogoutPage = () => {
 | 
			
		||||
  const { isLoggedIn, username, oauth, provider } = useUserContext();
 | 
			
		||||
  const { genericName } = useAppContext();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  if (!isLoggedIn) {
 | 
			
		||||
    return <Navigate to="/login" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const logoutMutation = useMutation({
 | 
			
		||||
    mutationFn: () => {
 | 
			
		||||
      return axios.post("/api/logout");
 | 
			
		||||
    },
 | 
			
		||||
    onError: () => {
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: t("logoutFailTitle"),
 | 
			
		||||
        message: t("logoutFailSubtitle"),
 | 
			
		||||
        color: "red",
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess: () => {
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: t("logoutSuccessTitle"),
 | 
			
		||||
        message: t("logoutSuccessSubtitle"),
 | 
			
		||||
        color: "green",
 | 
			
		||||
      });
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        window.location.replace("/login");
 | 
			
		||||
      }, 500);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          {t("logoutTitle")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>
 | 
			
		||||
          {oauth ? (
 | 
			
		||||
            <Trans
 | 
			
		||||
              i18nKey="logoutOauthSubtitle"
 | 
			
		||||
              t={t}
 | 
			
		||||
              components={{ Code: <Code /> }}
 | 
			
		||||
              values={{
 | 
			
		||||
                provider:
 | 
			
		||||
                  provider === "generic" ? genericName : capitalize(provider),
 | 
			
		||||
                username: username,
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Trans
 | 
			
		||||
              i18nKey="logoutUsernameSubtitle"
 | 
			
		||||
              t={t}
 | 
			
		||||
              components={{ Code: <Code /> }}
 | 
			
		||||
              values={{
 | 
			
		||||
                username: username,
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Button
 | 
			
		||||
          fullWidth
 | 
			
		||||
          mt="xl"
 | 
			
		||||
          onClick={() => logoutMutation.mutate()}
 | 
			
		||||
          loading={logoutMutation.isPending}
 | 
			
		||||
        >
 | 
			
		||||
          {t("logoutTitle")}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,20 +1,27 @@
 | 
			
		||||
import { Button, Paper, Text } from "@mantine/core";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardContent,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
} from "@/components/ui/card";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { useNavigate } from "react-router";
 | 
			
		||||
 | 
			
		||||
export const NotFoundPage = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          {t("notFoundTitle")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>{t("notFoundSubtitle")}</Text>
 | 
			
		||||
        <Button fullWidth mt="xl" onClick={() => window.location.replace("/")}>
 | 
			
		||||
          {t("notFoundButton")}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Layout>
 | 
			
		||||
    <Card className="min-w-xs md:max-w-sm">
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <CardTitle className="text-3xl">{t("notFoundTitle")}</CardTitle>
 | 
			
		||||
        <CardDescription>{t("notFoundSubtitle")}</CardDescription>
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
      <CardContent className="flex flex-col items-stretch">
 | 
			
		||||
        <Button onClick={() => navigate("/")}>{t("notFoundButton")}</Button>
 | 
			
		||||
      </CardContent>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,66 +1,44 @@
 | 
			
		||||
import { Navigate } from "react-router";
 | 
			
		||||
import { useUserContext } from "../context/user-context";
 | 
			
		||||
import { Title, Paper, Text } from "@mantine/core";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { TotpForm } from "../components/auth/totp-form";
 | 
			
		||||
import { useMutation } from "@tanstack/react-query";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { notifications } from "@mantine/notifications";
 | 
			
		||||
import { useAppContext } from "../context/app-context";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardContent,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
} from "@/components/ui/card";
 | 
			
		||||
import {
 | 
			
		||||
  InputOTP,
 | 
			
		||||
  InputOTPGroup,
 | 
			
		||||
  InputOTPSeparator,
 | 
			
		||||
  InputOTPSlot,
 | 
			
		||||
} from "@/components/ui/input-otp";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
export const TotpPage = () => {
 | 
			
		||||
  const queryString = window.location.search;
 | 
			
		||||
  const params = new URLSearchParams(queryString);
 | 
			
		||||
  const redirectUri = params.get("redirect_uri") ?? "";
 | 
			
		||||
 | 
			
		||||
  const { totpPending, isLoggedIn } = useUserContext();
 | 
			
		||||
  const { title } = useAppContext();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  if (isLoggedIn) {
 | 
			
		||||
    return <Navigate to={`/logout`} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!totpPending) {
 | 
			
		||||
    return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const totpMutation = useMutation({
 | 
			
		||||
    mutationFn: async (totp: { code: string }) => {
 | 
			
		||||
      await axios.post("/api/totp", totp);
 | 
			
		||||
    },
 | 
			
		||||
    onError: () => {
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: t("totpFailTitle"),
 | 
			
		||||
        message: t("totpFailSubtitle"),
 | 
			
		||||
        color: "red",
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess: () => {
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: t("totpSuccessTitle"),
 | 
			
		||||
        message: t("totpSuccessSubtitle"),
 | 
			
		||||
        color: "green",
 | 
			
		||||
      });
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        window.location.replace(`/continue?redirect_uri=${redirectUri}`);
 | 
			
		||||
      }, 500);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Title ta="center">{title}</Title>
 | 
			
		||||
      <Paper shadow="md" p="xl" mt={30} radius="md" withBorder>
 | 
			
		||||
        <Text size="lg" fw={500} mb="md" ta="center">
 | 
			
		||||
          {t("totpTitle")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <TotpForm
 | 
			
		||||
          isPending={totpMutation.isPending}
 | 
			
		||||
          onSubmit={(values) => totpMutation.mutate(values)}
 | 
			
		||||
        />
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Layout>
 | 
			
		||||
    <Card className="min-w-xs md:max-w-sm">
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <CardTitle className="text-3xl">{t("totpTitle")}</CardTitle>
 | 
			
		||||
        <CardDescription>{t("totpSubtitle")}</CardDescription>
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
      <CardContent className="flex flex-col gap-6 items-stretch">
 | 
			
		||||
        <InputOTP maxLength={6}>
 | 
			
		||||
          <InputOTPGroup>
 | 
			
		||||
            <InputOTPSlot index={0} />
 | 
			
		||||
            <InputOTPSlot index={1} />
 | 
			
		||||
            <InputOTPSlot index={2} />
 | 
			
		||||
          </InputOTPGroup>
 | 
			
		||||
          <InputOTPSeparator />
 | 
			
		||||
          <InputOTPGroup>
 | 
			
		||||
            <InputOTPSlot index={3} />
 | 
			
		||||
            <InputOTPSlot index={4} />
 | 
			
		||||
            <InputOTPSlot index={5} />
 | 
			
		||||
          </InputOTPGroup>
 | 
			
		||||
        </InputOTP>
 | 
			
		||||
        <Button>{t("continueTitle")}</Button>
 | 
			
		||||
      </CardContent>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,56 +0,0 @@
 | 
			
		||||
import { Button, Code, Paper, Text } from "@mantine/core";
 | 
			
		||||
import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { Navigate } from "react-router";
 | 
			
		||||
import { isQueryValid } from "../utils/utils";
 | 
			
		||||
import { Trans, useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
export const UnauthorizedPage = () => {
 | 
			
		||||
  const queryString = window.location.search;
 | 
			
		||||
  const params = new URLSearchParams(queryString);
 | 
			
		||||
  const username = params.get("username") ?? "";
 | 
			
		||||
  const resource = params.get("resource") ?? "";
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  if (!isQueryValid(username)) {
 | 
			
		||||
    return <Navigate to="/" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
			
		||||
        <Text size="xl" fw={700}>
 | 
			
		||||
          {t("Unauthorized")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>
 | 
			
		||||
          {isQueryValid(resource) ? (
 | 
			
		||||
            <Text>
 | 
			
		||||
              <Trans
 | 
			
		||||
                i18nKey="unauthorizedResourceSubtitle"
 | 
			
		||||
                t={t}
 | 
			
		||||
                components={{ Code: <Code /> }}
 | 
			
		||||
                values={{ resource, username }}
 | 
			
		||||
              />
 | 
			
		||||
            </Text>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Text>
 | 
			
		||||
              <Trans
 | 
			
		||||
                i18nKey="unaothorizedLoginSubtitle"
 | 
			
		||||
                t={t}
 | 
			
		||||
                components={{ Code: <Code /> }}
 | 
			
		||||
                values={{ username }}
 | 
			
		||||
              />
 | 
			
		||||
            </Text>
 | 
			
		||||
          )}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Button
 | 
			
		||||
          fullWidth
 | 
			
		||||
          mt="xl"
 | 
			
		||||
          onClick={() => window.location.replace("/login")}
 | 
			
		||||
        >
 | 
			
		||||
          {t("unauthorizedButton")}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
 | 
			
		||||
export const appContextSchema = z.object({
 | 
			
		||||
  configuredProviders: z.array(z.string()),
 | 
			
		||||
  disableContinue: z.boolean(),
 | 
			
		||||
  title: z.string(),
 | 
			
		||||
  genericName: z.string(),
 | 
			
		||||
  domain: z.string(),
 | 
			
		||||
  forgotPasswordMessage: z.string(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export type AppContextSchemaType = z.infer<typeof appContextSchema>;
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
 | 
			
		||||
export const loginSchema = z.object({
 | 
			
		||||
  username: z.string(),
 | 
			
		||||
  password: z.string(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export type LoginFormValues = z.infer<typeof loginSchema>;
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
 | 
			
		||||
export const userContextSchema = z.object({
 | 
			
		||||
  isLoggedIn: z.boolean(),
 | 
			
		||||
  username: z.string(),
 | 
			
		||||
  oauth: z.boolean(),
 | 
			
		||||
  provider: z.string(),
 | 
			
		||||
  totpPending: z.boolean(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export type UserContextSchemaType = z.infer<typeof userContextSchema>;
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
 | 
			
		||||
export const isQueryValid = (value: string) => value.trim() !== "" && value !== "null";
 | 
			
		||||
export const escapeRegex = (value: string) => value.replace(/[-\/\\^$.*+?()[\]{}|]/g, "\\$&");
 | 
			
		||||
@@ -1,5 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    // Resolve paths
 | 
			
		||||
    "baseUrl": ".",
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 | 
			
		||||
    "target": "ES2020",
 | 
			
		||||
    "useDefineForClassFields": true,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,5 +3,11 @@
 | 
			
		||||
  "references": [
 | 
			
		||||
    { "path": "./tsconfig.app.json" },
 | 
			
		||||
    { "path": "./tsconfig.node.json" }
 | 
			
		||||
  ]
 | 
			
		||||
  ],
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "baseUrl": ".",
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,14 @@
 | 
			
		||||
import { defineConfig } from "vite";
 | 
			
		||||
import react from "@vitejs/plugin-react-swc";
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import react from '@vitejs/plugin-react'
 | 
			
		||||
import path from "path"
 | 
			
		||||
import tailwindcss from "@tailwindcss/vite"
 | 
			
		||||
 | 
			
		||||
// https://vite.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [react()],
 | 
			
		||||
  server: {
 | 
			
		||||
    host: "0.0.0.0",
 | 
			
		||||
    proxy: {
 | 
			
		||||
      "/api": {
 | 
			
		||||
        target: "http://tinyauth-backend:3000/api",
 | 
			
		||||
        changeOrigin: true,
 | 
			
		||||
        rewrite: (path) => path.replace(/^\/api/, ""),
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
  plugins: [react(), tailwindcss()],
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      "@": path.resolve(__dirname, "./src"),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||