your personal website on atproto - mirror blento.app

Merge branch 'main' into music-streaming-card

authored by Florian and committed by GitHub 76dba08b c1d70c8a

+674 -13
+5
package.json
··· 50 50 "@atcute/tid": "^1.1.1", 51 51 "@cloudflare/workers-types": "^4.20260123.0", 52 52 "@ethercorps/sveltekit-og": "^4.2.1", 53 + "@foxui/3d": "^0.4.7", 53 54 "@foxui/colors": "^0.4.7", 54 55 "@foxui/core": "^0.4.7", 55 56 "@foxui/social": "^0.4.7", ··· 57 58 "@foxui/visual": "^0.4.7", 58 59 "@number-flow/svelte": "^0.3.10", 59 60 "@tailwindcss/typography": "^0.5.19", 61 + "@threlte/core": "^8.3.1", 62 + "@threlte/extras": "^9.7.1", 60 63 "@tiptap/core": "^3.16.0", 61 64 "@tiptap/extension-document": "^3.16.0", 62 65 "@tiptap/extension-image": "^3.16.0", ··· 65 68 "@tiptap/extension-placeholder": "^3.16.0", 66 69 "@tiptap/extension-text": "^3.16.0", 67 70 "@tiptap/starter-kit": "^3.16.0", 71 + "@types/three": "^0.176.0", 68 72 "bits-ui": "^2.15.4", 69 73 "clsx": "^2.1.1", 70 74 "gsap": "^3.14.2", ··· 80 84 "svelte-sonner": "^1.0.7", 81 85 "tailwind-merge": "^3.4.0", 82 86 "tailwind-variants": "^3.2.2", 87 + "three": "^0.176.0", 83 88 "turndown": "^7.2.2", 84 89 "wrangler": "^4.60.0" 85 90 },
+257
pnpm-lock.yaml
··· 41 41 '@ethercorps/sveltekit-og': 42 42 specifier: ^4.2.1 43 43 version: 4.2.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2))) 44 + '@foxui/3d': 45 + specifier: ^0.4.7 46 + version: 0.4.7(svelte@5.48.0)(tailwindcss@4.1.18) 44 47 '@foxui/colors': 45 48 specifier: ^0.4.7 46 49 version: 0.4.7(svelte@5.48.0)(tailwindcss@4.1.18) ··· 62 65 '@tailwindcss/typography': 63 66 specifier: ^0.5.19 64 67 version: 0.5.19(tailwindcss@4.1.18) 68 + '@threlte/core': 69 + specifier: ^8.3.1 70 + version: 8.3.1(svelte@5.48.0)(three@0.176.0) 71 + '@threlte/extras': 72 + specifier: ^9.7.1 73 + version: 9.7.1(@types/three@0.176.0)(svelte@5.48.0)(three@0.176.0) 65 74 '@tiptap/core': 66 75 specifier: ^3.16.0 67 76 version: 3.16.0(@tiptap/pm@3.16.0) ··· 86 95 '@tiptap/starter-kit': 87 96 specifier: ^3.16.0 88 97 version: 3.16.0 98 + '@types/three': 99 + specifier: ^0.176.0 100 + version: 0.176.0 89 101 bits-ui: 90 102 specifier: ^2.15.4 91 103 version: 2.15.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.48.0) ··· 131 143 tailwind-variants: 132 144 specifier: ^3.2.2 133 145 version: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18) 146 + three: 147 + specifier: ^0.176.0 148 + version: 0.176.0 134 149 turndown: 135 150 specifier: ^7.2.2 136 151 version: 7.2.2 ··· 329 344 resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 330 345 engines: {node: '>=12'} 331 346 347 + '@dimforge/rapier3d-compat@0.12.0': 348 + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} 349 + 332 350 '@emnapi/runtime@1.8.1': 333 351 resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} 334 352 ··· 708 726 709 727 '@floating-ui/utils@0.2.10': 710 728 resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} 729 + 730 + '@foxui/3d@0.4.7': 731 + resolution: {integrity: sha512-6aFWbdvMPbVqvRlr1gc438RoWE/1+B7/dB1b682tl+K1ycMhWkgMzicckxafLY2A1haOv8bgAArZJ+Lt63RMNg==} 732 + peerDependencies: 733 + svelte: '>=5' 734 + tailwindcss: '>=3' 711 735 712 736 '@foxui/colors@0.4.7': 713 737 resolution: {integrity: sha512-P40GBuwerikysWctVL1KSUrjxzlAf5j1jZ0z6+pzkWdfTv8wGxXlJcvJCkA1MkTeatLkyBrHLrIF17u2iRrMkw==} ··· 1313 1337 '@texel/color@1.1.11': 1314 1338 resolution: {integrity: sha512-/3kKgfBqzrRfLl4RsEccx+Yfj1kVL6Bh6DejVWZ+DPg/jJdcfdYZ5fpD1nXFwWd8OQNATjz+WqsfQfUynSsgRg==} 1315 1339 1340 + '@threejs-kit/instanced-sprite-mesh@2.5.1': 1341 + resolution: {integrity: sha512-pmt1ALRhbHhCJQTj2FuthH6PeLIeaM4hOuS2JO3kWSwlnvx/9xuUkjFR3JOi/myMqsH7pSsLIROSaBxDfttjeA==} 1342 + peerDependencies: 1343 + three: '>=0.170.0' 1344 + 1345 + '@threlte/core@8.3.1': 1346 + resolution: {integrity: sha512-qKjjNCQ+40hyeBcfOMh/8ef5x/j5PG5Wmo/L9Ye0aDCcdD6fCewWxfp7tV/J3CxPzX1dEp1JGK7sjyc7ntZSrg==} 1347 + peerDependencies: 1348 + svelte: '>=5' 1349 + three: '>=0.160' 1350 + 1351 + '@threlte/extras@9.7.1': 1352 + resolution: {integrity: sha512-SGm59HDCdHxADFHuweHfFDknwubkCPodyK0pbfsVtOWWOX26gE2xfK7aKolh6YFDiPAjWjGxN0jIgkNbbr1ohg==} 1353 + peerDependencies: 1354 + svelte: '>=5' 1355 + three: '>=0.160' 1356 + 1316 1357 '@tiptap/core@3.16.0': 1317 1358 resolution: {integrity: sha512-XegRaNuoQ/guzBQU2xHxOwFXXrtoXW9tiyXDhssSqylvZmBVSlRIPNHA6ArkHBKm6ehLf6+6Y9fF3uky1yCXYQ==} 1318 1359 peerDependencies: ··· 1449 1490 '@tiptap/starter-kit@3.16.0': 1450 1491 resolution: {integrity: sha512-eWi+77SgKyhSx91Hmn32ER+gPN6FfInGtod4A+XxSG+LqS/sn6kpUEdowYrnqiZzhUXZCSTSJvC+UcMUZHOkxQ==} 1451 1492 1493 + '@tweenjs/tween.js@23.1.3': 1494 + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} 1495 + 1452 1496 '@types/bun@1.3.6': 1453 1497 resolution: {integrity: sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA==} 1454 1498 ··· 1485 1529 '@types/pbf@3.0.5': 1486 1530 resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} 1487 1531 1532 + '@types/stats.js@0.17.4': 1533 + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} 1534 + 1488 1535 '@types/supercluster@7.1.3': 1489 1536 resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} 1490 1537 1538 + '@types/three@0.176.0': 1539 + resolution: {integrity: sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==} 1540 + 1491 1541 '@types/turndown@5.0.6': 1492 1542 resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} 1543 + 1544 + '@types/webxr@0.5.24': 1545 + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} 1493 1546 1494 1547 '@typescript-eslint/eslint-plugin@8.53.1': 1495 1548 resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==} ··· 1556 1609 '@use-gesture/vanilla@10.3.1': 1557 1610 resolution: {integrity: sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==} 1558 1611 1612 + '@webgpu/types@0.1.69': 1613 + resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} 1614 + 1559 1615 acorn-jsx@5.3.2: 1560 1616 resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 1561 1617 peerDependencies: ··· 1594 1650 resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} 1595 1651 engines: {node: '>= 0.4'} 1596 1652 1653 + bidi-js@1.0.3: 1654 + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} 1655 + 1597 1656 bits-ui@1.8.0: 1598 1657 resolution: {integrity: sha512-CXD6Orp7l8QevNDcRPLXc/b8iMVgxDWT2LyTwsdLzJKh9CxesOmPuNePSPqAxKoT59FIdU4aFPS1k7eBdbaCxg==} 1599 1658 engines: {node: '>=18', pnpm: '>=8.7.0'} ··· 1628 1687 1629 1688 camelize@1.0.1: 1630 1689 resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} 1690 + 1691 + camera-controls@3.1.2: 1692 + resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} 1693 + engines: {node: '>=22.0.0', npm: '>=10.5.1'} 1694 + peerDependencies: 1695 + three: '>=0.126.1' 1631 1696 1632 1697 canvas-confetti@1.9.4: 1633 1698 resolution: {integrity: sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==} ··· 1753 1818 devalue@5.6.2: 1754 1819 resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} 1755 1820 1821 + diet-sprite@0.0.1: 1822 + resolution: {integrity: sha512-zSHI2WDAn1wJqJYxcmjWfJv3Iw8oL9reQIbEyx2x2/EZ4/qmUTIo8/5qOCurnAcq61EwtJJaZ0XTy2NRYqpB5A==} 1823 + 1756 1824 dom-serializer@2.0.0: 1757 1825 resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} 1758 1826 ··· 1765 1833 1766 1834 domutils@3.2.2: 1767 1835 resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 1836 + 1837 + earcut@2.2.4: 1838 + resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} 1768 1839 1769 1840 earcut@3.0.2: 1770 1841 resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} ··· 1901 1972 1902 1973 fflate@0.7.4: 1903 1974 resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} 1975 + 1976 + fflate@0.8.2: 1977 + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} 1904 1978 1905 1979 file-entry-cache@8.0.0: 1906 1980 resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} ··· 2157 2231 resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} 2158 2232 hasBin: true 2159 2233 2234 + maath@0.10.8: 2235 + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} 2236 + peerDependencies: 2237 + '@types/three': '>=0.134.0' 2238 + three: '>=0.134.0' 2239 + 2160 2240 magic-string@0.30.21: 2161 2241 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 2162 2242 ··· 2178 2258 mdurl@2.0.0: 2179 2259 resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} 2180 2260 2261 + meshoptimizer@0.18.1: 2262 + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} 2263 + 2181 2264 mini-svg-data-uri@1.4.4: 2182 2265 resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} 2183 2266 hasBin: true ··· 2193 2276 minimatch@9.0.5: 2194 2277 resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 2195 2278 engines: {node: '>=16 || 14 >=14.17'} 2279 + 2280 + mitt@3.0.1: 2281 + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} 2196 2282 2197 2283 mlly@1.8.0: 2198 2284 resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} ··· 2518 2604 resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} 2519 2605 engines: {node: '>=8'} 2520 2606 2607 + require-from-string@2.0.2: 2608 + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 2609 + engines: {node: '>=0.10.0'} 2610 + 2521 2611 resolve-from@4.0.0: 2522 2612 resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 2523 2613 engines: {node: '>=4'} ··· 2708 2798 resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} 2709 2799 engines: {node: '>=6'} 2710 2800 2801 + three-instanced-uniforms-mesh@0.52.4: 2802 + resolution: {integrity: sha512-YwDBy05hfKZQtU+Rp0KyDf9yH4GxfhxMbVt9OYruxdgLfPwmDG5oAbGoW0DrKtKZSM3BfFcCiejiOHCjFBTeng==} 2803 + peerDependencies: 2804 + three: '>=0.125.0' 2805 + 2806 + three-mesh-bvh@0.9.7: 2807 + resolution: {integrity: sha512-EYSJbykeAjhVxwZjuUYq/kelIbqBoV9sbAgvZ+j1xCgZyNYSkr51WDJWS4WIfK2OX6YcjBGoTicX4RoOVQzx0g==} 2808 + peerDependencies: 2809 + three: '>= 0.159.0' 2810 + 2811 + three-perf@1.0.11: 2812 + resolution: {integrity: sha512-OgBpZjwL+csQKGKZjpkH/QHdbGFMxqngMbSEJeSnVNfXDYd6On7WHNv/GhUZH4YxIpNMwMahBWrNnsJvnbSJHQ==} 2813 + peerDependencies: 2814 + three: '>=0.170' 2815 + 2816 + three-viewport-gizmo@2.2.0: 2817 + resolution: {integrity: sha512-Jo9Liur1rUmdKk75FZumLU/+hbF+RtJHi1qsKZpntjKlCYScK6tjbYoqvJ9M+IJphrlQJF5oReFW7Sambh0N4Q==} 2818 + peerDependencies: 2819 + three: '>=0.162.0 <1.0.0' 2820 + 2821 + three@0.176.0: 2822 + resolution: {integrity: sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==} 2823 + 2711 2824 tiny-inflate@1.0.3: 2712 2825 resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} 2713 2826 ··· 2726 2839 resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} 2727 2840 engines: {node: '>=6'} 2728 2841 2842 + troika-three-text@0.52.4: 2843 + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} 2844 + peerDependencies: 2845 + three: '>=0.125.0' 2846 + 2847 + troika-three-utils@0.52.4: 2848 + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} 2849 + peerDependencies: 2850 + three: '>=0.125.0' 2851 + 2852 + troika-worker-utils@0.52.0: 2853 + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} 2854 + 2729 2855 ts-api-utils@2.4.0: 2730 2856 resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} 2731 2857 engines: {node: '>=18.12'} ··· 2737 2863 2738 2864 turndown@7.2.2: 2739 2865 resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==} 2866 + 2867 + tweakpane@3.1.10: 2868 + resolution: {integrity: sha512-rqwnl/pUa7+inhI2E9ayGTqqP0EPOOn/wVvSWjZsRbZUItzNShny7pzwL3hVlaN4m9t/aZhsP0aFQ9U5VVR2VQ==} 2740 2869 2741 2870 type-check@0.4.0: 2742 2871 resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} ··· 2849 2978 w3c-keyname@2.2.8: 2850 2979 resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} 2851 2980 2981 + webgl-sdf-generator@1.1.1: 2982 + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} 2983 + 2852 2984 whatwg-encoding@3.1.1: 2853 2985 resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} 2854 2986 engines: {node: '>=18'} ··· 3077 3209 '@cspotcode/source-map-support@0.8.1': 3078 3210 dependencies: 3079 3211 '@jridgewell/trace-mapping': 0.3.9 3212 + 3213 + '@dimforge/rapier3d-compat@0.12.0': {} 3080 3214 3081 3215 '@emnapi/runtime@1.8.1': 3082 3216 dependencies: ··· 3317 3451 '@floating-ui/utils': 0.2.10 3318 3452 3319 3453 '@floating-ui/utils@0.2.10': {} 3454 + 3455 + '@foxui/3d@0.4.7(svelte@5.48.0)(tailwindcss@4.1.18)': 3456 + dependencies: 3457 + '@foxui/core': 0.4.7(svelte@5.48.0)(tailwindcss@4.1.18) 3458 + '@threlte/core': 8.3.1(svelte@5.48.0)(three@0.176.0) 3459 + '@threlte/extras': 9.7.1(@types/three@0.176.0)(svelte@5.48.0)(three@0.176.0) 3460 + '@types/three': 0.176.0 3461 + bits-ui: 1.8.0(svelte@5.48.0) 3462 + svelte: 5.48.0 3463 + tailwindcss: 4.1.18 3464 + three: 0.176.0 3320 3465 3321 3466 '@foxui/colors@0.4.7(svelte@5.48.0)(tailwindcss@4.1.18)': 3322 3467 dependencies: ··· 3814 3959 3815 3960 '@texel/color@1.1.11': {} 3816 3961 3962 + '@threejs-kit/instanced-sprite-mesh@2.5.1(@types/three@0.176.0)(three@0.176.0)': 3963 + dependencies: 3964 + diet-sprite: 0.0.1 3965 + earcut: 2.2.4 3966 + maath: 0.10.8(@types/three@0.176.0)(three@0.176.0) 3967 + three: 0.176.0 3968 + three-instanced-uniforms-mesh: 0.52.4(three@0.176.0) 3969 + troika-three-utils: 0.52.4(three@0.176.0) 3970 + transitivePeerDependencies: 3971 + - '@types/three' 3972 + 3973 + '@threlte/core@8.3.1(svelte@5.48.0)(three@0.176.0)': 3974 + dependencies: 3975 + mitt: 3.0.1 3976 + svelte: 5.48.0 3977 + three: 0.176.0 3978 + 3979 + '@threlte/extras@9.7.1(@types/three@0.176.0)(svelte@5.48.0)(three@0.176.0)': 3980 + dependencies: 3981 + '@threejs-kit/instanced-sprite-mesh': 2.5.1(@types/three@0.176.0)(three@0.176.0) 3982 + camera-controls: 3.1.2(three@0.176.0) 3983 + svelte: 5.48.0 3984 + three: 0.176.0 3985 + three-mesh-bvh: 0.9.7(three@0.176.0) 3986 + three-perf: 1.0.11(three@0.176.0) 3987 + three-viewport-gizmo: 2.2.0(three@0.176.0) 3988 + troika-three-text: 0.52.4(three@0.176.0) 3989 + transitivePeerDependencies: 3990 + - '@types/three' 3991 + 3817 3992 '@tiptap/core@3.16.0(@tiptap/pm@3.16.0)': 3818 3993 dependencies: 3819 3994 '@tiptap/pm': 3.16.0 ··· 3967 4142 '@tiptap/extension-underline': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0)) 3968 4143 '@tiptap/extensions': 3.16.0(@tiptap/core@3.16.0(@tiptap/pm@3.16.0))(@tiptap/pm@3.16.0) 3969 4144 '@tiptap/pm': 3.16.0 4145 + 4146 + '@tweenjs/tween.js@23.1.3': {} 3970 4147 3971 4148 '@types/bun@1.3.6': 3972 4149 dependencies: ··· 4001 4178 4002 4179 '@types/pbf@3.0.5': {} 4003 4180 4181 + '@types/stats.js@0.17.4': {} 4182 + 4004 4183 '@types/supercluster@7.1.3': 4005 4184 dependencies: 4006 4185 '@types/geojson': 7946.0.16 4007 4186 4187 + '@types/three@0.176.0': 4188 + dependencies: 4189 + '@dimforge/rapier3d-compat': 0.12.0 4190 + '@tweenjs/tween.js': 23.1.3 4191 + '@types/stats.js': 0.17.4 4192 + '@types/webxr': 0.5.24 4193 + '@webgpu/types': 0.1.69 4194 + fflate: 0.8.2 4195 + meshoptimizer: 0.18.1 4196 + 4008 4197 '@types/turndown@5.0.6': {} 4198 + 4199 + '@types/webxr@0.5.24': {} 4009 4200 4010 4201 '@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': 4011 4202 dependencies: ··· 4103 4294 '@use-gesture/vanilla@10.3.1': 4104 4295 dependencies: 4105 4296 '@use-gesture/core': 10.3.1 4297 + 4298 + '@webgpu/types@0.1.69': {} 4106 4299 4107 4300 acorn-jsx@5.3.2(acorn@8.15.0): 4108 4301 dependencies: ··· 4133 4326 4134 4327 base64-js@0.0.8: {} 4135 4328 4329 + bidi-js@1.0.3: 4330 + dependencies: 4331 + require-from-string: 2.0.2 4332 + 4136 4333 bits-ui@1.8.0(svelte@5.48.0): 4137 4334 dependencies: 4138 4335 '@floating-ui/core': 1.7.3 ··· 4178 4375 callsites@3.1.0: {} 4179 4376 4180 4377 camelize@1.0.1: {} 4378 + 4379 + camera-controls@3.1.2(three@0.176.0): 4380 + dependencies: 4381 + three: 0.176.0 4181 4382 4182 4383 canvas-confetti@1.9.4: {} 4183 4384 ··· 4298 4499 4299 4500 devalue@5.6.2: {} 4300 4501 4502 + diet-sprite@0.0.1: {} 4503 + 4301 4504 dom-serializer@2.0.0: 4302 4505 dependencies: 4303 4506 domelementtype: 2.3.0 ··· 4315 4518 dom-serializer: 2.0.0 4316 4519 domelementtype: 2.3.0 4317 4520 domhandler: 5.0.3 4521 + 4522 + earcut@2.2.4: {} 4318 4523 4319 4524 earcut@3.0.2: {} 4320 4525 ··· 4512 4717 4513 4718 fflate@0.7.4: {} 4514 4719 4720 + fflate@0.8.2: {} 4721 + 4515 4722 file-entry-cache@8.0.0: 4516 4723 dependencies: 4517 4724 flat-cache: 4.0.1 ··· 4713 4920 4714 4921 lz-string@1.5.0: {} 4715 4922 4923 + maath@0.10.8(@types/three@0.176.0)(three@0.176.0): 4924 + dependencies: 4925 + '@types/three': 0.176.0 4926 + three: 0.176.0 4927 + 4716 4928 magic-string@0.30.21: 4717 4929 dependencies: 4718 4930 '@jridgewell/sourcemap-codec': 1.5.5 ··· 4765 4977 4766 4978 mdurl@2.0.0: {} 4767 4979 4980 + meshoptimizer@0.18.1: {} 4981 + 4768 4982 mini-svg-data-uri@1.4.4: {} 4769 4983 4770 4984 miniflare@4.20260120.0: ··· 4787 5001 minimatch@9.0.5: 4788 5002 dependencies: 4789 5003 brace-expansion: 2.0.2 5004 + 5005 + mitt@3.0.1: {} 4790 5006 4791 5007 mlly@1.8.0: 4792 5008 dependencies: ··· 5083 5299 5084 5300 regexparam@3.0.0: {} 5085 5301 5302 + require-from-string@2.0.2: {} 5303 + 5086 5304 resolve-from@4.0.0: {} 5087 5305 5088 5306 resolve-protobuf-schema@2.1.0: ··· 5331 5549 5332 5550 tapable@2.3.0: {} 5333 5551 5552 + three-instanced-uniforms-mesh@0.52.4(three@0.176.0): 5553 + dependencies: 5554 + three: 0.176.0 5555 + troika-three-utils: 0.52.4(three@0.176.0) 5556 + 5557 + three-mesh-bvh@0.9.7(three@0.176.0): 5558 + dependencies: 5559 + three: 0.176.0 5560 + 5561 + three-perf@1.0.11(three@0.176.0): 5562 + dependencies: 5563 + three: 0.176.0 5564 + troika-three-text: 0.52.4(three@0.176.0) 5565 + tweakpane: 3.1.10 5566 + 5567 + three-viewport-gizmo@2.2.0(three@0.176.0): 5568 + dependencies: 5569 + three: 0.176.0 5570 + 5571 + three@0.176.0: {} 5572 + 5334 5573 tiny-inflate@1.0.3: {} 5335 5574 5336 5575 tinyglobby@0.2.15: ··· 5344 5583 5345 5584 totalist@3.0.1: {} 5346 5585 5586 + troika-three-text@0.52.4(three@0.176.0): 5587 + dependencies: 5588 + bidi-js: 1.0.3 5589 + three: 0.176.0 5590 + troika-three-utils: 0.52.4(three@0.176.0) 5591 + troika-worker-utils: 0.52.0 5592 + webgl-sdf-generator: 1.1.1 5593 + 5594 + troika-three-utils@0.52.4(three@0.176.0): 5595 + dependencies: 5596 + three: 0.176.0 5597 + 5598 + troika-worker-utils@0.52.0: {} 5599 + 5347 5600 ts-api-utils@2.4.0(typescript@5.9.3): 5348 5601 dependencies: 5349 5602 typescript: 5.9.3 ··· 5353 5606 turndown@7.2.2: 5354 5607 dependencies: 5355 5608 '@mixmark-io/domino': 2.2.0 5609 + 5610 + tweakpane@3.1.10: {} 5356 5611 5357 5612 type-check@0.4.0: 5358 5613 dependencies: ··· 5434 5689 vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2) 5435 5690 5436 5691 w3c-keyname@2.2.8: {} 5692 + 5693 + webgl-sdf-generator@1.1.1: {} 5437 5694 5438 5695 whatwg-encoding@3.1.1: 5439 5696 dependencies:
+103
src/lib/cards/Model3DCard/CreateModel3DCardModal.svelte
··· 1 + <script lang="ts"> 2 + import { Alert, Button, Modal, Subheading } from '@foxui/core'; 3 + import type { CreationModalComponentProps } from '../types'; 4 + 5 + let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 6 + 7 + let errorMessage = $state(''); 8 + let fileInput = $state<HTMLInputElement | undefined>(undefined); 9 + 10 + function handleFileSelect(event: Event) { 11 + const input = event.target as HTMLInputElement; 12 + const file = input.files?.[0]; 13 + 14 + if (!file) return; 15 + 16 + const extension = file.name.toLowerCase().split('.').pop(); 17 + if (!['gltf', 'glb', 'stl', 'fbx'].includes(extension || '')) { 18 + errorMessage = 'Please select a .gltf, .glb, .stl, or .fbx file'; 19 + return; 20 + } 21 + 22 + errorMessage = ''; 23 + item.cardData.modelFile = { 24 + blob: file, 25 + objectUrl: URL.createObjectURL(file), 26 + name: file.name, 27 + type: extension 28 + }; 29 + } 30 + 31 + function clearFile() { 32 + if (item.cardData.modelFile?.objectUrl) { 33 + URL.revokeObjectURL(item.cardData.modelFile.objectUrl); 34 + } 35 + item.cardData.modelFile = undefined; 36 + } 37 + 38 + function canCreate() { 39 + if (!item.cardData.modelFile) { 40 + errorMessage = 'Please upload a file'; 41 + return false; 42 + } 43 + return true; 44 + } 45 + </script> 46 + 47 + <Modal open={true} closeButton={false}> 48 + <Subheading>Add a 3D Model</Subheading> 49 + 50 + <div> 51 + <p class="text-base-600 dark:text-base-400 mb-2 text-sm"> 52 + Upload a 3D model file (.glb, .stl, .fbx, or .gltf) 53 + </p> 54 + {#if item.cardData.modelFile} 55 + <div 56 + class="bg-base-100 dark:bg-base-800 flex items-center justify-between rounded-lg border p-3" 57 + > 58 + <span class="text-sm">{item.cardData.modelFile.name}</span> 59 + <Button size="sm" variant="ghost" onclick={clearFile}>Remove</Button> 60 + </div> 61 + {:else} 62 + <input 63 + bind:this={fileInput} 64 + type="file" 65 + accept=".gltf,.glb,.stl,.fbx" 66 + onchange={handleFileSelect} 67 + class="hidden" 68 + /> 69 + <Button variant="secondary" onclick={() => fileInput?.click()} class="w-full"> 70 + <svg 71 + xmlns="http://www.w3.org/2000/svg" 72 + fill="none" 73 + viewBox="0 0 24 24" 74 + stroke-width="1.5" 75 + stroke="currentColor" 76 + class="mr-2 size-5" 77 + > 78 + <path 79 + stroke-linecap="round" 80 + stroke-linejoin="round" 81 + d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" 82 + /> 83 + </svg> 84 + Choose File 85 + </Button> 86 + {/if} 87 + </div> 88 + 89 + {#if errorMessage} 90 + <Alert type="error" title="Error"><span>{errorMessage}</span></Alert> 91 + {/if} 92 + 93 + <div class="mt-4 flex justify-end gap-2"> 94 + <Button onclick={oncancel} variant="ghost">Cancel</Button> 95 + <Button 96 + onclick={() => { 97 + if (canCreate()) oncreate(); 98 + }} 99 + > 100 + Create 101 + </Button> 102 + </div> 103 + </Modal>
+74
src/lib/cards/Model3DCard/Model3DCard.svelte
··· 1 + <script lang="ts"> 2 + import { Canvas } from '@threlte/core'; 3 + import { CineonToneMapping } from 'three'; 4 + import type { ContentComponentProps } from '../types'; 5 + import Model3DScene from './Model3DScene.svelte'; 6 + import { getDidContext } from '$lib/website/context'; 7 + import { getBlobURL } from '$lib/atproto'; 8 + import type { Did } from '@atcute/lexicons'; 9 + import { onMount } from 'svelte'; 10 + 11 + let { item }: ContentComponentProps = $props(); 12 + 13 + let isHovering = $state(false); 14 + let objectUrl = $state<string | undefined>(undefined); 15 + let isLoading = $state(false); 16 + 17 + const did = getDidContext(); 18 + 19 + // Fetch blob from PDS and create object URL (like VideoCard does) 20 + onMount(async () => { 21 + if (item.cardData.modelBlob?.$type === 'blob') { 22 + isLoading = true; 23 + try { 24 + const pdsUrl = await getBlobURL({ did: did as Did, blob: item.cardData.modelBlob }); 25 + const response = await fetch(pdsUrl); 26 + if (!response.ok) throw new Error(response.statusText); 27 + const blob = await response.blob(); 28 + objectUrl = URL.createObjectURL(blob); 29 + } catch (e) { 30 + console.error('Failed to load 3D model:', e); 31 + } finally { 32 + isLoading = false; 33 + } 34 + } 35 + }); 36 + 37 + // Get the model URL from various sources 38 + let modelUrl = $derived.by(() => { 39 + // Local file (during editing before save) 40 + if (item.cardData.modelFile?.objectUrl) { 41 + return item.cardData.modelFile.objectUrl; 42 + } 43 + 44 + // Uploaded blob (after save) - use fetched object URL 45 + if (item.cardData.modelBlob?.$type === 'blob') { 46 + return objectUrl; 47 + } 48 + 49 + return undefined; 50 + }); 51 + 52 + let modelType = $derived(item.cardData.modelFile?.type || item.cardData.modelType || 'gltf') as 53 + | 'gltf' 54 + | 'stl' 55 + | 'fbx'; 56 + </script> 57 + 58 + <div 59 + class="absolute inset-0 h-full w-full" 60 + role="img" 61 + aria-label="3D model viewer" 62 + onpointerenter={() => (isHovering = true)} 63 + onpointerleave={() => (isHovering = false)} 64 + > 65 + {#if modelUrl} 66 + <Canvas toneMapping={CineonToneMapping}> 67 + <Model3DScene path={modelUrl} hover={isHovering} {modelType} /> 68 + </Canvas> 69 + {:else if isLoading} 70 + <div class="flex h-full items-center justify-center text-sm opacity-50">Loading model...</div> 71 + {:else} 72 + <div class="flex h-full items-center justify-center text-sm opacity-50">No model loaded</div> 73 + {/if} 74 + </div>
+161
src/lib/cards/Model3DCard/Model3DScene.svelte
··· 1 + <script lang="ts"> 2 + import { T, useTask, useThrelte } from '@threlte/core'; 3 + import { GLTF, OrbitControls } from '@threlte/extras'; 4 + import type { ThrelteGltf } from '@threlte/extras'; 5 + import { onMount } from 'svelte'; 6 + import { 7 + Box3, 8 + Group, 9 + Vector3, 10 + BufferGeometry, 11 + Mesh, 12 + MeshStandardMaterial, 13 + type Object3D 14 + } from 'three'; 15 + import { STLLoader } from 'three/addons/loaders/STLLoader.js'; 16 + import { FBXLoader } from 'three/addons/loaders/FBXLoader.js'; 17 + 18 + let { 19 + path, 20 + hover = false, 21 + modelType = 'gltf' 22 + }: { 23 + path: string; 24 + hover?: boolean; 25 + modelType?: 'gltf' | 'stl' | 'fbx'; 26 + } = $props(); 27 + 28 + let rotation = $state(0); 29 + let group: Group | undefined = $state(); 30 + let stlMesh: Mesh | undefined = $state(); 31 + let stlLoaded = $state(false); 32 + let fbxGroup: Group | undefined = $state(); 33 + let fbxLoaded = $state(false); 34 + 35 + const { start, stop } = useTask((delta: number) => { 36 + rotation += delta * 0.5; 37 + }); 38 + 39 + $effect(() => { 40 + if (hover) { 41 + start(); 42 + } else { 43 + stop(); 44 + } 45 + }); 46 + 47 + const { renderer } = useThrelte(); 48 + 49 + onMount(() => { 50 + renderer.toneMappingExposure = 0.7; 51 + }); 52 + 53 + // Load STL file 54 + $effect(() => { 55 + if (modelType === 'stl' && path) { 56 + stlLoaded = false; 57 + const loader = new STLLoader(); 58 + loader.load( 59 + path, 60 + (geometry: BufferGeometry) => { 61 + // Center and scale the geometry 62 + geometry.computeBoundingBox(); 63 + const box = geometry.boundingBox; 64 + if (box) { 65 + const size = new Vector3(); 66 + box.getSize(size); 67 + const center = new Vector3(); 68 + box.getCenter(center); 69 + 70 + const maxSize = Math.max(size.x, size.y, size.z); 71 + const scale = 1.2 / maxSize; 72 + 73 + geometry.translate(-center.x, -center.y, -center.z); 74 + geometry.scale(scale, scale, scale); 75 + } 76 + 77 + // Create mesh with a nice material 78 + const material = new MeshStandardMaterial({ 79 + color: 0x808080, 80 + metalness: 0.3, 81 + roughness: 0.6 82 + }); 83 + 84 + stlMesh = new Mesh(geometry, material); 85 + stlLoaded = true; 86 + }, 87 + undefined, 88 + (error) => { 89 + console.error('Error loading STL:', error); 90 + } 91 + ); 92 + } 93 + }); 94 + 95 + // Load FBX file 96 + $effect(() => { 97 + if (modelType === 'fbx' && path) { 98 + fbxLoaded = false; 99 + const loader = new FBXLoader(); 100 + loader.load( 101 + path, 102 + (object: Group) => { 103 + // Center and scale the model 104 + const box = new Box3().setFromObject(object); 105 + const size = box.getSize(new Vector3()); 106 + const center = box.getCenter(new Vector3()); 107 + 108 + const maxSize = Math.max(size.x, size.y, size.z); 109 + const scale = 1.2 / maxSize; 110 + 111 + object.scale.set(scale, scale, scale); 112 + object.position.set(-center.x * scale, -center.y * scale, -center.z * scale); 113 + 114 + fbxGroup = object; 115 + fbxLoaded = true; 116 + }, 117 + undefined, 118 + (error) => { 119 + console.error('Error loading FBX:', error); 120 + } 121 + ); 122 + } 123 + }); 124 + 125 + function handleGltfLoad(gltf: ThrelteGltf) { 126 + if (!group) return; 127 + 128 + const box = new Box3().setFromObject(gltf.scene as Object3D); 129 + const size = box.getSize(new Vector3()); 130 + const center = box.getCenter(new Vector3()); 131 + 132 + let maxSize = Math.max(size.x, size.y, size.z); 133 + let scale = 1.2 / maxSize; 134 + 135 + group.scale.set(scale, scale, scale); 136 + group.position.set(-center.x * scale, -center.y * scale, -center.z * scale); 137 + } 138 + </script> 139 + 140 + <T.PerspectiveCamera makeDefault position={[0, 0, 2.5]} fov={50} near={0.1} far={100}> 141 + <OrbitControls enableZoom={false} enablePan={false} /> 142 + </T.PerspectiveCamera> 143 + 144 + <T.DirectionalLight args={[0xffffff, 2]} position={[-1, 1, 1]} /> 145 + <T.AmbientLight args={[0xffffff, 0.7]} /> 146 + 147 + <T.Group rotation={[0.3, rotation + 0.5, 0]}> 148 + {#if modelType === 'stl'} 149 + {#if stlLoaded && stlMesh} 150 + <T is={stlMesh} /> 151 + {/if} 152 + {:else if modelType === 'fbx'} 153 + {#if fbxLoaded && fbxGroup} 154 + <T is={fbxGroup} /> 155 + {/if} 156 + {:else} 157 + <T.Group bind:ref={group}> 158 + <GLTF url={path} onload={handleGltfLoad} /> 159 + </T.Group> 160 + {/if} 161 + </T.Group>
+62
src/lib/cards/Model3DCard/index.ts
··· 1 + import { uploadBlob } from '$lib/atproto'; 2 + import type { CardDefinition } from '../types'; 3 + import CreateModel3DCardModal from './CreateModel3DCardModal.svelte'; 4 + import Model3DCard from './Model3DCard.svelte'; 5 + 6 + export const Model3DCardDefinition = { 7 + type: 'model3d', 8 + contentComponent: Model3DCard, 9 + creationModalComponent: CreateModel3DCardModal, 10 + sidebarButtonText: '3D Model', 11 + 12 + createNew: (card) => { 13 + card.w = 4; 14 + card.h = 4; 15 + card.mobileW = 4; 16 + card.mobileH = 4; 17 + card.cardData = { 18 + modelType: 'gltf' // 'gltf' | 'stl' 19 + }; 20 + }, 21 + 22 + upload: async (item) => { 23 + // Handle file upload 24 + if (item.cardData.modelFile?.blob) { 25 + let blob: Blob = item.cardData.modelFile.blob; 26 + const modelType = item.cardData.modelFile.type || 'glb'; 27 + 28 + // Ensure blob has a MIME type (STL/FBX files often have empty type) 29 + if (!blob.type) { 30 + const mimeTypes: Record<string, string> = { 31 + stl: 'model/stl', 32 + glb: 'model/gltf-binary', 33 + gltf: 'model/gltf+json', 34 + fbx: 'application/octet-stream' 35 + }; 36 + const mimeType = mimeTypes[modelType] || 'application/octet-stream'; 37 + blob = new Blob([blob], { type: mimeType }); 38 + } 39 + 40 + // Upload the blob to the PDS 41 + const uploadedBlob = await uploadBlob({ blob }); 42 + 43 + if (uploadedBlob) { 44 + item.cardData.modelBlob = uploadedBlob; 45 + item.cardData.modelType = modelType; 46 + } 47 + 48 + // Clean up the temporary file data 49 + if (item.cardData.modelFile.objectUrl) { 50 + URL.revokeObjectURL(item.cardData.modelFile.objectUrl); 51 + } 52 + delete item.cardData.modelFile; 53 + } 54 + 55 + return item; 56 + }, 57 + 58 + minW: 2, 59 + minH: 2, 60 + 61 + name: '3D Model Card' 62 + } as CardDefinition & { type: 'model3d' };
+9 -12
src/lib/cards/TimerCard/TimerCard.svelte
··· 88 88 class="text-base-900 dark:text-base-100 accent:text-base-900 flex items-center text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 89 89 style="font-variant-numeric: tabular-nums;" 90 90 > 91 - <NumberFlow value={clockHours} format={{ minimumIntegerDigits: 2 }} trend={1} /> 92 - <span class="text-base-400 dark:text-base-500 mx-0.5 @sm:mx-1">:</span> 91 + <NumberFlow value={clockHours} format={{ minimumIntegerDigits: 2 }} /> 92 + <span class="text-base-400 dark:text-base-500 accent:text-accent-950 mx-0.5">:</span> 93 93 <NumberFlow 94 94 value={clockMinutes} 95 95 format={{ minimumIntegerDigits: 2 }} 96 96 digits={{ 1: { max: 5 } }} 97 97 trend={1} 98 98 /> 99 - <span class="text-base-400 dark:text-base-500 mx-0.5 @sm:mx-1">:</span> 99 + <span class="text-base-400 dark:text-base-500 accent:text-accent-950 mx-0.5">:</span> 100 100 <NumberFlow 101 101 value={clockSeconds} 102 102 format={{ minimumIntegerDigits: 2 }} ··· 121 121 > 122 122 {#if eventDays > 0} 123 123 <div class="flex flex-col items-center"> 124 - <NumberFlow 125 - value={eventDays} 126 - trend={-1} 127 - class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 128 - /> 129 - <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">days</span> 124 + <NumberFlow value={eventDays} trend={-1} class="text-4xl font-bold" /> 125 + <span class="text-base-500 dark:text-base-400 accent:text-base-700 text-xs">days</span 126 + > 130 127 </div> 131 128 {/if} 132 129 <div class="flex flex-col items-center"> ··· 136 133 format={{ minimumIntegerDigits: 2 }} 137 134 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 138 135 /> 139 - <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">hrs</span> 136 + <span class="text-base-500 dark:text-base-400 accent:text-base-700 text-xs">hrs</span> 140 137 </div> 141 138 <div class="flex flex-col items-center"> 142 139 <NumberFlow ··· 146 143 digits={{ 1: { max: 5 } }} 147 144 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 148 145 /> 149 - <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">min</span> 146 + <span class="text-base-500 dark:text-base-400 accent:text-base-700 text-xs">min</span> 150 147 </div> 151 148 <div class="flex flex-col items-center"> 152 149 <NumberFlow ··· 156 153 digits={{ 1: { max: 5 } }} 157 154 class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 158 155 /> 159 - <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">sec</span> 156 + <span class="text-base-500 dark:text-base-400 accent:text-base-700 text-xs">sec</span> 160 157 </div> 161 158 </div> 162 159 </NumberFlowGroup>
+3 -1
src/lib/cards/index.ts
··· 31 31 import { DrawCardDefinition } from './DrawCard'; 32 32 import { TimerCardDefinition } from './TimerCard'; 33 33 import { SpotifyCardDefinition } from './SpotifyCard'; 34 + import { Model3DCardDefinition } from './Model3DCard'; 34 35 35 36 export const AllCardDefinitions = [ 36 37 ImageCardDefinition, ··· 64 65 VCardCardDefinition, 65 66 DrawCardDefinition, 66 67 TimerCardDefinition, 67 - SpotifyCardDefinition 68 + SpotifyCardDefinition, 69 + Model3DCardDefinition 68 70 ] as const; 69 71 70 72 export const CardDefinitionsByType = AllCardDefinitions.reduce(