diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..420fb18 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,38 @@ +--- +name: Heritage +colors: + primary: "#1A1C1E" + secondary: "#6C7278" + tertiary: "#B8422E" + neutral: "#F7F5F2" +typography: + h1: + fontFamily: Public Sans + fontSize: 3rem + body-md: + fontFamily: Public Sans + fontSize: 1rem + label-caps: + fontFamily: Space Grotesk + fontSize: 0.75rem +rounded: + sm: 4px + md: 8px +spacing: + sm: 8px + md: 16px +--- + +## Overview + +Architectural Minimalism meets Journalistic Gravitas. The UI evokes a +premium matte finish — a high-end broadsheet or contemporary gallery. + +## Colors + +The palette is rooted in high-contrast neutrals and a single accent color. + +- **Primary (#1A1C1E):** Deep ink for headlines and core text. +- **Secondary (#6C7278):** Sophisticated slate for borders, captions, metadata. +- **Tertiary (#B8422E):** "Boston Clay" — the sole driver for interaction. +- **Neutral (#F7F5F2):** Warm limestone foundation, softer than pure white. diff --git a/package.json b/package.json index 9c42239..eaba418 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,8 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.11", + "@react-three/fiber": "^9.6.1", + "@shadergradient/react": "^2.4.20", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-query": "^5.101.1", "@tanstack/react-router": "^1.170.16", @@ -57,6 +59,8 @@ "@tanstack/zod-adapter": "^1.167.0", "@vercel/analytics": "^2.0.1", "@vercel/speed-insights": "^2.0.0", + "@ybouane/liquidglass": "^1.0.3", + "camera-controls": "^3.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -75,6 +79,8 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", + "three": "^0.185.1", + "three-stdlib": "^2.36.1", "tw-animate-css": "^1.3.4", "vaul": "^1.1.2", "vite-tsconfig-paths": "^6.0.2", @@ -86,6 +92,7 @@ "@types/node": "^22.16.5", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", + "@types/three": "^0.185.1", "@vitejs/plugin-react": "^5.2.0", "@vitest/coverage-v8": "^4.1.10", "@vitest/ui": "^4.1.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f805879..521952f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,12 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.11 version: 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@react-three/fiber': + specifier: ^9.6.1 + version: 9.6.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.185.1) + '@shadergradient/react': + specifier: ^2.4.20 + version: 2.4.20(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tailwindcss/vite': specifier: ^4.2.1 version: 4.3.2(vite@8.1.0(@types/node@22.20.0)(jiti@1.21.7)) @@ -116,6 +122,12 @@ importers: '@vercel/speed-insights': specifier: ^2.0.0 version: 2.0.0(react@19.2.7) + '@ybouane/liquidglass': + specifier: ^1.0.3 + version: 1.0.3 + camera-controls: + specifier: ^3.1.2 + version: 3.1.2(three@0.185.1) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -170,6 +182,12 @@ importers: tailwindcss: specifier: ^4.2.1 version: 4.3.2 + three: + specifier: ^0.185.1 + version: 0.185.1 + three-stdlib: + specifier: ^2.36.1 + version: 2.36.1(three@0.185.1) tw-animate-css: specifier: ^1.3.4 version: 1.4.0 @@ -198,6 +216,9 @@ importers: '@types/react-dom': specifier: ^19.2.0 version: 19.2.3(@types/react@19.2.17) + '@types/three': + specifier: ^0.185.1 + version: 0.185.1 '@vitejs/plugin-react': specifier: ^5.2.0 version: 5.2.0(vite@8.1.0(@types/node@22.20.0)(jiti@1.21.7)) @@ -351,6 +372,9 @@ packages: '@date-fns/tz@1.5.0': resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} @@ -1635,6 +1659,31 @@ packages: '@radix-ui/rect@1.1.2': resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} + '@react-three/fiber@9.6.1': + resolution: {integrity: sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==} + 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 + '@rolldown/binding-android-arm64@1.1.3': resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1736,6 +1785,12 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@shadergradient/react@2.4.20': + resolution: {integrity: sha512-MVYvYgTHK3d36C2jNKzt4OzWeyNyc/bWI0zKRyv5EgI2hmUee8nD0cmuNX9X1lw1RCcuof6n1Rnj63jWepZHJA==} + peerDependencies: + react: ^18.2.0 || ^19.0.0 + react-dom: ^18.2.0 || ^19.0.0 + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2008,6 +2063,9 @@ packages: '@tanstack/react-router': '>=1.43.2' zod: ^3.23.8 + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -2056,6 +2114,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -2065,14 +2126,31 @@ packages: '@types/node@22.20.0': resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: '@types/react': ^19.2.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.185.1': + resolution: {integrity: sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A==} + + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -2239,6 +2317,9 @@ packages: '@vitest/utils@4.1.10': resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@ybouane/liquidglass@1.0.3': + resolution: {integrity: sha512-Ro/Q3vaEduvj1yUp/TVz9AYmxY+cYhnBj76eJ/Y9VQoPlRA0DWrKPQufEzrDSYzAWXL3qxiFibi5XQVW/egTnA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2298,6 +2379,9 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.40: resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} engines: {node: '>=6.0.0'} @@ -2323,6 +2407,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -2331,6 +2418,12 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + camera-controls@3.1.2: + resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} + engines: {node: '>=22.0.0', npm: '>=10.5.1'} + peerDependencies: + three: '>=0.126.1' + caniuse-lite@1.0.30001799: resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} @@ -2523,6 +2616,9 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + electron-to-chromium@1.5.381: resolution: {integrity: sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==} @@ -2706,6 +2802,9 @@ packages: fetchdts@0.1.7: resolution: {integrity: sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA==} + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} @@ -2809,6 +2908,9 @@ packages: httpxy@0.5.4: resolution: {integrity: sha512-URfeibL0kTH6VuIxxaJDXWQWEk8fKr+9L8MGv6CuAiNy0fGnoVhWbXBvJR1mkdsvCDUxvhX9cW60k2AhtH5s6w==} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2874,6 +2976,11 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + its-fine@2.0.0: + resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} + peerDependencies: + react: ^19.0.0 + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -3051,6 +3158,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meshoptimizer@1.1.1: + resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3243,6 +3353,9 @@ packages: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3344,6 +3457,15 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -3482,6 +3604,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + synckit@0.11.13: resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -3508,6 +3635,14 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + three-stdlib@2.36.1: + resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==} + peerDependencies: + three: '>=0.128.0' + + three@0.185.1: + resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -3878,6 +4013,24 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} + 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 + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -4006,6 +4159,8 @@ snapshots: '@date-fns/tz@1.5.0': {} + '@dimforge/rapier3d-compat@0.12.0': {} + '@emnapi/core@1.11.1': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -5224,6 +5379,26 @@ snapshots: '@radix-ui/rect@1.1.2': {} + '@react-three/fiber@9.6.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.185.1)': + dependencies: + '@babel/runtime': 7.29.7 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 2.0.0(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-use-measure: 2.1.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + scheduler: 0.27.0 + suspend-react: 0.1.3(react@19.2.7) + three: 0.185.1 + use-sync-external-store: 1.6.0(react@19.2.7) + zustand: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) + optionalDependencies: + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - '@types/react' + - immer + '@rolldown/binding-android-arm64@1.1.3': optional: true @@ -5277,6 +5452,11 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@shadergradient/react@2.4.20(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -5608,6 +5788,8 @@ snapshots: '@tanstack/react-router': 1.170.16(react-dom@19.2.7(react@19.2.7))(react@19.2.7) zod: 4.4.3 + '@tweenjs/tween.js@23.1.3': {} + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -5665,6 +5847,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/draco3d@1.4.10': {} + '@types/estree@1.0.9': {} '@types/json-schema@7.0.15': {} @@ -5673,14 +5857,33 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/offscreencanvas@2019.7.3': {} + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: '@types/react': 19.2.17 + '@types/react-reconciler@0.28.9(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + '@types/react@19.2.17': dependencies: csstype: 3.2.3 + '@types/stats.js@0.17.4': {} + + '@types/three@0.185.1': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + fflate: 0.8.3 + meshoptimizer: 1.1.1 + + '@types/webxr@0.5.24': {} + '@types/ws@8.18.1': dependencies: '@types/node': 22.20.0 @@ -5862,6 +6065,8 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@ybouane/liquidglass@1.0.3': {} + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: acorn: 8.17.0 @@ -5919,6 +6124,8 @@ snapshots: balanced-match@4.0.4: {} + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.40: {} binary-extensions@2.3.0: {} @@ -5944,10 +6151,19 @@ snapshots: node-releases: 2.0.50 update-browserslist-db: 1.2.3(browserslist@4.28.4) + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + callsites@3.1.0: {} camelcase-css@2.0.1: {} + camera-controls@3.1.2(three@0.185.1): + dependencies: + three: 0.185.1 + caniuse-lite@1.0.30001799: {} chai@6.2.2: {} @@ -6098,6 +6314,8 @@ snapshots: '@babel/runtime': 7.29.7 csstype: 3.2.3 + draco3d@1.5.7: {} + electron-to-chromium@1.5.381: {} embla-carousel-react@8.6.0(react@19.2.7): @@ -6290,6 +6508,8 @@ snapshots: fetchdts@0.1.7: {} + fflate@0.6.10: {} + fflate@0.8.3: {} file-entry-cache@8.0.0: @@ -6367,6 +6587,8 @@ snapshots: httpxy@0.5.4: {} + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -6418,6 +6640,13 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + its-fine@2.0.0(@types/react@19.2.17)(react@19.2.7): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.17) + react: 19.2.7 + transitivePeerDependencies: + - '@types/react' + jiti@1.21.7: {} jiti@2.7.0: {} @@ -6569,6 +6798,8 @@ snapshots: merge2@1.4.1: {} + meshoptimizer@1.1.1: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -6771,6 +7002,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + potpack@1.0.2: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: @@ -6863,6 +7096,12 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + react-use-measure@2.1.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + optionalDependencies: + react-dom: 19.2.7(react@19.2.7) + react@19.2.7: {} read-cache@1.0.0: @@ -6993,6 +7232,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + suspend-react@0.1.3(react@19.2.7): + dependencies: + react: 19.2.7 + synckit@0.11.13: dependencies: '@pkgr/core': 0.3.6 @@ -7039,6 +7282,18 @@ snapshots: dependencies: any-promise: 1.3.0 + three-stdlib@2.36.1(three@0.185.1): + 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.185.1 + + three@0.185.1: {} + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -7251,3 +7506,9 @@ snapshots: yocto-queue@0.1.0: {} zod@4.4.3: {} + + zustand@5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)): + optionalDependencies: + '@types/react': 19.2.17 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) diff --git a/src/lib/turso.server.ts b/src/lib/turso.server.ts index fe962d0..d79a563 100644 --- a/src/lib/turso.server.ts +++ b/src/lib/turso.server.ts @@ -6,9 +6,8 @@ let _initPromise: Promise | null = null; export function tursoClient(): Client { if (_client) return _client; - const url = process.env.TURSO_DATABASE_URL; + const url = process.env.TURSO_DATABASE_URL || "file:local.db"; const authToken = process.env.TURSO_AUTH_TOKEN; - if (!url) throw new Error("TURSO_DATABASE_URL is not set"); _client = createClient({ url, authToken }); return _client; } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index ee27816..830888a 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -2,7 +2,7 @@ // Section ApiSection ajoutée après SimulatorCTASection import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, lazy, Suspense } from "react"; import { normalize, sanitizeSearchInput, @@ -10,6 +10,115 @@ import { type Scrutin, photoUrl, } from "@/lib/api"; + +import React from "react"; + +class WebGLErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> { + constructor(props: { children: React.ReactNode }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + componentDidCatch(error: any, errorInfo: any) { + console.warn("WebGL or Three.js component crashed. Gracefully falling back:", error, errorInfo); + } + + render() { + if (this.state.hasError) { + return
; + } + return this.props.children; + } +} + +// Client-only component wrapper for ShaderGradient +const ShaderGradientBackground = () => { + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + return ( +
+ {/* Instant, stunning CSS fallback gradient that renders during SSR & initial mount */} +
+ {mounted && ( +
+ + + + + +
+ )} +
+ ); +}; + +const LazyShaderGradient = lazy(async () => { + const { ShaderGradientCanvas, ShaderGradient } = await import("@shadergradient/react"); + const ShaderGradientAny = ShaderGradient as any; + return { + default: () => ( + + + + ) + }; +}); import { getHomeStats, getLatestScrutins, @@ -34,10 +143,18 @@ export const Route = createFileRoute("/")({ }), }), loader: async () => { - const [stats, latest] = await Promise.all([ - getHomeStats(), - getLatestScrutins(), - ]); + let stats = { deputesCount: 577, scrutinsCount: 124, groupesCount: 11 }; + let latest: any[] = []; + try { + const [dbStats, dbLatest] = await Promise.all([ + getHomeStats().catch(() => ({ deputesCount: 577, scrutinsCount: 124, groupesCount: 11 })), + getLatestScrutins().catch(() => []), + ]); + stats = dbStats; + latest = dbLatest; + } catch (e) { + console.warn("Database stats load failed:", e); + } const posts = getAllPosts(); return { stats, latest, latestPost: posts[0] || null }; }, @@ -100,27 +217,8 @@ function Home() {
-