From 508ba07848e0eeda0ce560e1b1be643cd306fd32 Mon Sep 17 00:00:00 2001 From: Marc Robin Richter Date: Mon, 16 Mar 2026 11:41:31 +0100 Subject: [PATCH] master piece --- .claude/settings.local.json | 8 + index.html | 2 +- package.json | 8 +- pnpm-lock.yaml | 827 ++++++++++++++++++++++++++--- public/ascii/logo.txt | 6 + src/App.css | 184 ------- src/App.tsx | 153 ++---- src/commands/handlers.ts | 222 ++++++++ src/components/BootSequence.ts | 104 ++++ src/components/BrickTransition.tsx | 143 +++++ src/components/Bricked.tsx | 103 ++++ src/components/CommandPrompt.tsx | 152 ++++++ src/components/CrtOverlay.tsx | 51 ++ src/components/MatrixRain.tsx | 240 +++++++++ src/components/Terminal.tsx | 32 ++ src/components/TerminalLine.tsx | 39 ++ src/hooks/useTerminalSequence.ts | 32 ++ src/hooks/useTypewriter.ts | 26 + src/index.css | 120 +---- src/shaders/crt.frag.ts | 23 + src/shaders/crt.vert.ts | 7 + src/utils/loadAsciiArt.ts | 5 + vite.config.ts | 3 +- 23 files changed, 2024 insertions(+), 466 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 public/ascii/logo.txt delete mode 100644 src/App.css create mode 100644 src/commands/handlers.ts create mode 100644 src/components/BootSequence.ts create mode 100644 src/components/BrickTransition.tsx create mode 100644 src/components/Bricked.tsx create mode 100644 src/components/CommandPrompt.tsx create mode 100644 src/components/CrtOverlay.tsx create mode 100644 src/components/MatrixRain.tsx create mode 100644 src/components/Terminal.tsx create mode 100644 src/components/TerminalLine.tsx create mode 100644 src/hooks/useTerminalSequence.ts create mode 100644 src/hooks/useTypewriter.ts create mode 100644 src/shaders/crt.frag.ts create mode 100644 src/shaders/crt.vert.ts create mode 100644 src/utils/loadAsciiArt.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..cae7b22 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm add:*)", + "Bash(pnpm build:*)" + ] + } +} diff --git a/index.html b/index.html index d833018..865917f 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - claude-test + 503 - Stift15
diff --git a/package.json b/package.json index 5c108cb..4d2da7f 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,20 @@ "preview": "vite preview" }, "dependencies": { + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", + "@tailwindcss/vite": "^4.2.1", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "tailwindcss": "^4.2.1", + "three": "^0.183.2" }, "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^24.12.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/three": "^0.183.1", "@vitejs/plugin-react": "^6.0.0", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a95482a..3d08366 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,12 +5,27 @@ settings: excludeLinksFromLockfile: false dependencies: + '@react-three/drei': + specifier: ^10.7.7 + version: 10.7.7(@react-three/fiber@9.5.0)(@types/react@19.2.14)(@types/three@0.183.1)(react-dom@19.2.4)(react@19.2.4)(three@0.183.2) + '@react-three/fiber': + specifier: ^9.5.0 + version: 9.5.0(@types/react@19.2.14)(react-dom@19.2.4)(react@19.2.4)(three@0.183.2) + '@tailwindcss/vite': + specifier: ^4.2.1 + version: 4.2.1(vite@8.0.0) react: specifier: ^19.2.4 version: 19.2.4 react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) + tailwindcss: + specifier: ^4.2.1 + version: 4.2.1 + three: + specifier: ^0.183.2 + version: 0.183.2 devDependencies: '@eslint/js': @@ -25,6 +40,9 @@ devDependencies: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) + '@types/three': + specifier: ^0.183.1 + version: 0.183.1 '@vitejs/plugin-react': specifier: ^6.0.0 version: 6.0.1(vite@8.0.0) @@ -171,6 +189,11 @@ packages: '@babel/types': 7.29.0 dev: true + /@babel/runtime@7.28.6: + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + dev: false + /@babel/template@7.28.6: resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -203,13 +226,15 @@ packages: '@babel/helper-validator-identifier': 7.28.5 dev: true + /@dimforge/rapier3d-compat@0.12.0: + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + /@emnapi/core@1.9.0: resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} requiresBuild: true dependencies: '@emnapi/wasi-threads': 1.2.0 tslib: 2.8.1 - dev: true optional: true /@emnapi/runtime@1.9.0: @@ -217,7 +242,6 @@ packages: requiresBuild: true dependencies: tslib: 2.8.1 - dev: true optional: true /@emnapi/wasi-threads@1.2.0: @@ -225,7 +249,6 @@ packages: requiresBuild: true dependencies: tslib: 2.8.1 - dev: true optional: true /@eslint-community/eslint-utils@4.9.1(eslint@9.39.4): @@ -331,30 +354,38 @@ packages: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - dev: true /@jridgewell/remapping@2.3.5: resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - dev: true /@jridgewell/resolve-uri@3.1.2: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - dev: true /@jridgewell/sourcemap-codec@1.5.5: resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - dev: true /@jridgewell/trace-mapping@0.3.31: resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - dev: true + + /@mediapipe/tasks-vision@0.10.17: + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + dev: false + + /@monogrid/gainmap-js@3.4.0(three@0.183.2): + resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==} + peerDependencies: + three: '>= 0.159.0' + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.183.2 + dev: false /@napi-rs/wasm-runtime@1.1.1: resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} @@ -363,17 +394,99 @@ packages: '@emnapi/core': 1.9.0 '@emnapi/runtime': 1.9.0 '@tybys/wasm-util': 0.10.1 - dev: true optional: true /@oxc-project/runtime@0.115.0: resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} engines: {node: ^20.19.0 || >=22.12.0} - dev: true /@oxc-project/types@0.115.0: resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} - dev: true + + /@react-three/drei@10.7.7(@react-three/fiber@9.5.0)(@types/react@19.2.14)(@types/three@0.183.1)(react-dom@19.2.4)(react@19.2.4)(three@0.183.2): + resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==} + peerDependencies: + '@react-three/fiber': ^9.0.0 + react: ^19 + react-dom: ^19 + three: '>=0.159' + peerDependenciesMeta: + react-dom: + optional: true + dependencies: + '@babel/runtime': 7.28.6 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.4.0(three@0.183.2) + '@react-three/fiber': 9.5.0(@types/react@19.2.14)(react-dom@19.2.4)(react@19.2.4)(three@0.183.2) + '@use-gesture/react': 10.3.1(react@19.2.4) + camera-controls: 3.1.2(three@0.183.2) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.15 + maath: 0.10.8(@types/three@0.183.1)(three@0.183.2) + meshline: 3.3.1(three@0.183.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + stats-gl: 2.4.2(@types/three@0.183.1)(three@0.183.2) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@19.2.4) + three: 0.183.2 + three-mesh-bvh: 0.8.3(three@0.183.2) + three-stdlib: 2.36.1(three@0.183.2) + troika-three-text: 0.52.4(three@0.183.2) + tunnel-rat: 0.1.2(@types/react@19.2.14)(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + utility-types: 3.11.0 + zustand: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + dev: false + + /@react-three/fiber@9.5.0(@types/react@19.2.14)(react-dom@19.2.4)(react@19.2.4)(three@0.183.2): + resolution: {integrity: sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: '>=19 <19.3' + react-dom: '>=19 <19.3' + react-native: '>=0.78' + three: '>=0.156' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.28.6 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 2.0.0(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-use-measure: 2.1.7(react-dom@19.2.4)(react@19.2.4) + scheduler: 0.27.0 + suspend-react: 0.1.3(react@19.2.4) + three: 0.183.2 + use-sync-external-store: 1.6.0(react@19.2.4) + zustand: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0) + transitivePeerDependencies: + - '@types/react' + - immer + dev: false /@rolldown/binding-android-arm64@1.0.0-rc.9: resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} @@ -381,7 +494,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: true optional: true /@rolldown/binding-darwin-arm64@1.0.0-rc.9: @@ -390,7 +502,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: true optional: true /@rolldown/binding-darwin-x64@1.0.0-rc.9: @@ -399,7 +510,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: true optional: true /@rolldown/binding-freebsd-x64@1.0.0-rc.9: @@ -408,7 +518,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: true optional: true /@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9: @@ -417,7 +526,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: true optional: true /@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9: @@ -426,7 +534,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /@rolldown/binding-linux-arm64-musl@1.0.0-rc.9: @@ -435,7 +542,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9: @@ -444,7 +550,6 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: true optional: true /@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9: @@ -453,7 +558,6 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: true optional: true /@rolldown/binding-linux-x64-gnu@1.0.0-rc.9: @@ -462,7 +566,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /@rolldown/binding-linux-x64-musl@1.0.0-rc.9: @@ -471,7 +574,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /@rolldown/binding-openharmony-arm64@1.0.0-rc.9: @@ -480,7 +582,6 @@ packages: cpu: [arm64] os: [openharmony] requiresBuild: true - dev: true optional: true /@rolldown/binding-wasm32-wasi@1.0.0-rc.9: @@ -490,7 +591,6 @@ packages: requiresBuild: true dependencies: '@napi-rs/wasm-runtime': 1.1.1 - dev: true optional: true /@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9: @@ -499,7 +599,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: true optional: true /@rolldown/binding-win32-x64-msvc@1.0.0-rc.9: @@ -508,7 +607,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: true optional: true /@rolldown/pluginutils@1.0.0-rc.7: @@ -517,16 +615,176 @@ packages: /@rolldown/pluginutils@1.0.0-rc.9: resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} - dev: true + + /@tailwindcss/node@4.2.1: + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + dev: false + + /@tailwindcss/oxide-android-arm64@4.2.1: + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-darwin-arm64@4.2.1: + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-darwin-x64@4.2.1: + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-freebsd-x64@4.2.1: + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1: + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-linux-arm64-gnu@4.2.1: + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-linux-arm64-musl@4.2.1: + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-linux-x64-gnu@4.2.1: + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-linux-x64-musl@4.2.1: + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-wasm32-wasi@4.2.1: + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + requiresBuild: true + dev: false + optional: true + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + /@tailwindcss/oxide-win32-arm64-msvc@4.2.1: + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-win32-x64-msvc@4.2.1: + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide@4.2.1: + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + dev: false + + /@tailwindcss/vite@4.2.1(vite@8.0.0): + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 8.0.0(@types/node@24.12.0) + dev: false + + /@tweenjs/tween.js@23.1.3: + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} /@tybys/wasm-util@0.10.1: resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} requiresBuild: true dependencies: tslib: 2.8.1 - dev: true optional: true + /@types/draco3d@1.4.10: + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + dev: false + /@types/estree@1.0.8: resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} dev: true @@ -539,7 +797,10 @@ packages: resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} dependencies: undici-types: 7.16.0 - dev: true + + /@types/offscreencanvas@2019.7.3: + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + dev: false /@types/react-dom@19.2.3(@types/react@19.2.14): resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} @@ -549,11 +810,35 @@ packages: '@types/react': 19.2.14 dev: true + /@types/react-reconciler@0.28.9(@types/react@19.2.14): + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + dependencies: + '@types/react': 19.2.14 + dev: false + /@types/react@19.2.14: resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} dependencies: csstype: 3.2.3 - dev: true + + /@types/stats.js@0.17.4: + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + /@types/three@0.183.1: + resolution: {integrity: sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==} + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + '@webgpu/types': 0.1.69 + fflate: 0.8.2 + meshoptimizer: 1.0.1 + + /@types/webxr@0.5.24: + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} /@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0)(eslint@9.39.4)(typescript@5.9.3): resolution: {integrity: sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==} @@ -695,6 +980,19 @@ packages: eslint-visitor-keys: 5.0.1 dev: true + /@use-gesture/core@10.3.1: + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + dev: false + + /@use-gesture/react@10.3.1(react@19.2.4): + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + dependencies: + '@use-gesture/core': 10.3.1 + react: 19.2.4 + dev: false + /@vitejs/plugin-react@6.0.1(vite@8.0.0): resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -712,6 +1010,9 @@ packages: vite: 8.0.0(@types/node@24.12.0) dev: true + /@webgpu/types@0.1.69: + resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} + /acorn-jsx@5.3.2(acorn@8.16.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -755,12 +1056,22 @@ packages: engines: {node: 18 || 20 || >=22} dev: true + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + /baseline-browser-mapping@2.10.8: resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==} engines: {node: '>=6.0.0'} hasBin: true dev: true + /bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + dependencies: + require-from-string: 2.0.2 + dev: false + /brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} dependencies: @@ -787,11 +1098,27 @@ packages: update-browserslist-db: 1.2.3(browserslist@4.28.1) dev: true + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} dev: true + /camera-controls@3.1.2(three@0.183.2): + resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} + engines: {node: '>=22.0.0', npm: '>=10.5.1'} + peerDependencies: + three: '>=0.126.1' + dependencies: + three: 0.183.2 + dev: false + /caniuse-lite@1.0.30001779: resolution: {integrity: sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==} dev: true @@ -823,6 +1150,14 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true + /cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + dependencies: + cross-spawn: 7.0.6 + dev: false + /cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -830,11 +1165,9 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: true /csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - dev: true /debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -852,15 +1185,32 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + dependencies: + webgl-constants: 1.1.1 + dev: false + /detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dev: true + + /draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + dev: false /electron-to-chromium@1.5.313: resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} dev: true + /enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + dev: false + /escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1021,7 +1371,13 @@ packages: optional: true dependencies: picomatch: 4.0.3 - dev: true + + /fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + dev: false + + /fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} /file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} @@ -1055,7 +1411,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true - dev: true optional: true /gensync@1.0.0-beta.2: @@ -1080,6 +1435,14 @@ packages: engines: {node: '>=18'} dev: true + /glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + dev: false + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: false + /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1095,6 +1458,14 @@ packages: hermes-estree: 0.25.1 dev: true + /hls.js@1.6.15: + resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} + dev: false + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + /ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1105,6 +1476,10 @@ packages: engines: {node: '>= 4'} dev: true + /immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + dev: false + /import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1130,9 +1505,28 @@ packages: is-extglob: 2.1.1 dev: true + /is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + dev: false + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true + + /its-fine@2.0.0(@types/react@19.2.14)(react@19.2.4): + resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} + peerDependencies: + react: ^19.0.0 + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.14) + react: 19.2.4 + transitivePeerDependencies: + - '@types/react' + dev: false + + /jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + dev: false /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1183,13 +1577,36 @@ packages: type-check: 0.4.0 dev: true + /lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + dependencies: + immediate: 3.0.6 + dev: false + + /lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + /lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] requiresBuild: true - dev: true + optional: true + + /lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false optional: true /lightningcss-darwin-arm64@1.32.0: @@ -1198,7 +1615,15 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: true + optional: true + + /lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false optional: true /lightningcss-darwin-x64@1.32.0: @@ -1207,7 +1632,15 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: true + optional: true + + /lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false optional: true /lightningcss-freebsd-x64@1.32.0: @@ -1216,7 +1649,15 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: true + optional: true + + /lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false optional: true /lightningcss-linux-arm-gnueabihf@1.32.0: @@ -1225,7 +1666,15 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: true + optional: true + + /lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false optional: true /lightningcss-linux-arm64-gnu@1.32.0: @@ -1234,7 +1683,15 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true + optional: true + + /lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false optional: true /lightningcss-linux-arm64-musl@1.32.0: @@ -1243,7 +1700,15 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true + optional: true + + /lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false optional: true /lightningcss-linux-x64-gnu@1.32.0: @@ -1252,7 +1717,15 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true + optional: true + + /lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false optional: true /lightningcss-linux-x64-musl@1.32.0: @@ -1261,7 +1734,15 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true + optional: true + + /lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false optional: true /lightningcss-win32-arm64-msvc@1.32.0: @@ -1270,7 +1751,15 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: true + optional: true + + /lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false optional: true /lightningcss-win32-x64-msvc@1.32.0: @@ -1279,9 +1768,27 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: true optional: true + /lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + dev: false + /lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} @@ -1299,7 +1806,6 @@ packages: lightningcss-linux-x64-musl: 1.32.0 lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - dev: true /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -1318,6 +1824,33 @@ packages: yallist: 3.1.1 dev: true + /maath@0.10.8(@types/three@0.183.1)(three@0.183.2): + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + dependencies: + '@types/three': 0.183.1 + three: 0.183.2 + dev: false + + /magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + dev: false + + /meshline@3.3.1(three@0.183.2): + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + dependencies: + three: 0.183.2 + dev: false + + /meshoptimizer@1.0.1: + resolution: {integrity: sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==} + /minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -1339,7 +1872,6 @@ packages: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: true /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1390,16 +1922,13 @@ packages: /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - dev: true /picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - dev: true /picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - dev: true /postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} @@ -1408,13 +1937,23 @@ packages: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - dev: true + + /potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + dev: false /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} dev: true + /promise-worker-transferable@1.0.4: + resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + dependencies: + is-promise: 2.2.2 + lie: 3.3.0 + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1429,11 +1968,29 @@ packages: scheduler: 0.27.0 dev: false + /react-use-measure@2.1.7(react-dom@19.2.4)(react@19.2.4): + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + dev: false + /react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} dev: false + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1462,7 +2019,6 @@ packages: '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 - dev: true /scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1484,17 +2040,28 @@ packages: engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 - dev: true /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - dev: true /source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - dev: true + + /stats-gl@2.4.2(@types/three@0.183.1)(three@0.183.2): + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + dependencies: + '@types/three': 0.183.1 + three: 0.183.2 + dev: false + + /stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + dev: false /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -1508,13 +2075,79 @@ packages: has-flag: 4.0.0 dev: true + /suspend-react@0.1.3(react@19.2.4): + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + dependencies: + react: 19.2.4 + dev: false + + /tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + dev: false + + /tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + dev: false + + /three-mesh-bvh@0.8.3(three@0.183.2): + resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} + peerDependencies: + three: '>= 0.159.0' + dependencies: + three: 0.183.2 + dev: false + + /three-stdlib@2.36.1(three@0.183.2): + resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==} + peerDependencies: + three: '>=0.128.0' + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.24 + draco3d: 1.5.7 + fflate: 0.6.10 + potpack: 1.0.2 + three: 0.183.2 + dev: false + + /three@0.183.2: + resolution: {integrity: sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==} + dev: false + /tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - dev: true + + /troika-three-text@0.52.4(three@0.183.2): + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} + peerDependencies: + three: '>=0.125.0' + dependencies: + bidi-js: 1.0.3 + three: 0.183.2 + troika-three-utils: 0.52.4(three@0.183.2) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + dev: false + + /troika-three-utils@0.52.4(three@0.183.2): + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} + peerDependencies: + three: '>=0.125.0' + dependencies: + three: 0.183.2 + dev: false + + /troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + dev: false /ts-api-utils@2.4.0(typescript@5.9.3): resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} @@ -1528,9 +2161,18 @@ packages: /tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} requiresBuild: true - dev: true optional: true + /tunnel-rat@0.1.2(@types/react@19.2.14)(react@19.2.4): + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + dependencies: + zustand: 4.5.7(@types/react@19.2.14)(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - immer + - react + dev: false + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1563,7 +2205,6 @@ packages: /undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - dev: true /update-browserslist-db@1.2.3(browserslist@4.28.1): resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} @@ -1582,6 +2223,19 @@ packages: punycode: 2.3.1 dev: true + /use-sync-external-store@1.6.0(react@19.2.4): + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + dependencies: + react: 19.2.4 + dev: false + + /utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + dev: false + /vite@8.0.0(@types/node@24.12.0): resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1634,7 +2288,14 @@ packages: tinyglobby: 0.2.15 optionalDependencies: fsevents: 2.3.3 - dev: true + + /webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + dev: false + + /webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + dev: false /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -1642,7 +2303,6 @@ packages: hasBin: true dependencies: isexe: 2.0.0 - dev: true /word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} @@ -1670,3 +2330,46 @@ packages: /zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} dev: true + + /zustand@4.5.7(@types/react@19.2.14)(react@19.2.4): + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': 19.2.14 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + dev: false + + /zustand@5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0): + resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + dependencies: + '@types/react': 19.2.14 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + dev: false diff --git a/public/ascii/logo.txt b/public/ascii/logo.txt new file mode 100644 index 0000000..e2ce14d --- /dev/null +++ b/public/ascii/logo.txt @@ -0,0 +1,6 @@ + _____ _______ _____ ______ _______ __ _____ +/ ____|__ __|_ _| ____|__ __/_ | ____| +| (___ | | | | | |__ | | | | |__ + \___ \ | | | | | __| | | | |___ \ + ____) | | | _| |_| | | | | |___) | +|_____/ |_| |_____|_| |_| |_|____/ \ No newline at end of file diff --git a/src/App.css b/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/src/App.tsx b/src/App.tsx index 46a5992..d6ea0b7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,120 +1,47 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from './assets/vite.svg' -import heroImg from './assets/hero.png' -import './App.css' +import { useState, useCallback } from 'react' +import { Terminal } from './components/Terminal' +import { MatrixRain } from './components/MatrixRain' +import { CommandPrompt } from './components/CommandPrompt' +import { BrickTransition } from './components/BrickTransition' +import { Bricked } from './components/Bricked' +import { CrtOverlay } from './components/CrtOverlay' + +type AppPhase = 'boot' | 'matrix' | 'prompt' | 'brick-transition' | 'bricked'; + +function getInitialPhase(): AppPhase { + if (localStorage.getItem('stift15-bricked') === 'true') return 'bricked'; + return 'boot'; +} function App() { - const [count, setCount] = useState(0) + const [phase, setPhase] = useState(getInitialPhase); + + const advancePhase = useCallback(() => { + setPhase((p) => { + if (p === 'boot') return 'matrix'; + if (p === 'matrix') return 'prompt'; + return p; + }); + }, []); + + const reboot = useCallback(() => { + setPhase('matrix'); + }, []); + + const brick = useCallback(() => { + localStorage.setItem('stift15-bricked', 'true'); + setPhase('brick-transition'); + }, []); return ( - <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.tsx and save to test HMR -

-
- -
- -
- -
-
- -

Documentation

-

Your questions, answered

- -
-
- -

Connect with us

-

Join the Vite community

- -
-
- -
-
- +
+ {phase === 'boot' && } + {phase === 'matrix' && } + {phase === 'prompt' && } + {phase === 'brick-transition' && setPhase('bricked')} />} + {phase === 'bricked' && } + +
) } diff --git a/src/commands/handlers.ts b/src/commands/handlers.ts new file mode 100644 index 0000000..2e792e4 --- /dev/null +++ b/src/commands/handlers.ts @@ -0,0 +1,222 @@ +import type { LineColor } from '../components/BootSequence'; + +export interface OutputLine { + text: string; + color: LineColor; +} + +type CommandHandler = (args: string[]) => OutputLine[]; + +function green(text: string): OutputLine { + return { text, color: 'green' }; +} +function amber(text: string): OutputLine { + return { text, color: 'amber' }; +} +function dim(text: string): OutputLine { + return { text, color: 'dim' }; +} +function red(text: string): OutputLine { + return { text, color: 'red' }; +} + +const FS_LISTING = [ + 'drwxr-xr-x frank staff games/', + 'drwxr-xr-x frank staff mods/', + 'drwxr-xr-x frank staff frank_broke_this/', + 'drwxr-xr-x frank staff backups_that_dont_work/', + '-rw-r--r-- root staff todo.txt', + '-rw-r--r-- root staff server_rules.txt', + '-rw-r--r-- frank staff definitely_not_a_virus.exe', + '-rwx------ root staff fix_franks_mess.sh', + '-rw-r--r-- frank staff iou_hosting_fees.txt', + 'drwxrwxrwx frank staff node_modules/ (2.1 GB)', +]; + +const FILE_CONTENTS: Record = { + 'todo.txt': [ + '1. Fix Frank\'s mess', + '2. Update the server (postponed since 2019)', + '3. Actually play games instead of fixing the server', + '4. Collect hosting fees from Frank', + '5. Give up', + ], + 'server_rules.txt': [ + 'STIFT15 SERVER RULES:', + '=====================', + '1. Do not let Frank push to main', + '2. Do NOT let Frank push to main', + '3. Seriously, Frank, stop pushing to main', + '4. No mining crypto on the game server', + '5. Frank, rule 4 applies to you specifically', + '6. Whoever keeps downloading texture packs: stop (Frank)', + '7. The server admin is always right', + '8. If the server is down, it\'s Frank\'s fault', + ], + 'iou_hosting_fees.txt': [ + 'Frank owes:', + ' January .... $15.00', + ' February ... $15.00', + ' March ...... $15.00', + ' April ...... $15.00 + $47.00 (emergency server recovery)', + ' May ........ $15.00', + ' Total ...... $122.00', + '', + 'Status: "I\'ll get you next month" (since January)', + ], + 'definitely_not_a_virus.exe': [ + 'Nice try. This is a Linux server.', + ], +}; + +const NEOFETCH = [ + ' _______ stift15@server', + ' / _____| ---------------', + ' | (___ OS: Linux 6.9.0-gamer-generic', + ' \\___ \\ Host: Stift15 Gaming HQ', + ' ____) | Kernel: 6.9.0-gamer', + ' |_____/ Uptime: 4h 20m 69s', + ' ____ _____ _____ Packages: 847 (node_modules)', + ' |____|_____|_____| Shell: stift15-sh 1.0', + ' CPU: AMD Ryzen 9 9950X @ 5.7GHz', + ' S T I F T 1 5 GPU: NVIDIA RTX 5090 "Mortgage Ed."', + ' Memory: 63420 MB / 65536 MB', + ' Disk: 98% used (node_modules)', + ' RGB: SYNCED (mood: aggressive)', +]; + +export const handlers: Record = { + help: () => [ + amber('Available commands:'), + green(' help - Show this help message'), + green(' ls - List files'), + green(' cat - Read a file'), + green(' cd - Change directory'), + green(' whoami - Who are you?'), + green(' nano - Text editor'), + green(' neofetch - System info'), + green(' clear - Clear terminal'), + green(' blame - Find who\'s responsible'), + green(' uptime - Server uptime'), + green(' ping - Ping a target'), + green(' rm - Remove a file'), + green(' sudo - Run as root'), + green(' reboot - Reboot the server'), + ], + + ls: () => FS_LISTING.map((line) => dim(line)), + + cat: (args) => { + const file = args[0]; + if (!file) return [red('cat: missing file operand')]; + const contents = FILE_CONTENTS[file]; + if (!contents) return [red(`cat: ${file}: No such file or directory`)]; + return contents.map((line) => green(line)); + }, + + cd: (args) => { + const dir = args[0]; + if (!dir) return [dim('~')]; + if (dir === 'node_modules') return [red('cd: node_modules: event horizon detected, cannot enter')]; + if (dir === 'frank_broke_this') return [red('cd: frank_broke_this: directory too corrupted to enter')]; + if (dir === 'games') return [amber('cd: games: you should be fixing the server, not playing games')]; + if (dir === 'backups_that_dont_work') return [red('cd: backups_that_dont_work: Permission denied (also they don\'t work)')]; + return [red(`cd: ${dir}: No such file or directory`)]; + }, + + whoami: () => [ + green('guest'), + dim('(you thought you were root? lol)'), + ], + + nano: () => [ + red('nano: command disabled.'), + dim('Frank deleted it to "save disk space."'), + dim('He then installed 47 texture packs.'), + ], + + neofetch: () => NEOFETCH.map((line) => green(line)), + + blame: () => [ + green('Analyzing git history...'), + green('Checking commit logs...'), + green('Cross-referencing with incident reports...'), + green(''), + amber('Result: It was Frank.'), + dim('(confidence: 99.7%)'), + dim('(remaining 0.3%: also Frank, using an alt account)'), + ], + + uptime: () => [ + green(' 03:14:15 up 4:20, 69 users, load average: 4.04, 0.00, 0.00'), + dim('(new personal record!)'), + ], + + ping: (args) => { + const target = args[0]; + if (!target) return [red('ping: missing target')]; + const lower = target.toLowerCase(); + if (lower === 'frank') { + return [ + green(`PING frank (192.168.1.???) 56(84) bytes of data.`), + amber('Request timeout. Frank is AFK again.'), + amber('Request timeout. Frank is "getting food."'), + amber('Request timeout. Frank disconnected.'), + red('--- frank ping statistics ---'), + red('3 packets transmitted, 0 received, 100% packet loss'), + dim('(as usual)'), + ]; + } + if (lower === 'franks.mom' || lower === 'frank.mom' || lower === 'franksmom') { + return [ + green(`PING franks.mom (192.168.1.1) 56(84) bytes of data.`), + dim('...'), + dim('...'), + dim('...'), + amber('Request timeout.'), + amber('Request timeout.'), + amber('Request timeout.'), + red('--- franks.mom ping statistics ---'), + red('3 packets transmitted, 0 received, 100% packet loss'), + dim(''), + dim('franks.mom is unreachable.'), + dim('She blocked this server after the 3 AM incident.'), + dim('Frank says "it wasn\'t that loud" but we have witnesses.'), + ]; + } + if (lower === 'google.com' || lower === 'google') { + return [ + green(`PING google.com (142.250.80.46) 56(84) bytes of data.`), + green('64 bytes: icmp_seq=1 ttl=118 time=4.20ms'), + green('64 bytes: icmp_seq=2 ttl=118 time=6.90ms'), + dim('--- google.com ping statistics ---'), + dim('2 packets transmitted, 2 received, 0% packet loss'), + dim('(at least something works)'), + ]; + } + return [ + green(`PING ${target} 56(84) bytes of data.`), + green(`64 bytes: icmp_seq=1 ttl=64 time=${(Math.random() * 100).toFixed(1)}ms`), + green(`64 bytes: icmp_seq=2 ttl=64 time=${(Math.random() * 100).toFixed(1)}ms`), + ]; + }, + + rm: (args) => { + const joined = args.join(' '); + if (joined.includes('-rf') && joined.includes('/')) { + // Special case handled in CommandPrompt + return []; + } + if (args.length === 0) return [red('rm: missing operand')]; + return [red(`rm: cannot remove '${args[args.length - 1]}': Permission denied`)]; + }, + + sudo: (args) => { + if (args.length === 0) return [red('sudo: missing command')]; + return [ + red('Nice try.'), + dim(`[sudo] incident reported to the server admin.`), + dim(`[sudo] also reported to Frank's mom.`), + ]; + }, +}; diff --git a/src/components/BootSequence.ts b/src/components/BootSequence.ts new file mode 100644 index 0000000..2ed9cfa --- /dev/null +++ b/src/components/BootSequence.ts @@ -0,0 +1,104 @@ +export type LineColor = 'green' | 'amber' | 'red' | 'dim'; + +export interface BootLine { + text: string; + color: LineColor; + delay: number; // ms before this line appears after the previous + typewriter?: boolean; +} + +export const BOOT_SEQUENCE: BootLine[] = [ + // Phase 1 - BIOS POST + { text: 'AWARD BIOS v6.9.420 (c) 2024 Stift15 Systems Ltd.', color: 'dim', delay: 300 }, + { text: 'Performing Power-On Self Test...', color: 'dim', delay: 200 }, + { text: '', color: 'dim', delay: 100 }, + { text: 'CPU: AMD Ryzen 9 9950X "GamerFuel Edition" @ 5.7 GHz .......... OK', color: 'green', delay: 150 }, + { text: 'RAM: 65536 MB DDR5-6000 ....................................... OK', color: 'green', delay: 120 }, + { text: 'GPU: NVIDIA RTX 5090 "Mortgage Edition" ........................ OK', color: 'green', delay: 120 }, + { text: 'RGB Subsystem .................................................. SYNCED (mood: aggressive)', color: 'green', delay: 100 }, + { text: 'Gamer Chair Lumbar Support ..................................... RECLINED', color: 'green', delay: 100 }, + { text: '', color: 'dim', delay: 80 }, + { text: 'Detecting boot devices...', color: 'dim', delay: 400 }, + { text: 'Boot device: /dev/sda1 (labeled "DO_NOT_DELETE_MOM")', color: 'dim', delay: 300 }, + { text: '', color: 'dim', delay: 200 }, + + // Phase 2 - Kernel Boot + { text: 'Loading GRUB 2.12 ...', color: 'green', delay: 300 }, + { text: 'kernel: Linux 6.9.0-gamer-generic loading...', color: 'green', delay: 200 }, + { text: '[ 0.000000] Command line: root=/dev/sda1 ro quiet splash=off fps_unlock=true', color: 'dim', delay: 80 }, + { text: '[ 0.004200] Initializing gaming subsystems...', color: 'dim', delay: 80 }, + { text: '[ 0.013370] RGB controller mapped to /dev/rgb0', color: 'dim', delay: 60 }, + { text: '[ 0.024601] Registering thermal zone: "CPU Package" (target: yes)', color: 'dim', delay: 60 }, + { text: '[ 0.031337] Loading module: gamer_posture.ko', color: 'dim', delay: 60 }, + { text: '[ 0.042069] Mounting /dev/fridge (read-only, sadness mode)', color: 'dim', delay: 60 }, + { text: '', color: 'dim', delay: 200 }, + + // Phase 3 - Services Starting + { text: '[ OK ] Started Gamer Posture Reminder Service', color: 'green', delay: 150 }, + { text: '[ OK ] Started Mountain Dew Level Monitor', color: 'green', delay: 120 }, + { text: '[ OK ] Started Discord Rich Presence Daemon', color: 'green', delay: 120 }, + { text: '[ OK ] Started Mechanical Keyboard Click Amplifier', color: 'green', delay: 100 }, + { text: '[ OK ] Started Rage Quit Prevention System (v0.1-beta)', color: 'green', delay: 100 }, + { text: '[WARN] Hot Pocket Proximity Sensor returned NaN', color: 'amber', delay: 300 }, + { text: '[ OK ] Started Frank\'s "Temporary" Minecraft Server (uptime: 847 days)', color: 'green', delay: 150 }, + { text: '[ OK ] Started nginx web server on port 80', color: 'green', delay: 100 }, + { text: '[ OK ] Started Node.js application server on port 3000', color: 'green', delay: 100 }, + { text: '[WARN] server.js has mass of 2.1 GB (node_modules singularity detected)', color: 'amber', delay: 400 }, + { text: '[WARN] Gravitational pull from node_modules is affecting nearby files', color: 'amber', delay: 200 }, + { text: '', color: 'dim', delay: 300 }, + + // Phase 4 - The Crash + { text: '[FAIL] HTTP Health Check: GET /api/status returned... nothing', color: 'red', delay: 600 }, + { text: '[FAIL] The backend is not responding.', color: 'red', delay: 400 }, + { text: '[FAIL] The backend has never responded. To anything. Ever.', color: 'red', delay: 500 }, + { text: '', color: 'dim', delay: 200 }, + { text: '[WARN] Attempting emergency restart...', color: 'amber', delay: 800 }, + { text: '[ 4.040404] Process \'server.js\' invoked OOM killer', color: 'red', delay: 300 }, + { text: '[ 4.040405] OOM killer selected process \'stift15-game-server\' (adj 1000)', color: 'red', delay: 150 }, + { text: '[ 4.040406] Out of memory: Killed process 1337 (stift15-game-server)', color: 'red', delay: 150 }, + { text: '[ 4.040407] Reason: Frank downloaded the entire Steam Workshop into /tmp', color: 'red', delay: 300 }, + { text: '', color: 'dim', delay: 200 }, + { text: '[FAIL] Emergency restart failed. Exit code: 418 (I\'m a teapot)', color: 'red', delay: 600 }, + { text: '', color: 'dim', delay: 800 }, + + // Phase 5 - The 503 Error (typewriter lines) + { text: '============================================================', color: 'red', delay: 400, typewriter: true }, + { text: ' ERROR 503 - SERVICE UNAVAILABLE', color: 'red', delay: 100, typewriter: true }, + { text: '============================================================', color: 'red', delay: 100, typewriter: true }, + { text: '', color: 'dim', delay: 200 }, + { text: ' The server hosting the Stift15 gaming group has encountered', color: 'green', delay: 60, typewriter: true }, + { text: ' a fatal error and is currently taking a rage-quit break.', color: 'green', delay: 60, typewriter: true }, + { text: '', color: 'dim', delay: 300 }, + { text: ' DIAGNOSTIC SUMMARY:', color: 'amber', delay: 200, typewriter: true }, + { text: ' - Last stable connection: "lol"', color: 'green', delay: 80, typewriter: true }, + { text: ' - Uptime before crash: 4h 20m 69s', color: 'green', delay: 80, typewriter: true }, + { text: ' - Packets lost: all of them', color: 'green', delay: 80, typewriter: true }, + { text: ' - Root cause: someone git pushed node_modules to prod', color: 'green', delay: 80, typewriter: true }, + { text: ' - Secondary cause: Frank', color: 'green', delay: 80, typewriter: true }, + { text: ' - Tertiary cause: also Frank', color: 'green', delay: 80, typewriter: true }, + { text: ' - Frank\'s response: "works on my machine"', color: 'green', delay: 80, typewriter: true }, + { text: '', color: 'dim', delay: 300 }, + { text: ' RECOMMENDED ACTIONS:', color: 'amber', delay: 200, typewriter: true }, + { text: ' 1. Touch grass (estimated ETA: never)', color: 'green', delay: 80, typewriter: true }, + { text: ' 2. Check if Frank pushed to main again', color: 'green', delay: 80, typewriter: true }, + { text: ' 3. Blame the lag', color: 'green', delay: 80, typewriter: true }, + { text: ' 4. Alt+F4 your expectations', color: 'green', delay: 80, typewriter: true }, + { text: ' 5. Have you tried turning Frank off and on again?', color: 'green', delay: 80, typewriter: true }, + { text: '', color: 'dim', delay: 300 }, + { text: ' If this error persists, please scream into the void or', color: 'green', delay: 60, typewriter: true }, + { text: ' message the server admin, who is also screaming into the void.', color: 'green', delay: 60, typewriter: true }, + { text: '', color: 'dim', delay: 400 }, + { text: '============================================================', color: 'red', delay: 100, typewriter: true }, + { text: ' SERVER ADMIN NOTE:', color: 'amber', delay: 200, typewriter: true }, + { text: ' No, I will not fix this at 3 AM. I have work tomorrow.', color: 'green', delay: 60, typewriter: true }, + { text: ' The server can stay dead. It builds character.', color: 'green', delay: 60, typewriter: true }, + { text: ' Also Frank still owes me for last month\'s hosting.', color: 'green', delay: 60, typewriter: true }, + { text: '============================================================', color: 'red', delay: 100, typewriter: true }, + { text: '', color: 'dim', delay: 1500 }, + + // Phase 6 - Reconnect + { text: '> Reconnecting in 3 seconds...', color: 'dim', delay: 0 }, + { text: '> ...', color: 'dim', delay: 1000 }, + { text: '> ...', color: 'dim', delay: 1000 }, + { text: '> CONNECTION ESTABLISHED', color: 'green', delay: 1000 }, +]; diff --git a/src/components/BrickTransition.tsx b/src/components/BrickTransition.tsx new file mode 100644 index 0000000..7a952b7 --- /dev/null +++ b/src/components/BrickTransition.tsx @@ -0,0 +1,143 @@ +import { useEffect, useRef, useState } from 'react'; + +interface BrickTransitionProps { + onComplete: () => void; +} + +const DELETE_PATHS = [ + '/bin', + '/boot', + '/dev', + '/etc', + '/home', + '/lib', + '/media', + '/mnt', + '/opt', + '/proc', + '/root', + '/sbin', + '/srv', + '/sys', + '/tmp', + '/usr', + '/usr/lib', + '/usr/share', + '/usr/bin', + '/usr/local/games', + '/usr/local/node_modules (2.1 GB)', + '/var', + '/var/log', + '/var/frank_was_here', +]; + +type Phase = 'deleting' | 'kernel-panic' | 'glitch' | 'black'; + +export function BrickTransition({ onComplete }: BrickTransitionProps) { + const [phase, setPhase] = useState('deleting'); + const [deletedCount, setDeletedCount] = useState(0); + const [glitchOpacity, setGlitchOpacity] = useState(1); + const intervalRef = useRef>(undefined); + const containerRef = useRef(null); + + // Phase 1: Rapid-fire file deletion + useEffect(() => { + if (phase !== 'deleting') return; + let i = 0; + intervalRef.current = setInterval(() => { + i++; + setDeletedCount(i); + if (i >= DELETE_PATHS.length) { + clearInterval(intervalRef.current); + setTimeout(() => setPhase('kernel-panic'), 400); + } + }, 80); + return () => clearInterval(intervalRef.current); + }, [phase]); + + // Phase 2: Kernel panic text, then glitch + useEffect(() => { + if (phase !== 'kernel-panic') return; + const timer = setTimeout(() => setPhase('glitch'), 2000); + return () => clearTimeout(timer); + }, [phase]); + + // Phase 3: Screen glitch — flicker and dissolve + useEffect(() => { + if (phase !== 'glitch') return; + let frame = 0; + const totalFrames = 30; + intervalRef.current = setInterval(() => { + frame++; + // Flicker between visible and dark, trending toward black + const progress = frame / totalFrames; + if (Math.random() < progress) { + setGlitchOpacity(0); + } else { + setGlitchOpacity(Math.random() * 0.5 + 0.3); + } + if (frame >= totalFrames) { + clearInterval(intervalRef.current); + setPhase('black'); + } + }, 60); + return () => clearInterval(intervalRef.current); + }, [phase]); + + // Phase 4: Black screen, then transition to bricked + useEffect(() => { + if (phase !== 'black') return; + const timer = setTimeout(onComplete, 1500); + return () => clearTimeout(timer); + }, [phase, onComplete]); + + if (phase === 'black') { + return
; + } + + return ( +
+ {/* Deletion log */} +
+ rm: deleting entire filesystem... +
+ {DELETE_PATHS.slice(0, deletedCount).map((path, i) => ( +
+ rm: {path} ... deleted +
+ ))} + + {/* Kernel panic */} + {(phase === 'kernel-panic' || phase === 'glitch') && ( +
+
 
+
+ Kernel panic - not syncing: Attempted to kill init! +
+
+ exitcode=0x00000009 +
+
 
+
+ [ 9.999999] ---[ end Kernel panic - not syncing ]--- +
+
 
+
+ You absolute legend. You actually did it. +
+
+ )} +
+ ); +} diff --git a/src/components/Bricked.tsx b/src/components/Bricked.tsx new file mode 100644 index 0000000..882548f --- /dev/null +++ b/src/components/Bricked.tsx @@ -0,0 +1,103 @@ +import { useEffect, useRef, useState } from 'react'; + +const LINES = [ + { text: 'AWARD BIOS v6.9.420 (c) 2024 Stift15 Systems Ltd.', delay: 300 }, + { text: 'Performing Power-On Self Test...', delay: 200 }, + { text: '', delay: 100 }, + { text: 'CPU: AMD Ryzen 9 9950X "GamerFuel Edition" @ 5.7 GHz .......... OK', delay: 150 }, + { text: 'RAM: 65536 MB DDR5-6000 ....................................... OK', delay: 120 }, + { text: 'GPU: NVIDIA RTX 5090 "Mortgage Edition" ........................ OK', delay: 120 }, + { text: '', delay: 200 }, + { text: 'Detecting boot devices...', delay: 400 }, + { text: 'Boot device: /dev/sda1 (labeled "DO_NOT_DELETE_MOM")', delay: 300 }, + { text: '', delay: 200 }, + { text: 'Loading GRUB 2.12 ...', delay: 300 }, + { text: '', delay: 500 }, + { text: 'error: no such partition.', delay: 400 }, + { text: 'error: file \'/boot/vmlinuz-6.9.0-gamer-generic\' not found.', delay: 300 }, + { text: 'error: you need to load the kernel first.', delay: 300 }, + { text: '', delay: 600 }, + { text: 'GRUB rescue>', delay: 200 }, + { text: '', delay: 1000 }, + { text: '============================================================', delay: 400 }, + { text: ' FATAL: NO BOOTABLE FILESYSTEM FOUND', delay: 200 }, + { text: '============================================================', delay: 200 }, + { text: '', delay: 300 }, + { text: ' Someone ran rm -rf / on this server.', delay: 200 }, + { text: ' The entire filesystem has been deleted.', delay: 200 }, + { text: ' There is nothing left.', delay: 200 }, + { text: '', delay: 300 }, + { text: ' The server admin has been notified.', delay: 200 }, + { text: ' The server admin is crying.', delay: 300 }, + { text: '', delay: 500 }, + { text: ' This server is now a very expensive paperweight.', delay: 200 }, + { text: ' Good job.', delay: 300 }, + { text: '', delay: 800 }, + { text: ' Press F to pay respects.', delay: 0 }, +]; + +export function Bricked() { + const [visibleCount, setVisibleCount] = useState(0); + const [unbricked, setUnbricked] = useState(false); + const timeoutRef = useRef>(undefined); + const containerRef = useRef(null); + + useEffect(() => { + if (visibleCount >= LINES.length) return; + timeoutRef.current = setTimeout(() => { + setVisibleCount((c) => c + 1); + }, LINES[visibleCount].delay); + return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; + }, [visibleCount]); + + useEffect(() => { + containerRef.current?.focus(); + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'f' || e.key === 'F') { + localStorage.removeItem('stift15-bricked'); + setUnbricked(true); + } + }; + + if (unbricked) { + return ( +
+
+
Respects paid. Server restored.
+
Reload the page to reconnect.
+
+
+ ); + } + + const visible = LINES.slice(0, visibleCount); + + return ( +
containerRef.current?.focus()} + > + {visible.map((line, i) => ( +
= 12 && i <= 14 ? 'text-term-red' : i >= 18 ? 'text-term-red' : 'text-term-dim'} whitespace-pre leading-[1.4]`} + > + {line.text || '\u00A0'} +
+ ))} + {visibleCount >= LINES.length && ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/CommandPrompt.tsx b/src/components/CommandPrompt.tsx new file mode 100644 index 0000000..bc85a0a --- /dev/null +++ b/src/components/CommandPrompt.tsx @@ -0,0 +1,152 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { handlers, type OutputLine } from '../commands/handlers'; + +const PROMPT = 'stift15@server:~$ '; + +const WELCOME: OutputLine[] = [ + { text: '> Connection restored.', color: 'green' }, + { text: '> Welcome to Stift15 Terminal v1.0', color: 'green' }, + { text: '> Type \'help\' for available commands.', color: 'dim' }, + { text: '', color: 'dim' }, +]; + +interface CommandPromptProps { + onReboot?: () => void; + onBrick?: () => void; +} + +export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) { + const [outputLines, setOutputLines] = useState(WELCOME); + const [input, setInput] = useState(''); + const [history, setHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + const containerRef = useRef(null); + const bottomRef = useRef(null); + + const scrollToBottom = useCallback(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, []); + + useEffect(scrollToBottom, [outputLines.length, scrollToBottom]); + + // Focus container on mount + useEffect(() => { + containerRef.current?.focus(); + }, []); + + const executeCommand = useCallback( + (cmdLine: string) => { + const echoLine: OutputLine = { text: PROMPT + cmdLine, color: 'green' }; + const trimmed = cmdLine.trim(); + + if (!trimmed) { + setOutputLines((prev) => [...prev, echoLine]); + return; + } + + const parts = trimmed.split(/\s+/); + const cmd = parts[0].toLowerCase(); + const args = parts.slice(1); + + if (cmd === 'clear') { + setOutputLines([]); + return; + } + + if (cmd === 'rm' && trimmed.includes('-rf') && trimmed.includes('/')) { + setOutputLines((prev) => [...prev, echoLine]); + setTimeout(() => onBrick?.(), 300); + return; + } + + if (cmd === 'reboot') { + setOutputLines((prev) => [ + ...prev, + echoLine, + { text: 'Rebooting server...', color: 'amber' }, + ]); + setTimeout(() => onReboot?.(), 800); + return; + } + + const handler = handlers[cmd]; + const result = handler + ? handler(args) + : [{ text: `${cmd}: command not found. Type 'help' for available commands.`, color: 'red' as const }]; + + setOutputLines((prev) => [...prev, echoLine, ...result, { text: '', color: 'dim' }]); + setHistory((prev) => [...prev, trimmed]); + setHistoryIndex(-1); + }, + [] + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + executeCommand(input); + setInput(''); + } else if (e.key === 'Backspace') { + setInput((prev) => prev.slice(0, -1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (history.length === 0) return; + const newIndex = historyIndex === -1 ? history.length - 1 : Math.max(0, historyIndex - 1); + setHistoryIndex(newIndex); + setInput(history[newIndex]); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + if (historyIndex === -1) return; + const newIndex = historyIndex + 1; + if (newIndex >= history.length) { + setHistoryIndex(-1); + setInput(''); + } else { + setHistoryIndex(newIndex); + setInput(history[newIndex]); + } + } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) { + setInput((prev) => prev + e.key); + } + }, + [input, executeCommand, history, historyIndex] + ); + + const colorClass: Record = { + green: 'text-term-green', + amber: 'text-term-amber', + red: 'text-term-red', + dim: 'text-term-dim', + }; + + return ( +
containerRef.current?.focus()} + > + {outputLines.map((line, i) => ( +
+ {line.text || '\u00A0'} +
+ ))} + + {/* Input line */} +
+ {PROMPT} + {input} + +
+ +
+
+ ); +} diff --git a/src/components/CrtOverlay.tsx b/src/components/CrtOverlay.tsx new file mode 100644 index 0000000..8cbe22b --- /dev/null +++ b/src/components/CrtOverlay.tsx @@ -0,0 +1,51 @@ +import { useRef, useMemo } from 'react'; +import { Canvas, useFrame } from '@react-three/fiber'; +import * as THREE from 'three'; +import { crtVertexShader } from '../shaders/crt.vert'; +import { crtFragmentShader } from '../shaders/crt.frag'; + +function CrtMesh() { + const materialRef = useRef(null); + + const uniforms = useMemo( + () => ({ + uTime: { value: 0 }, + }), + [] + ); + + useFrame(({ clock }) => { + if (materialRef.current) { + materialRef.current.uniforms.uTime.value = clock.getElapsedTime(); + } + }); + + return ( + + + + + ); +} + +export function CrtOverlay() { + return ( +
+ + + +
+ ); +} diff --git a/src/components/MatrixRain.tsx b/src/components/MatrixRain.tsx new file mode 100644 index 0000000..d166f99 --- /dev/null +++ b/src/components/MatrixRain.tsx @@ -0,0 +1,240 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; +import { loadAsciiArt } from '../utils/loadAsciiArt'; + +interface MatrixRainProps { + onComplete?: () => void; +} + +interface Column { + y: number; + speed: number; + chars: string[]; +} + +const FONT_SIZE = 14; +const CHAR_WIDTH = FONT_SIZE * 0.6; +const CHAR_HEIGHT = FONT_SIZE * 1.2; +const FADE_ALPHA = 0.05; +const RAIN_CHARS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789'; + +function randomChar(): string { + return RAIN_CHARS[Math.floor(Math.random() * RAIN_CHARS.length)]; +} + +function createColumn(totalRows: number): Column { + return { + y: Math.floor(Math.random() * -totalRows), + speed: 0.3 + Math.random() * 0.7, + chars: Array.from({ length: totalRows }, () => randomChar()), + }; +} + +export function MatrixRain({ onComplete }: MatrixRainProps) { + const canvasRef = useRef(null); + const [artLines, setArtLines] = useState(null); + const completedRef = useRef(false); + + useEffect(() => { + loadAsciiArt('/ascii/logo.txt').then(setArtLines); + }, []); + + const startAnimation = useCallback( + (canvas: HTMLCanvasElement, art: string[]) => { + const ctx = canvas.getContext('2d')!; + let animId: number; + + function resize() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + resize(); + + const resizeObserver = new ResizeObserver(resize); + resizeObserver.observe(canvas.parentElement!); + + const totalCols = Math.floor(canvas.width / CHAR_WIDTH); + const totalRows = Math.floor(canvas.height / CHAR_HEIGHT); + + // Center the art on screen + const artWidth = Math.max(...art.map((l) => l.length)); + const artHeight = art.length; + const artOffsetCol = Math.floor((totalCols - artWidth) / 2); + const artOffsetRow = Math.floor((totalRows - artHeight) / 2); + + // Build art grid lookup: artGrid[row][col] = character or undefined + const artGrid: (string | undefined)[][] = []; + for (let r = 0; r < artHeight; r++) { + artGrid[r] = []; + for (let c = 0; c < artWidth; c++) { + const ch = art[r]?.[c]; + if (ch && ch !== ' ') { + artGrid[r][c] = ch; + } + } + } + + // Track which art cells have been revealed and when (for glow decay) + const revealed: boolean[][] = Array.from({ length: artHeight }, () => + Array(artWidth).fill(false) + ); + const revealTimeMap: number[][] = Array.from({ length: artHeight }, () => + Array(artWidth).fill(0) + ); + const GLOW_DURATION = 1200; // ms for glow to fade out + let totalArtChars = 0; + for (let r = 0; r < artHeight; r++) { + for (let c = 0; c < artWidth; c++) { + if (artGrid[r][c]) totalArtChars++; + } + } + let revealedCount = 0; + let revealCompleteTime = 0; + + // Initialize columns + const columns: Column[] = Array.from({ length: totalCols }, () => + createColumn(totalRows) + ); + + // Clear to black initially + ctx.fillStyle = '#0a0a0a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + function draw() { + // Fade existing content + ctx.fillStyle = `rgba(10, 10, 10, ${FADE_ALPHA})`; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.font = `${FONT_SIZE}px "IBM Plex Mono", "Fira Code", "Courier New", monospace`; + ctx.textBaseline = 'top'; + + for (let col = 0; col < totalCols; col++) { + const column = columns[col]; + column.y += column.speed; + + const headRow = Math.floor(column.y); + + // Draw rain trail (a few characters behind the head) + const trailLen = 15; + for (let t = 0; t < trailLen; t++) { + const row = headRow - t; + if (row < 0 || row >= totalRows) continue; + + const x = col * CHAR_WIDTH; + const y = row * CHAR_HEIGHT; + + // Check if this position is an art cell + const artRow = row - artOffsetRow; + const artCol = col - artOffsetCol; + const isArtCell = + artRow >= 0 && + artRow < artHeight && + artCol >= 0 && + artCol < artWidth && + artGrid[artRow]?.[artCol]; + + if (isArtCell && t === 0) { + // Head just passed through — reveal this art character + if (!revealed[artRow][artCol]) { + revealed[artRow][artCol] = true; + revealTimeMap[artRow][artCol] = performance.now(); + revealedCount++; + } + } + + if (t === 0) { + // Head character — bright white-green + ctx.fillStyle = '#ccffcc'; + } else { + // Trail fades from bright to dim + const brightness = Math.max(0, 1 - t / trailLen); + const g = Math.floor(100 + 155 * brightness); + ctx.fillStyle = `rgb(0, ${g}, 0)`; + } + + // Randomly change trail characters + if (Math.random() < 0.05) { + column.chars[row % column.chars.length] = randomChar(); + } + + ctx.fillText( + column.chars[row % column.chars.length], + x, + y + ); + } + + // Reset column when it falls off screen + if (headRow - 15 > totalRows) { + columns[col] = createColumn(totalRows); + columns[col].y = Math.floor(Math.random() * -10); + } + } + + // Draw revealed art characters on top with glow effect + const now = performance.now(); + for (let r = 0; r < artHeight; r++) { + for (let c = 0; c < artWidth; c++) { + if (revealed[r][c] && artGrid[r][c]) { + const x = (artOffsetCol + c) * CHAR_WIDTH; + const y = (artOffsetRow + r) * CHAR_HEIGHT; + const elapsed = now - revealTimeMap[r][c]; + const glowIntensity = Math.max(0, 1 - elapsed / GLOW_DURATION); + + if (glowIntensity > 0) { + // Active glow: bright white core fading to green, with blur + ctx.shadowColor = `rgba(51, 255, 51, ${glowIntensity})`; + ctx.shadowBlur = 12 + glowIntensity * 16; + const white = Math.floor(glowIntensity * 200); + ctx.fillStyle = `rgb(${white + 51}, 255, ${white + 51})`; + } else { + // Settled: normal bright green, no shadow + ctx.shadowColor = 'transparent'; + ctx.shadowBlur = 0; + ctx.fillStyle = '#33ff33'; + } + + ctx.fillText(artGrid[r][c]!, x, y); + } + } + } + // Reset shadow state for next frame's rain drawing + ctx.shadowColor = 'transparent'; + ctx.shadowBlur = 0; + + // Check completion + if (revealedCount >= totalArtChars && !completedRef.current) { + if (revealCompleteTime === 0) { + revealCompleteTime = performance.now(); + } else if (performance.now() - revealCompleteTime > 2000) { + completedRef.current = true; + cancelAnimationFrame(animId); + onComplete?.(); + return; + } + } + + animId = requestAnimationFrame(draw); + } + + animId = requestAnimationFrame(draw); + + return () => { + cancelAnimationFrame(animId); + resizeObserver.disconnect(); + }; + }, + [onComplete] + ); + + useEffect(() => { + if (!canvasRef.current || !artLines) return; + return startAnimation(canvasRef.current, artLines); + }, [artLines, startAnimation]); + + return ( + + ); +} diff --git a/src/components/Terminal.tsx b/src/components/Terminal.tsx new file mode 100644 index 0000000..9667b38 --- /dev/null +++ b/src/components/Terminal.tsx @@ -0,0 +1,32 @@ +import { useEffect, useRef } from 'react'; +import { BOOT_SEQUENCE } from './BootSequence'; +import { TerminalLine } from './TerminalLine'; +import { useTerminalSequence } from '../hooks/useTerminalSequence'; + +interface TerminalProps { + onComplete?: () => void; +} + +export function Terminal({ onComplete }: TerminalProps) { + const { visibleLines } = useTerminalSequence(BOOT_SEQUENCE, onComplete); + const bottomRef = useRef(null); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [visibleLines.length]); + + return ( +
+ {visibleLines.map((line, i) => ( + + ))} +
+
+ ); +} diff --git a/src/components/TerminalLine.tsx b/src/components/TerminalLine.tsx new file mode 100644 index 0000000..2b86bcd --- /dev/null +++ b/src/components/TerminalLine.tsx @@ -0,0 +1,39 @@ +import { useTypewriter } from '../hooks/useTypewriter'; +import type { LineColor } from './BootSequence'; + +const colorClass: Record = { + green: 'text-term-green', + amber: 'text-term-amber', + red: 'text-term-red', + dim: 'text-term-dim', +}; + +interface TerminalLineProps { + text: string; + color: LineColor; + typewriter?: boolean; + isLast: boolean; +} + +function TypewriterText({ text }: { text: string }) { + const revealed = useTypewriter(text, 2, 16); + return <>{revealed}; +} + +export function TerminalLine({ text, color, typewriter, isLast }: TerminalLineProps) { + if (text === '') { + return
; + } + + return ( +
+ {typewriter ? : text} + {isLast && ( + + )} +
+ ); +} diff --git a/src/hooks/useTerminalSequence.ts b/src/hooks/useTerminalSequence.ts new file mode 100644 index 0000000..dcf8e0e --- /dev/null +++ b/src/hooks/useTerminalSequence.ts @@ -0,0 +1,32 @@ +import { useState, useEffect, useRef } from 'react'; +import type { BootLine } from '../components/BootSequence'; + +export function useTerminalSequence(lines: BootLine[], onComplete?: () => void) { + const [visibleCount, setVisibleCount] = useState(0); + const timeoutRef = useRef>(undefined); + const completedRef = useRef(false); + + useEffect(() => { + if (visibleCount >= lines.length) { + if (!completedRef.current && onComplete) { + completedRef.current = true; + timeoutRef.current = setTimeout(onComplete, 800); + } + return; + } + + const nextLine = lines[visibleCount]; + timeoutRef.current = setTimeout(() => { + setVisibleCount((c) => c + 1); + }, nextLine.delay); + + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, [visibleCount, lines, onComplete]); + + return { + visibleLines: lines.slice(0, visibleCount), + isComplete: visibleCount >= lines.length, + }; +} diff --git a/src/hooks/useTypewriter.ts b/src/hooks/useTypewriter.ts new file mode 100644 index 0000000..ffa695c --- /dev/null +++ b/src/hooks/useTypewriter.ts @@ -0,0 +1,26 @@ +import { useState, useEffect, useRef } from 'react'; + +export function useTypewriter(text: string, charsPerTick = 2, tickMs = 16) { + const [charCount, setCharCount] = useState(0); + const intervalRef = useRef>(undefined); + + useEffect(() => { + setCharCount(0); + intervalRef.current = setInterval(() => { + setCharCount((c) => { + const next = c + charsPerTick; + if (next >= text.length) { + clearInterval(intervalRef.current); + return text.length; + } + return next; + }); + }, tickMs); + + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [text, charsPerTick, tickMs]); + + return text.slice(0, charCount); +} diff --git a/src/index.css b/src/index.css index 5fb3313..08ba371 100644 --- a/src/index.css +++ b/src/index.css @@ -1,111 +1,23 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; +@import "tailwindcss"; - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } +@theme { + --color-term-green: #33ff33; + --color-term-amber: #ffbf00; + --color-term-red: #ff3333; + --color-term-dim: #338833; + --color-term-bg: #0a0a0a; + --font-mono: "IBM Plex Mono", "Fira Code", "Courier New", monospace; } -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } +@keyframes blink { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } } -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -body { +html, body, #root { margin: 0; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); + padding: 0; + height: 100%; + background: var(--color-term-bg); + overflow: hidden; } diff --git a/src/shaders/crt.frag.ts b/src/shaders/crt.frag.ts new file mode 100644 index 0000000..7287e63 --- /dev/null +++ b/src/shaders/crt.frag.ts @@ -0,0 +1,23 @@ +export const crtFragmentShader = /* glsl */ ` + uniform float uTime; + varying vec2 vUv; + + void main() { + vec2 uv = vUv; + + // Scanlines - thin horizontal dark bars scrolling down + float scanline = sin(uv.y * 800.0 + uTime * 2.0) * 0.04; + + // Vignette - darken edges to simulate CRT curvature + vec2 center = uv - 0.5; + float vignette = 1.0 - dot(center, center) * 1.5; + vignette = clamp(vignette, 0.0, 1.0); + + // Flicker - subtle brightness oscillation + float flicker = sin(uTime * 8.0) * 0.008 + sin(uTime * 13.0) * 0.005; + + // Combine: mostly transparent, with subtle darkening + float darkness = scanline + (1.0 - vignette) * 0.3 - flicker; + gl_FragColor = vec4(0.0, 0.0, 0.0, clamp(darkness, 0.0, 0.5)); + } +`; diff --git a/src/shaders/crt.vert.ts b/src/shaders/crt.vert.ts new file mode 100644 index 0000000..66c19d6 --- /dev/null +++ b/src/shaders/crt.vert.ts @@ -0,0 +1,7 @@ +export const crtVertexShader = /* glsl */ ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } +`; diff --git a/src/utils/loadAsciiArt.ts b/src/utils/loadAsciiArt.ts new file mode 100644 index 0000000..86163cb --- /dev/null +++ b/src/utils/loadAsciiArt.ts @@ -0,0 +1,5 @@ +export async function loadAsciiArt(path: string): Promise { + const res = await fetch(path); + const text = await res.text(); + return text.split('\n'); +} diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..c4069b7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tailwindcss()], })