your personal website on atproto - mirror blento.app

cleanup

Florian d7c66b0c be994819

+398 -2174
+1
package.json
··· 46 46 "@atproto/api": "^0.15.6", 47 47 "@atproto/common-web": "^0.4.2", 48 48 "@foxui/core": "^0.4.7", 49 + "@foxui/social": "^0.4.7", 49 50 "@tailwindcss/typography": "^0.5.16", 50 51 "@tiptap/core": "^2.12.0", 51 52 "@tiptap/extension-document": "^2.12.0",
+100
pnpm-lock.yaml
··· 23 23 '@foxui/core': 24 24 specifier: ^0.4.7 25 25 version: 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 26 + '@foxui/social': 27 + specifier: ^0.4.7 28 + version: 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 26 29 '@tailwindcss/typography': 27 30 specifier: ^0.5.16 28 31 version: 0.5.16(tailwindcss@4.1.5) ··· 601 604 svelte: '>=5' 602 605 tailwindcss: '>=3' 603 606 607 + '@foxui/social@0.4.7': 608 + resolution: {integrity: sha512-KeA0Ppl8NVzPwJsibAGFJMCPlLfOa7L3LQqS4YBfPd0hhribBQ9MHWTfN+JM0upYt/E97XPsL/EoIrWfY67obA==, tarball: https://registry.npmjs.org/@foxui/social/-/social-0.4.7.tgz} 609 + peerDependencies: 610 + svelte: '>=5' 611 + tailwindcss: '>=3' 612 + 613 + '@foxui/time@0.4.7': 614 + resolution: {integrity: sha512-N4jN1QfUi7IY53MQETZp4MDj6DwwONoRi4yrN96SjpB71w7cvhli1jQCSG4QqCtyvISaizlg4T5gzORg7PYWrA==, tarball: https://registry.npmjs.org/@foxui/time/-/time-0.4.7.tgz} 615 + peerDependencies: 616 + svelte: '>=5' 617 + tailwindcss: '>=3' 618 + 604 619 '@humanfs/core@0.19.1': 605 620 resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==, tarball: https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz} 606 621 engines: {node: '>=18.18.0'} ··· 1241 1256 resolution: {integrity: sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==, tarball: https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz} 1242 1257 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1243 1258 1259 + '@use-gesture/core@10.3.1': 1260 + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==, tarball: https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz} 1261 + 1262 + '@use-gesture/vanilla@10.3.1': 1263 + resolution: {integrity: sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==, tarball: https://registry.npmjs.org/@use-gesture/vanilla/-/vanilla-10.3.1.tgz} 1264 + 1244 1265 accepts@2.0.0: 1245 1266 resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==, tarball: https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz} 1246 1267 engines: {node: '>= 0.6'} ··· 1397 1418 resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==, tarball: https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz} 1398 1419 engines: {node: '>=18'} 1399 1420 1421 + core-js@3.47.0: 1422 + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==, tarball: https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz} 1423 + 1400 1424 cors@2.8.5: 1401 1425 resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==, tarball: https://registry.npmjs.org/cors/-/cors-2.8.5.tgz} 1402 1426 engines: {node: '>= 0.10'} ··· 1423 1447 engines: {node: '>=4'} 1424 1448 hasBin: true 1425 1449 1450 + custom-event-polyfill@1.0.7: 1451 + resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==, tarball: https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz} 1452 + 1426 1453 debug@4.4.0: 1427 1454 resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==, tarball: https://registry.npmjs.org/debug/-/debug-4.4.0.tgz} 1428 1455 engines: {node: '>=6.0'} ··· 1476 1503 1477 1504 ee-first@1.1.1: 1478 1505 resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, tarball: https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz} 1506 + 1507 + emoji-picker-element@1.28.1: 1508 + resolution: {integrity: sha512-8c64IPish2PWoV9oYCo2pvuPHwIv+uK9bO0dfpPyMupDAvaWL9ZvYhWNTAR+2sx7BhfRjciImqP6CIUgNX+DMg==, tarball: https://registry.npmjs.org/emoji-picker-element/-/emoji-picker-element-1.28.1.tgz} 1479 1509 1480 1510 encodeurl@2.0.0: 1481 1511 resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, tarball: https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz} ··· 1744 1774 resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, tarball: https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz} 1745 1775 engines: {node: '>= 0.4'} 1746 1776 1777 + hls.js@1.6.15: 1778 + resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==, tarball: https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz} 1779 + 1747 1780 htmlparser2@8.0.2: 1748 1781 resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==, tarball: https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz} 1749 1782 ··· 1782 1815 1783 1816 is-arrayish@0.3.4: 1784 1817 resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==, tarball: https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz} 1818 + 1819 + is-emoji-supported@0.0.5: 1820 + resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==, tarball: https://registry.npmjs.org/is-emoji-supported/-/is-emoji-supported-0.0.5.tgz} 1785 1821 1786 1822 is-extglob@2.1.1: 1787 1823 resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, tarball: https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz} ··· 1916 1952 linkifyjs@4.3.1: 1917 1953 resolution: {integrity: sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==, tarball: https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.1.tgz} 1918 1954 1955 + loadjs@4.3.0: 1956 + resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==, tarball: https://registry.npmjs.org/loadjs/-/loadjs-4.3.0.tgz} 1957 + 1919 1958 locate-character@3.0.0: 1920 1959 resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==, tarball: https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz} 1921 1960 ··· 2114 2153 resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==, tarball: https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz} 2115 2154 engines: {node: '>=16.20.0'} 2116 2155 2156 + plyr@3.8.4: 2157 + resolution: {integrity: sha512-DrzLbK9Wol3zeiuZCleD9aUOl0KAaBHR9H6WVVVYPZ4Ya+LYxUFTgSF1jooHcMQCv96Ws96wCaZzIoP3bES8pQ==, tarball: https://registry.npmjs.org/plyr/-/plyr-3.8.4.tgz} 2158 + 2117 2159 postcss-load-config@3.1.4: 2118 2160 resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==, tarball: https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz} 2119 2161 engines: {node: '>= 10'} ··· 2301 2343 resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==, tarball: https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz} 2302 2344 engines: {node: '>= 0.6'} 2303 2345 2346 + rangetouch@2.0.1: 2347 + resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==, tarball: https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz} 2348 + 2304 2349 raw-body@3.0.0: 2305 2350 resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==, tarball: https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz} 2306 2351 engines: {node: '>= 0.8'} ··· 2597 2642 2598 2643 uri-js@4.4.1: 2599 2644 resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, tarball: https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz} 2645 + 2646 + url-polyfill@1.1.14: 2647 + resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==, tarball: https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.14.tgz} 2600 2648 2601 2649 util-deprecate@1.0.2: 2602 2650 resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, tarball: https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz} ··· 3036 3084 tailwind-variants: 1.0.0(tailwindcss@4.1.5) 3037 3085 tailwindcss: 4.1.5 3038 3086 3087 + '@foxui/social@0.4.7(svelte@5.45.8)(tailwindcss@4.1.5)': 3088 + dependencies: 3089 + '@atproto/api': 0.15.6 3090 + '@foxui/core': 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 3091 + '@foxui/time': 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 3092 + '@use-gesture/vanilla': 10.3.1 3093 + bits-ui: 1.8.0(svelte@5.45.8) 3094 + emoji-picker-element: 1.28.1 3095 + hls.js: 1.6.15 3096 + is-emoji-supported: 0.0.5 3097 + plyr: 3.8.4 3098 + svelte: 5.45.8 3099 + tailwindcss: 4.1.5 3100 + 3101 + '@foxui/time@0.4.7(svelte@5.45.8)(tailwindcss@4.1.5)': 3102 + dependencies: 3103 + '@foxui/core': 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 3104 + '@number-flow/svelte': 0.3.9(svelte@5.45.8) 3105 + bits-ui: 1.8.0(svelte@5.45.8) 3106 + svelte: 5.45.8 3107 + tailwindcss: 4.1.5 3108 + 3039 3109 '@humanfs/core@0.19.1': {} 3040 3110 3041 3111 '@humanfs/node@0.16.6': ··· 3652 3722 '@typescript-eslint/types': 8.32.0 3653 3723 eslint-visitor-keys: 4.2.0 3654 3724 3725 + '@use-gesture/core@10.3.1': {} 3726 + 3727 + '@use-gesture/vanilla@10.3.1': 3728 + dependencies: 3729 + '@use-gesture/core': 10.3.1 3730 + 3655 3731 accepts@2.0.0: 3656 3732 dependencies: 3657 3733 mime-types: 3.0.1 ··· 3821 3897 3822 3898 cookie@1.1.1: {} 3823 3899 3900 + core-js@3.47.0: {} 3901 + 3824 3902 cors@2.8.5: 3825 3903 dependencies: 3826 3904 object-assign: 4.1.1 ··· 3847 3925 css.escape@1.5.1: {} 3848 3926 3849 3927 cssesc@3.0.0: {} 3928 + 3929 + custom-event-polyfill@1.0.7: {} 3850 3930 3851 3931 debug@4.4.0: 3852 3932 dependencies: ··· 3891 3971 gopd: 1.2.0 3892 3972 3893 3973 ee-first@1.1.1: {} 3974 + 3975 + emoji-picker-element@1.28.1: {} 3894 3976 3895 3977 encodeurl@2.0.0: {} 3896 3978 ··· 4246 4328 dependencies: 4247 4329 function-bind: 1.1.2 4248 4330 4331 + hls.js@1.6.15: {} 4332 + 4249 4333 htmlparser2@8.0.2: 4250 4334 dependencies: 4251 4335 domelementtype: 2.3.0 ··· 4283 4367 ipaddr.js@1.9.1: {} 4284 4368 4285 4369 is-arrayish@0.3.4: {} 4370 + 4371 + is-emoji-supported@0.0.5: {} 4286 4372 4287 4373 is-extglob@2.1.1: {} 4288 4374 ··· 4383 4469 uc.micro: 2.1.0 4384 4470 4385 4471 linkifyjs@4.3.1: {} 4472 + 4473 + loadjs@4.3.0: {} 4386 4474 4387 4475 locate-character@3.0.0: {} 4388 4476 ··· 4556 4644 4557 4645 pkce-challenge@5.0.0: {} 4558 4646 4647 + plyr@3.8.4: 4648 + dependencies: 4649 + core-js: 3.47.0 4650 + custom-event-polyfill: 1.0.7 4651 + loadjs: 4.3.0 4652 + rangetouch: 2.0.1 4653 + url-polyfill: 1.1.14 4654 + 4559 4655 postcss-load-config@3.1.4(postcss@8.5.3): 4560 4656 dependencies: 4561 4657 lilconfig: 2.1.0 ··· 4721 4817 queue-microtask@1.2.3: {} 4722 4818 4723 4819 range-parser@1.2.1: {} 4820 + 4821 + rangetouch@2.0.1: {} 4724 4822 4725 4823 raw-body@3.0.0: 4726 4824 dependencies: ··· 5080 5178 uri-js@4.4.1: 5081 5179 dependencies: 5082 5180 punycode: 2.3.1 5181 + 5182 + url-polyfill@1.1.14: {} 5083 5183 5084 5184 util-deprecate@1.0.2: {} 5085 5185
+119 -147
src/lib/EditableWebsite.svelte
··· 1 1 <script lang="ts"> 2 - import { setContext } from 'svelte'; 3 - import { BlueskyLogin, Button, Navbar, toast, Toaster } from './website/foxui'; 4 2 import { client, login } from '$lib/oauth/auth.svelte.js'; 5 3 6 - import { settingsModal } from './website/components/head/EditHead.svelte'; 7 - import HeadItem from './website/components/head/HeadItem.svelte'; 8 - import { 9 - setDataContext, 10 - setDidContext, 11 - setIsEditing, 12 - setUpdateFunctionsContext, 13 - type UpdateFunction 14 - } from './website/context'; 4 + import { Navbar, Button, toast, Toaster } from '@foxui/core'; 5 + import { BlueskyLogin } from '@foxui/social'; 15 6 16 7 import { margin } from '$lib'; 17 - import EditingImageCard from './cards/ImageCard/EditingImageCard.svelte'; 18 - import { cardsEqual, clamp, fixCollisions, overlaps, sortItems } from './helper'; 8 + import { cardsEqual, clamp, fixCollisions, overlaps, setPositionOfNewItem } from './helper'; 19 9 import Profile from './Profile.svelte'; 20 10 import type { Item } from './types'; 21 11 import { deleteRecord, putRecord } from './oauth/atproto'; 22 12 import { innerWidth } from 'svelte/reactivity/window'; 23 13 import { TID } from '@atproto/common-web'; 24 14 import EditingCard from './cards/Card/EditingCard.svelte'; 15 + import { CardDefinitionsByType } from './cards'; 25 16 26 17 let { 27 18 handle, ··· 33 24 // svelte-ignore state_referenced_locally 34 25 let items: Item[] = $state(originalItems); 35 26 36 - let updateFunctions: UpdateFunction[] = $state([]); 37 - 38 - setIsEditing(true); 39 - // svelte-ignore state_referenced_locally 40 - setDidContext(data.did); 41 - setUpdateFunctionsContext(updateFunctions); 42 - // svelte-ignore state_referenced_locally 43 - setContext('current', data.current); 44 - // svelte-ignore state_referenced_locally 45 - setDataContext(data.data); 46 - 47 27 let container: HTMLDivElement | undefined = $state(); 48 28 49 29 let activeDragElement: { ··· 87 67 mobileX: 0, 88 68 mobileY: 0, 89 69 cardType: type, 90 - cardData: { 91 - href: 'https://bsky.app/profile/flo-bit.dev' 92 - } 70 + cardData: {} 93 71 }; 72 + const cardDef = CardDefinitionsByType[type]; 73 + cardDef?.createNew?.(newItem); 94 74 95 - let foundPosition = false; 96 - while (!foundPosition) { 97 - for (newItem.x = 0; newItem.x <= 4 - newItem.w; newItem.x++) { 98 - let collision = items.find((item) => overlaps(newItem, item)); 99 - console.log('checking position', newItem.x, newItem.y, 'collision:', collision); 100 - if (!collision) { 101 - foundPosition = true; 102 - break; 103 - } 104 - } 105 - if (!foundPosition) newItem.y += 1; 106 - } 107 - 108 - let foundMobilePosition = false; 109 - while (!foundMobilePosition) { 110 - for (newItem.mobileX = 0; newItem.mobileX <= 4 - newItem.mobileW; newItem.mobileX += 1) { 111 - let collision = items.find((item) => overlaps(newItem, item, true)); 112 - 113 - if (!collision) { 114 - foundMobilePosition = true; 115 - break; 116 - } 117 - } 118 - if (!foundMobilePosition) newItem.mobileY! += 2; 119 - } 75 + setPositionOfNewItem(newItem, items); 120 76 121 77 items = [...items, newItem]; 122 78 } 79 + 80 + let isSaving = $state(false); 123 81 </script> 124 82 125 - <Profile {handle} {did} /> 83 + <Profile {handle} {did} {data} /> 126 84 127 - <div class="mx-auto max-w-2xl lg:grid-cols-4 lg:grid lg:max-w-none xl:grid-cols-3"> 85 + <div class="mx-auto max-w-2xl lg:grid lg:max-w-none lg:grid-cols-4 xl:grid-cols-3"> 128 86 <div></div> 129 87 <!-- svelte-ignore a11y_no_static_element_interactions --> 130 88 <div ··· 186 144 activeDragElement.element = null; 187 145 return true; 188 146 }} 189 - class="relative col-span-3 xl:col-span-2 py-8 px-2 lg:px-8" 147 + class="relative col-span-3 px-2 py-8 lg:px-8 xl:col-span-2" 190 148 style="container-type: inline-size;" 191 149 > 192 150 {#each items as item, i} ··· 239 197 </div> 240 198 </div> 241 199 242 - <HeadItem collection="com.example.head" /> 243 - 244 - <Navbar 245 - class="dark:bg-base-900 bg-base-100 top-auto bottom-2 mx-4 mt-3 max-w-3xl rounded-full px-4 lg:mx-auto" 246 - > 247 - <div class="flex items-center gap-2"> 248 - <Button 249 - size="iconLg" 250 - variant="ghost" 251 - class="backdrop-blur-none" 252 - onclick={() => (settingsModal.show = true)} 253 - > 254 - <svg 255 - xmlns="http://www.w3.org/2000/svg" 256 - fill="none" 257 - viewBox="0 0 24 24" 258 - stroke-width="1.5" 259 - stroke="currentColor" 260 - > 261 - <path 262 - stroke-linecap="round" 263 - stroke-linejoin="round" 264 - d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" 265 - /> 266 - <path 267 - stroke-linecap="round" 268 - stroke-linejoin="round" 269 - d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 270 - /> 271 - </svg> 272 - </Button> 273 - <Button 274 - size="iconLg" 275 - variant="ghost" 276 - class="backdrop-blur-none" 277 - onclick={() => { 278 - newCard(); 279 - }} 280 - > 281 - <svg 282 - xmlns="http://www.w3.org/2000/svg" 283 - fill="none" 284 - viewBox="0 0 24 24" 285 - stroke-width="1.5" 286 - stroke="currentColor" 200 + {#if !client.isLoggedIn || client.profile?.did === did} 201 + <Navbar 202 + class="dark:bg-base-900 bg-base-100 top-auto bottom-2 mx-4 mt-3 max-w-3xl rounded-full px-4 lg:mx-auto" 203 + > 204 + <div class="flex items-center gap-2"> 205 + <Button 206 + size="iconLg" 207 + variant="ghost" 208 + class="backdrop-blur-none" 209 + onclick={() => { 210 + newCard('text'); 211 + }} 287 212 > 288 - <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> 289 - </svg> 290 - </Button> 291 - </div> 292 - <div class="flex items-center gap-2"> 293 - {#if client.isInitializing}{:else if client.isLoggedIn} 213 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" 214 + ><path 215 + fill="none" 216 + stroke="currentColor" 217 + stroke-linecap="round" 218 + stroke-linejoin="round" 219 + stroke-width="1.5" 220 + d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392" 221 + /></svg 222 + > 223 + </Button> 294 224 <Button 295 - onclick={async () => { 296 - // check if did is same 297 - if (client?.profile?.did !== data.did) { 298 - toast('Not authorized', { 299 - description: 'Please login with the correct account' 300 - }); 301 - return; 302 - } 225 + size="iconLg" 226 + variant="ghost" 227 + class="backdrop-blur-none" 228 + onclick={() => { 229 + newCard('link'); 230 + }} 231 + > 232 + <svg 233 + xmlns="http://www.w3.org/2000/svg" 234 + fill="none" 235 + viewBox="-2 -2 28 28" 236 + stroke-width="1.5" 237 + stroke="currentColor" 238 + > 239 + <path 240 + stroke-linecap="round" 241 + stroke-linejoin="round" 242 + d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 243 + /> 244 + </svg> 245 + </Button> 303 246 304 - for (const updateFunction of updateFunctions) { 305 - await updateFunction(); 306 - } 247 + <Button 248 + size="iconLg" 249 + variant="ghost" 250 + class="backdrop-blur-none" 251 + onclick={() => { 252 + newCard('image'); 253 + }} 254 + > 255 + <svg 256 + xmlns="http://www.w3.org/2000/svg" 257 + fill="none" 258 + viewBox="0 0 24 24" 259 + stroke-width="1.5" 260 + stroke="currentColor" 261 + > 262 + <path 263 + stroke-linecap="round" 264 + stroke-linejoin="round" 265 + d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" 266 + /> 267 + </svg> 268 + </Button> 269 + </div> 270 + <div class="flex items-center gap-2"> 271 + {#if client.isLoggedIn} 272 + <Button 273 + disabled={isSaving} 274 + onclick={async () => { 275 + isSaving = true; 307 276 308 - // find all cards that have been updated (where items differ from originalItems) 309 - for (let item of items) { 310 - const originalItem = originalItems.find((i) => cardsEqual(i, item)); 277 + // find all cards that have been updated (where items differ from originalItems) 278 + for (let item of items) { 279 + const originalItem = originalItems.find((i) => cardsEqual(i, item)); 311 280 312 - if (!originalItem) { 313 - console.log('updated or new item', item); 314 - item.updatedAt = new Date().toISOString(); 315 - await putRecord({ collection: 'com.example.bento', rkey: item.id, record: item }); 281 + if (!originalItem) { 282 + console.log('updated or new item', item); 283 + item.updatedAt = new Date().toISOString(); 284 + await putRecord({ collection: 'com.example.bento', rkey: item.id, record: item }); 285 + } 316 286 } 317 - } 318 287 319 - // delete items that are in originalItems but not in items 320 - for (let originalItem of originalItems) { 321 - const item = items.find((i) => i.id === originalItem.id); 322 - if (!item) { 323 - console.log('deleting item', originalItem); 324 - await deleteRecord({ collection: 'com.example.bento', rkey: originalItem.id, did }); 288 + // delete items that are in originalItems but not in items 289 + for (let originalItem of originalItems) { 290 + const item = items.find((i) => i.id === originalItem.id); 291 + if (!item) { 292 + console.log('deleting item', originalItem); 293 + await deleteRecord({ collection: 'com.example.bento', rkey: originalItem.id, did }); 294 + } 325 295 } 326 - } 327 296 328 - toast('Saved', { 329 - description: 'Your website has been saved!' 330 - }); 331 - }}>Save</Button 332 - > 333 - {:else} 334 - <BlueskyLogin 335 - login={async (handle) => { 336 - await login(handle); 337 - return true; 338 - }} 339 - /> 340 - {/if} 341 - </div> 342 - </Navbar> 297 + isSaving = false; 298 + 299 + toast('Saved', { 300 + description: 'Your website has been saved!' 301 + }); 302 + }}>{isSaving ? 'Saving...' : 'Save'}</Button 303 + > 304 + {:else} 305 + <BlueskyLogin 306 + login={async (handle) => { 307 + await login(handle); 308 + return true; 309 + }} 310 + /> 311 + {/if} 312 + </div> 313 + </Navbar> 314 + {/if} 343 315 344 316 <Toaster />
+55 -23
src/lib/Profile.svelte
··· 1 1 <script lang="ts"> 2 2 import Favicon from './Favicon.svelte'; 3 - import { MarkdownText, SingleRecord } from './website/components'; 4 3 5 - let { handle, did }: { handle: string; did: string } = $props(); 4 + import { marked } from 'marked'; 5 + import { client } from './oauth'; 6 + import { Button } from '@foxui/core'; 7 + let { 8 + handle, 9 + did, 10 + data, 11 + showEditButton = false 12 + }: { handle: string; did: string; data: any; showEditButton: boolean } = $props(); 13 + $inspect(data); 14 + 15 + const profileData = data?.data?.['app.bsky.actor.profile']?.self?.value; 16 + $inspect(profileData); 17 + 18 + const renderer = new marked.Renderer(); 19 + renderer.link = ({ href, title, text }) => 20 + `<a target="_blank" href="${href}" title="${title}">${text}</a>`; 6 21 </script> 7 22 8 - <div class="mx-auto flex max-w-2xl px-10 lg:px-12 pt-16 lg:pt-24 pb-8 lg:fixed lg:h-screen lg:w-1/4 xl:w-1/3 lg:max-w-none"> 23 + <div 24 + class="mx-auto flex max-w-2xl px-10 pt-16 pb-8 lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3" 25 + > 9 26 <div class="flex flex-col gap-4"> 10 - <SingleRecord collection="app.bsky.actor.profile" rkey="self"> 11 - {#snippet child(data)} 12 - <Favicon 13 - favicon={'https://cdn.bsky.app/img/avatar/plain/' + 14 - did + 15 - '/' + 16 - data.value.avatar.ref.$link} 17 - /> 18 - <img 19 - class="rounded-fulll size-32 lg:size-44 rounded-full" 20 - src={'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + data.value.avatar.ref.$link} 21 - alt="" 22 - /> 23 - <div class="line-clamp-2 text-4xl font-bold wrap-anywhere">{handle}</div> 27 + <Favicon 28 + favicon={'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + profileData?.avatar.ref.$link} 29 + /> 30 + <img 31 + class="rounded-fulll size-32 rounded-full lg:size-44" 32 + src={'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + profileData?.avatar.ref.$link} 33 + alt="" 34 + /> 35 + <div class="line-clamp-2 text-4xl font-bold wrap-anywhere">{handle}</div> 24 36 25 - <div 26 - class="text-base-600 dark:text-base-400 prose dark:prose-invert prose-a:text-accent-500 prose-a:no-underline line-clamp-3" 37 + <div 38 + class="text-base-600 dark:text-base-400 prose dark:prose-invert prose-a:text-accent-500 prose-a:no-underline line-clamp-3" 39 + > 40 + {@html marked.parse(profileData.description ?? '', { renderer })} 41 + </div> 42 + 43 + {#if showEditButton && client.isLoggedIn && client.profile?.did === did} 44 + <div> 45 + <Button href="./edit" class="mt-2"> 46 + <svg 47 + xmlns="http://www.w3.org/2000/svg" 48 + fill="none" 49 + viewBox="0 0 24 24" 50 + stroke-width="1.5" 51 + stroke="currentColor" 52 + > 53 + <path 54 + stroke-linecap="round" 55 + stroke-linejoin="round" 56 + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" 57 + /> 58 + </svg> 59 + 60 + Edit Your Website</Button 27 61 > 28 - <MarkdownText key="description" {data} /> 29 - </div> 30 - {/snippet} 31 - </SingleRecord> 62 + </div> 63 + {/if} 32 64 </div> 33 65 </div>
+2 -2
src/lib/Website.svelte
··· 5 5 import type { Item } from './types'; 6 6 import { innerWidth } from 'svelte/reactivity/window'; 7 7 8 - let { handle, did, items }: { handle: string; did: string; items: Item[] } = $props(); 8 + let { handle, did, items, data }: { handle: string; did: string; items: Item[], data:any } = $props(); 9 9 10 10 let isMobile = $derived((innerWidth.current ?? 1000) < 1024); 11 11 ··· 19 19 let container: HTMLDivElement | undefined = $state(); 20 20 </script> 21 21 22 - <Profile {handle} {did} /> 22 + <Profile {handle} {did} {data} showEditButton={true} /> 23 23 24 24 <div class="mx-auto max-w-2xl lg:grid lg:max-w-none lg:grid-cols-4 xl:grid-cols-3"> 25 25 <div></div>
+1 -3
src/lib/cards/Card/Card.svelte
··· 1 1 <script lang="ts"> 2 - import { CardDefinitionsByType } from '.'; 2 + import { CardDefinitionsByType } from '..'; 3 3 import BaseCard, { type BaseCardProps } from '../BaseCard/BaseCard.svelte'; 4 - import ImageCard from '../ImageCard/ImageCard.svelte'; 5 - import TextCard from '../TextCard/TextCard.svelte'; 6 4 7 5 let { item, ref = $bindable(null), ...rest }: BaseCardProps = $props(); 8 6 </script>
+1 -1
src/lib/cards/Card/EditingCard.svelte
··· 1 1 <script lang="ts"> 2 2 import type { BaseEditingCardProps } from '../BaseCard/BaseEditingCard.svelte'; 3 - import { CardDefinitionsByType } from '.'; 3 + import { CardDefinitionsByType } from '..'; 4 4 import BaseCard from '../BaseCard/BaseCard.svelte'; 5 5 6 6 let { item = $bindable(), ref = $bindable(), ...rest }: BaseEditingCardProps = $props();
-18
src/lib/cards/Card/index.ts
··· 1 - import { ImageCardDefinition } from '../ImageCard'; 2 - import { LinkCardDefinition } from '../LinkCard'; 3 - import { TextCardDefinition } from '../TextCard'; 4 - import type { CardDefinition } from '../types'; 5 - 6 - export const AllCardDefinitions = [ 7 - ImageCardDefinition, 8 - TextCardDefinition, 9 - LinkCardDefinition 10 - ] as const; 11 - 12 - export const CardDefinitionsByType = AllCardDefinitions.reduce( 13 - (acc, item) => { 14 - acc[item.type] = item; 15 - return acc; 16 - }, 17 - {} as Record<string, CardDefinition> 18 - );
+3 -2
src/lib/cards/ImageCard/index.ts
··· 9 9 createNew: (card) => { 10 10 card.cardType = 'image'; 11 11 card.cardData = { 12 - src: '', 12 + image: `https://picsum.photos/seed/${card.id}/800/800`, 13 13 alt: '', 14 - href: '' 14 + href: 'https://example.com' 15 15 }; 16 + console.log('adding new card', card); 16 17 } 17 18 } as CardDefinition & { type: 'image' };
+1 -3
src/lib/cards/LinkCard/index.ts
··· 9 9 createNew: (card) => { 10 10 card.cardType = 'link'; 11 11 card.cardData = { 12 - src: '', 13 - alt: '', 14 - href: '' 12 + href: 'https://flo-bit.dev' 15 13 }; 16 14 } 17 15 } as CardDefinition & { type: 'link' };
+1 -1
src/lib/cards/TextCard/index.ts
··· 9 9 createNew: (card) => { 10 10 card.cardType = 'text'; 11 11 card.cardData = { 12 - text: '' 12 + text: 'hello world' 13 13 }; 14 14 } 15 15 } as CardDefinition & { type: 'text' };
+18
src/lib/cards/index.ts
··· 1 + import { ImageCardDefinition } from './ImageCard'; 2 + import { LinkCardDefinition } from './LinkCard'; 3 + import { TextCardDefinition } from './TextCard'; 4 + import type { CardDefinition } from './types'; 5 + 6 + export const AllCardDefinitions = [ 7 + ImageCardDefinition, 8 + TextCardDefinition, 9 + LinkCardDefinition 10 + ] as const; 11 + 12 + export const CardDefinitionsByType = AllCardDefinitions.reduce( 13 + (acc, item) => { 14 + acc[item.type] = item; 15 + return acc; 16 + }, 17 + {} as Record<string, CardDefinition> 18 + );
+27
src/lib/helper.ts
··· 114 114 a.mobileY === b.mobileY 115 115 ); 116 116 } 117 + 118 + export function setPositionOfNewItem(newItem: Item, items: Item[]) { 119 + let foundPosition = false; 120 + while (!foundPosition) { 121 + for (newItem.x = 0; newItem.x <= 4 - newItem.w; newItem.x++) { 122 + const collision = items.find((item) => overlaps(newItem, item)); 123 + if (!collision) { 124 + foundPosition = true; 125 + break; 126 + } 127 + } 128 + if (!foundPosition) newItem.y += 1; 129 + } 130 + 131 + let foundMobilePosition = false; 132 + while (!foundMobilePosition) { 133 + for (newItem.mobileX = 0; newItem.mobileX <= 4 - newItem.mobileW; newItem.mobileX += 1) { 134 + const collision = items.find((item) => overlaps(newItem, item, true)); 135 + 136 + if (!collision) { 137 + foundMobilePosition = true; 138 + break; 139 + } 140 + } 141 + if (!foundMobilePosition) newItem.mobileY! += 2; 142 + } 143 + }
-107
src/lib/website/EditingWebsiteWrapper.svelte
··· 1 - <script lang="ts"> 2 - import { setContext } from 'svelte'; 3 - import { BlueskyLogin, Button, Navbar, toast, Toaster } from './foxui'; 4 - import { client, login } from '$lib/oauth/auth.svelte.js'; 5 - 6 - import { settingsModal } from './components/head/EditHead.svelte'; 7 - import { base } from '$app/paths'; 8 - import HeadItem from './components/head/HeadItem.svelte'; 9 - import { setDataContext, setDidContext, setIsEditing, setUpdateFunctionsContext, type UpdateFunction } from './context'; 10 - 11 - let updateFunctions: UpdateFunction[] = $state([]); 12 - 13 - let { data, children } = $props(); 14 - 15 - setIsEditing(true); 16 - 17 - // svelte-ignore state_referenced_locally 18 - setDidContext(data.did); 19 - setUpdateFunctionsContext(updateFunctions); 20 - // svelte-ignore state_referenced_locally 21 - setContext('current', data.current); 22 - // svelte-ignore state_referenced_locally 23 - setDataContext(data.data); 24 - </script> 25 - 26 - {@render children?.()} 27 - 28 - <HeadItem collection="com.example.head" /> 29 - 30 - <Navbar class="dark:bg-base-900 bg-base-100 top-auto bottom-2 mx-4 mt-3 max-w-3xl rounded-full px-4 lg:mx-auto"> 31 - <div class="flex items-center gap-2"> 32 - <Button size="iconLg" variant="ghost" class="backdrop-blur-none" href={base + '/'}> 33 - <span class="sr-only">home</span> 34 - <svg 35 - xmlns="http://www.w3.org/2000/svg" 36 - fill="none" 37 - viewBox="0 0 24 24" 38 - stroke-width="1.5" 39 - stroke="currentColor" 40 - > 41 - <path 42 - stroke-linecap="round" 43 - stroke-linejoin="round" 44 - d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" 45 - /> 46 - </svg> 47 - </Button> 48 - 49 - <Button 50 - size="iconLg" 51 - variant="ghost" 52 - class="backdrop-blur-none" 53 - onclick={() => (settingsModal.show = true)} 54 - > 55 - <svg 56 - xmlns="http://www.w3.org/2000/svg" 57 - fill="none" 58 - viewBox="0 0 24 24" 59 - stroke-width="1.5" 60 - stroke="currentColor" 61 - > 62 - <path 63 - stroke-linecap="round" 64 - stroke-linejoin="round" 65 - d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" 66 - /> 67 - <path 68 - stroke-linecap="round" 69 - stroke-linejoin="round" 70 - d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 71 - /> 72 - </svg> 73 - </Button> 74 - </div> 75 - <div class="flex items-center gap-2"> 76 - {#if client.isInitializing}{:else if client.isLoggedIn} 77 - <Button 78 - onclick={async () => { 79 - // check if did is same 80 - if (client?.profile?.did !== data.did) { 81 - toast('Not authorized', { 82 - description: 'Please login with the correct account' 83 - }); 84 - return; 85 - } 86 - 87 - for (const updateFunction of updateFunctions) { 88 - await updateFunction(); 89 - } 90 - 91 - toast('Saved', { 92 - description: 'Your website has been saved!' 93 - }); 94 - }}>Save</Button 95 - > 96 - {:else} 97 - <BlueskyLogin 98 - login={async (handle) => { 99 - await login(handle); 100 - return true; 101 - }} 102 - /> 103 - {/if} 104 - </div> 105 - </Navbar> 106 - 107 - <Toaster />
-19
src/lib/website/WebsiteWrapper.svelte
··· 1 - <script> 2 - import { setContext } from "svelte"; 3 - import { setDataContext, setDidContext, setIsEditing } from "./context"; 4 - 5 - let { data, children, handle } = $props(); 6 - 7 - // svelte-ignore state_referenced_locally 8 - setDidContext(data.did); 9 - // svelte-ignore state_referenced_locally 10 - setDataContext(data.data); 11 - // svelte-ignore state_referenced_locally 12 - setContext('current', data.current); 13 - 14 - setIsEditing(false); 15 - 16 - $inspect(data); 17 - </script> 18 - 19 - {@render children?.()}
-60
src/lib/website/components/head/EditHead.svelte
··· 1 - <script lang="ts" module> 2 - export const settingsModal = $state({ 3 - show: false, 4 - title: '', 5 - favicon: '', 6 - edited: false 7 - }); 8 - </script> 9 - 10 - <script lang="ts"> 11 - import { onDestroy, onMount } from 'svelte'; 12 - import Head from './Head.svelte'; 13 - import { Modal, Heading, Label, Input } from '../../foxui'; 14 - import { getUpdateRecordFunctionsContext } from '$lib/website/context'; 15 - 16 - let { data } = $props(); 17 - 18 - $effect(() => { 19 - settingsModal.title = data?.value?.title; 20 - settingsModal.favicon = data?.value?.favicon; 21 - }); 22 - 23 - const updateFunctions = getUpdateRecordFunctionsContext(); 24 - 25 - const update = async () => { 26 - if (!settingsModal.edited) return {}; 27 - 28 - settingsModal.edited = false; 29 - 30 - return { 31 - title: settingsModal.title, 32 - favicon: settingsModal.favicon 33 - }; 34 - }; 35 - 36 - onMount(() => { 37 - updateFunctions.push(update); 38 - }); 39 - 40 - onDestroy(() => { 41 - updateFunctions.splice(updateFunctions.indexOf(update), 1); 42 - }); 43 - 44 - $inspect(settingsModal); 45 - </script> 46 - 47 - <Modal bind:open={settingsModal.show}> 48 - <Heading>Website Settings</Heading> 49 - 50 - <Label>Title</Label> 51 - <Input 52 - type="text" 53 - bind:value={settingsModal.title} 54 - onkeydown={() => (settingsModal.edited = true)} 55 - /> 56 - </Modal> 57 - 58 - {#key settingsModal.title + settingsModal.favicon} 59 - <Head data={{ value: { title: settingsModal.title, favicon: settingsModal.favicon } }} /> 60 - {/key}
-38
src/lib/website/components/head/Head.svelte
··· 1 - <script lang="ts"> 2 - let { data } = $props(); 3 - </script> 4 - 5 - <svelte:head> 6 - {#if data?.value?.favicon} 7 - <link 8 - rel="icon" 9 - href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>{data 10 - .value.favicon}</text></svg>" 11 - /> 12 - {/if} 13 - 14 - <meta property="og:type" content="website" /> 15 - 16 - {#if data?.value?.description} 17 - <meta name="description" content={data.value.description} /> 18 - <meta property="og:description" content={data.value.description} /> 19 - <meta name="twitter:description" content={data.value.description} /> 20 - {/if} 21 - 22 - {#if data?.value?.title} 23 - <title>{data.value.title}</title> 24 - <meta property="og:title" content={data.value.title} /> 25 - <meta name="twitter:title" content={data.value.title} /> 26 - {/if} 27 - 28 - {#if data?.value?.image} 29 - <meta property="og:image" content={data.value.image} /> 30 - <meta name="twitter:image" content={data.value.image} /> 31 - <meta name="twitter:card" content="summary_large_image" /> 32 - {/if} 33 - 34 - {#if data?.value?.url} 35 - <meta property="twitter:domain" content={new URL(data.value.url).hostname} /> 36 - <meta property="twitter:url" content={data.value.url} /> 37 - {/if} 38 - </svelte:head>
-27
src/lib/website/components/head/HeadItem.svelte
··· 1 - <script lang="ts"> 2 - import { hasContext } from 'svelte'; 3 - import { SingleRecord } from '../single-record'; 4 - import Head from './Head.svelte'; 5 - import type { ElementType, IndividualCollections } from '$lib/website/data'; 6 - import { isEditing } from '$lib/website/context'; 7 - 8 - let { 9 - collection, 10 - rkey = 'self' 11 - }: { 12 - collection: IndividualCollections; 13 - rkey?: ElementType<IndividualCollections>; 14 - } = $props(); 15 - </script> 16 - 17 - <SingleRecord {collection} {rkey}> 18 - {#snippet child(data)} 19 - {#if isEditing()} 20 - {#await import('./EditHead.svelte') then { default: EditHead }} 21 - <EditHead {data} /> 22 - {/await} 23 - {:else} 24 - <Head {data} /> 25 - {/if} 26 - {/snippet} 27 - </SingleRecord>
-27
src/lib/website/components/image/Image.svelte
··· 1 - <script lang="ts"> 2 - import { base } from '$app/paths'; 3 - import { isEditing } from '$lib/website/context'; 4 - import { getContext } from 'svelte'; 5 - 6 - let { 7 - key, 8 - data, 9 - class: className, 10 - rkey 11 - }: { 12 - key: string; 13 - data: Record<string, any>; 14 - class?: string; 15 - rkey: string; 16 - } = $props(); 17 - 18 - $inspect(data); 19 - </script> 20 - 21 - {#if isEditing()} 22 - {#await import('./ImageEditor.svelte') then { default: ImageEditor }} 23 - <ImageEditor class={className} {key} data={data.value} {rkey} /> 24 - {/await} 25 - {:else} 26 - <img class={className} src={base + '/image/' + data.value?.[key]?.ref.$link} alt="" /> 27 - {/if}
-72
src/lib/website/components/image/ImageEditor.svelte
··· 1 - <script lang="ts"> 2 - import { getBlob, uploadImage } from '$lib/oauth/atproto'; 3 - import { getDidContext } from '$lib/website/context'; 4 - import { image_collection } from '$lib/website/data'; 5 - import { cn } from '$lib/website/foxui'; 6 - import Button from '$lib/website/foxui/button/Button.svelte'; 7 - 8 - let { 9 - key, 10 - data, 11 - class: className, 12 - rkey 13 - }: { 14 - key: string; 15 - data: Record<string, any>; 16 - class?: string; 17 - rkey: string; 18 - } = $props(); 19 - 20 - const did = getDidContext(); 21 - 22 - let image = $state(''); 23 - 24 - $effect(() => { 25 - console.log('getting blob for', data?.[key]?.ref.$link); 26 - getBlob({ cid: data?.[key]?.ref.$link, did }).then((url) => { 27 - image = url; 28 - }); 29 - }); 30 - $inspect(image); 31 - </script> 32 - 33 - <div class={cn(className)}> 34 - {#key image} 35 - {#if image} 36 - <img class="h-full w-full object-cover" src={image} alt="" /> 37 - {/if} 38 - {/key} 39 - 40 - <Button 41 - onclick={() => { 42 - const input = document.createElement('input'); 43 - input.type = 'file'; 44 - input.accept = 'image/*'; 45 - input.onchange = async () => { 46 - const file = input.files?.[0]; 47 - if (file) { 48 - // convert to blob 49 - const blob = new Blob([await file.arrayBuffer()], { type: file.type }); 50 - 51 - const blobInfo = await uploadImage({ 52 - image: blob, 53 - did, 54 - collection: image_collection, 55 - rkey, 56 - key 57 - }); 58 - 59 - data ??= {}; 60 - console.log('blobInfo', blobInfo); 61 - data[key] = blobInfo; 62 - 63 - getBlob({ cid: data?.[key]?.ref.$link, did }).then((url) => { 64 - image = url; 65 - }); 66 - } 67 - }; 68 - input.click(); 69 - }} 70 - class="absolute bottom-2 right-2">Edit</Button 71 - > 72 - </div>
-25
src/lib/website/components/image/ImageItem.svelte
··· 1 - <script lang="ts"> 2 - import { image_collection } from '$lib/website/data'; 3 - import { 4 - type ElementType, 5 - type IndividualCollections 6 - } from '$lib/website/types'; 7 - import { SingleRecord } from '../single-record'; 8 - import Image from './Image.svelte'; 9 - 10 - let { 11 - rkey = 'self', 12 - key, 13 - class: className 14 - }: { 15 - rkey?: ElementType<IndividualCollections>; 16 - key: string; 17 - class?: string; 18 - } = $props(); 19 - </script> 20 - 21 - <SingleRecord collection={image_collection} {rkey}> 22 - {#snippet child(data)} 23 - <Image {key} {data} class={className} {rkey} /> 24 - {/snippet} 25 - </SingleRecord>
-4
src/lib/website/components/index.ts
··· 1 - export * from './single-record'; 2 - export * from './markdown'; 3 - export * from './list'; 4 - export * from './plain-text';
-57
src/lib/website/components/list-record/EditSingleRecord.svelte
··· 1 - <script lang="ts"> 2 - import { onDestroy, onMount, type Snippet } from 'svelte'; 3 - import { 4 - getUpdateFunctionsContext, 5 - type UpdateRecordFunction, 6 - setUpdateRecordFunctionsContext 7 - } from '../../context'; 8 - import { putRecord } from '$lib/oauth/atproto'; 9 - import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 10 - import { parseUri } from '$lib/website/utils'; 11 - 12 - let { 13 - data, 14 - child 15 - }: { 16 - data: ListRecord; 17 - child: Snippet<[ListRecord]>; 18 - } = $props(); 19 - 20 - let updateRecordFunctions: UpdateRecordFunction[] = $state([]); 21 - setUpdateRecordFunctionsContext(updateRecordFunctions); 22 - 23 - const updateFunctions = getUpdateFunctionsContext(); 24 - const update = async () => { 25 - const updated: Record<string, any> = {}; 26 - 27 - for (const updateFunction of updateRecordFunctions) { 28 - const updatedPart = await updateFunction(); 29 - for (const key in updatedPart) { 30 - updated[key] = updatedPart[key]; 31 - data.value[key] = updatedPart[key]; 32 - } 33 - } 34 - 35 - if (Object.keys(updated).length > 0 || !data.cid) { 36 - if (!data.value.createdAt) { 37 - data.value.createdAt = new Date().toISOString(); 38 - } 39 - data.value.updatedAt = new Date().toISOString(); 40 - 41 - const { collection, rkey } = parseUri(data.uri); 42 - await putRecord({ collection, rkey, record: data.value }); 43 - return true; 44 - } 45 - 46 - return false; 47 - }; 48 - 49 - onMount(() => { 50 - updateFunctions.push(update); 51 - }); 52 - onDestroy(() => { 53 - updateFunctions.splice(updateFunctions.indexOf(update), 1); 54 - }); 55 - </script> 56 - 57 - {@render child(data)}
-46
src/lib/website/components/list-record/SingleRecord.svelte
··· 1 - <script lang="ts"> 2 - import { getRecord } from '$lib/oauth/atproto'; 3 - import { getContext, type Snippet } from 'svelte'; 4 - import { type ElementType, type IndividualCollections } from '../../types'; 5 - import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 6 - import { getDataContext, getDidContext, isEditing } from '$lib/website/context'; 7 - 8 - let { 9 - collection, 10 - rkey, 11 - child 12 - }: { 13 - collection: IndividualCollections; 14 - rkey: ElementType<IndividualCollections>; 15 - child: Snippet<[ListRecord]>; 16 - } = $props(); 17 - 18 - const data = getDataContext(); 19 - const did = getDidContext(); 20 - </script> 21 - 22 - {#if isEditing()} 23 - {#await import('./EditSingleRecord.svelte') then { default: EditSingleRecord }} 24 - {#await getRecord({ did, collection, rkey }) then record} 25 - <EditSingleRecord data={record}> 26 - {#snippet child(recordData)} 27 - {@render child(recordData)} 28 - {/snippet} 29 - </EditSingleRecord> 30 - {:catch error} 31 - <EditSingleRecord 32 - data={{ 33 - uri: 'at://' + did + '/' + collection + '/' + rkey, 34 - value: {}, 35 - cid: '' 36 - }} 37 - > 38 - {#snippet child(recordData)} 39 - {@render child(recordData)} 40 - {/snippet} 41 - </EditSingleRecord> 42 - {/await} 43 - {/await} 44 - {:else} 45 - {@render child(data?.[collection]?.[rkey])} 46 - {/if}
-2
src/lib/website/components/list-record/index.ts
··· 1 - export { default as EditSingleRecord } from './EditSingleRecord.svelte'; 2 - export { default as SingleRecord } from './SingleRecord.svelte';
-62
src/lib/website/components/list/EditingList.svelte
··· 1 - <script lang="ts"> 2 - import { getContext, type Snippet } from 'svelte'; 3 - import { SingleRecord } from '..'; 4 - import { TID } from '@atproto/common-web'; 5 - import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 6 - import { deleteRecord } from '$lib/oauth/atproto'; 7 - import { parseUri } from '../../utils'; 8 - import type { ListCollections } from '$lib/website/types'; 9 - 10 - let { 11 - records, 12 - collection, 13 - item, 14 - addItem, 15 - empty 16 - }: { 17 - records: Record<string, ListRecord>; 18 - collection: ListCollections; 19 - item: Snippet<[any, any]>; 20 - addItem: Snippet<[any]>; 21 - empty?: Snippet; 22 - } = $props(); 23 - 24 - let allRecords = $state(records); 25 - 26 - $inspect(records); 27 - 28 - const did = getContext('did') as string; 29 - </script> 30 - 31 - {#if Object.keys(allRecords).length === 0 && empty} 32 - {@render empty()} 33 - {:else} 34 - {#each Object.values(allRecords) as itemData} 35 - <SingleRecord data={itemData} collection={collection} rkey={parseUri(itemData.uri as string).rkey}> 36 - {#snippet child(data)} 37 - {@render item(data, async () => { 38 - if (!itemData.cid) { 39 - const { rkey } = parseUri(itemData.uri as string); 40 - delete allRecords[rkey]; 41 - return; 42 - } 43 - const { rkey } = parseUri(itemData.uri as string); 44 - await deleteRecord({ did, collection, rkey }); 45 - 46 - delete allRecords[rkey]; 47 - })} 48 - {/snippet} 49 - </SingleRecord> 50 - {/each} 51 - {/if} 52 - 53 - {@render addItem?.((record) => { 54 - const rkey = TID.nextStr(); 55 - 56 - allRecords[rkey] = { 57 - cid: '', 58 - uri: 'at://' + did + '/' + collection + '/' + rkey, 59 - value: record ?? {} 60 - }; 61 - console.log(allRecords); 62 - })}
-36
src/lib/website/components/list/List.svelte
··· 1 - <script lang="ts"> 2 - import { getContext, hasContext, type Snippet } from 'svelte'; 3 - import { listRecords } from '$lib/oauth/atproto'; 4 - import EditingList from './EditingList.svelte'; 5 - import type { ListCollections } from '$lib/website/types'; 6 - import { getDataContext, isEditing } from '$lib/website/context'; 7 - 8 - let { 9 - collection, 10 - item, 11 - addItem, 12 - empty 13 - }: { 14 - collection: ListCollections; 15 - item: Snippet<[any, any]>; 16 - addItem: Snippet<[any]>; 17 - empty?: Snippet; 18 - } = $props(); 19 - 20 - const data = getDataContext(); 21 - const did = getContext('did') as string; 22 - </script> 23 - 24 - {#if isEditing()} 25 - {#await listRecords({ did, collection }) then records} 26 - <EditingList {records} {collection} {item} {addItem} {empty} /> 27 - {:catch error} 28 - {error} 29 - {/await} 30 - {:else if !data[collection] || Object.keys(data[collection]).length === 0} 31 - {@render empty?.()} 32 - {:else} 33 - {#each Object.values(data[collection]) as itemData} 34 - {@render item(itemData, () => {})} 35 - {/each} 36 - {/if}
-2
src/lib/website/components/list/index.ts
··· 1 - export { default as EditingList } from './EditingList.svelte'; 2 - export { default as List } from './List.svelte';
-25
src/lib/website/components/markdown/MarkdownItem.svelte
··· 1 - <script lang="ts"> 2 - import { type ElementType, type IndividualCollections } from '../../types'; 3 - import { SingleRecord } from '..'; 4 - import MarkdownText from './MarkdownText.svelte'; 5 - 6 - let { 7 - collection, 8 - rkey, 9 - key, 10 - placeholder, 11 - defaultContent 12 - }: { 13 - collection: IndividualCollections; 14 - rkey: ElementType<IndividualCollections>; 15 - key: string; 16 - placeholder?: string; 17 - defaultContent?: string; 18 - } = $props(); 19 - </script> 20 - 21 - <SingleRecord {collection} {rkey}> 22 - {#snippet child(data)} 23 - <MarkdownText {key} {placeholder} {defaultContent} {data} /> 24 - {/snippet} 25 - </SingleRecord>
-27
src/lib/website/components/markdown/MarkdownText.svelte
··· 1 - <script lang="ts"> 2 - import { isEditing } from '$lib/website/context'; 3 - import { marked } from 'marked'; 4 - import { getContext } from 'svelte'; 5 - 6 - let { 7 - key, 8 - data, 9 - placeholder, 10 - defaultContent, 11 - class: className 12 - }: { 13 - key: string; 14 - data: Record<string, any>; 15 - placeholder?: string; 16 - defaultContent?: string; 17 - class?: string; 18 - } = $props(); 19 - </script> 20 - 21 - {#if isEditing()} 22 - {#await import('./MarkdownTextEditor.svelte') then { default: MarkdownTextEditor }} 23 - <MarkdownTextEditor {key} data={data.value} {placeholder} {defaultContent} /> 24 - {/await} 25 - {:else} 26 - {@html marked.parse(data?.value?.[key] ?? defaultContent ?? ('' as string))} 27 - {/if}
-137
src/lib/website/components/markdown/MarkdownTextEditor.svelte
··· 1 - <script lang="ts"> 2 - import { onDestroy, onMount } from 'svelte'; 3 - import { Editor, type Content, type Extensions } from '@tiptap/core'; 4 - import StarterKit from '@tiptap/starter-kit'; 5 - import Image from '@tiptap/extension-image'; 6 - import Placeholder from '@tiptap/extension-placeholder'; 7 - import Link from '@tiptap/extension-link'; 8 - import { marked } from 'marked'; 9 - import { generateJSON } from '@tiptap/core'; 10 - import TurndownService from 'turndown'; 11 - import { RichTextLink } from './extensions/RichTextLink'; 12 - import { getUpdateRecordFunctionsContext } from '../../context'; 13 - 14 - let element: HTMLElement | undefined = $state(); 15 - let editor: Editor | null = $state(null); 16 - 17 - let loaded = $state(false); 18 - 19 - let edited = $state(false); 20 - 21 - let { 22 - key, 23 - data, 24 - placeholder = '', 25 - defaultContent = '' 26 - }: { 27 - key: string; 28 - data: Record<string, any>; 29 - placeholder?: string; 30 - defaultContent?: string; 31 - } = $props(); 32 - 33 - const updateFunctions = getUpdateRecordFunctionsContext(); 34 - 35 - const update = async () => { 36 - if (!edited || !editor) return {}; 37 - 38 - edited = false; 39 - 40 - const html = editor.getHTML(); 41 - 42 - var turndownService = new TurndownService({ 43 - headingStyle: 'atx', 44 - bulletListMarker: '-' 45 - }); 46 - const markdown = turndownService.turndown(html); 47 - 48 - edited = false; 49 - 50 - return { 51 - [key]: markdown 52 - }; 53 - }; 54 - 55 - onMount(async () => { 56 - if (!element || editor) return; 57 - 58 - let json: Content = ''; 59 - 60 - try { 61 - let html = await marked.parse(data[key] ?? (defaultContent as string)); 62 - 63 - // parse to json 64 - json = generateJSON(html, [ 65 - StarterKit.configure(), 66 - Image.configure(), 67 - RichTextLink.configure({ 68 - openOnClick: false 69 - }) 70 - ]); 71 - } catch (error) { 72 - console.error(error); 73 - } 74 - 75 - let extensions: Extensions = [ 76 - StarterKit.configure(), 77 - Image.configure(), 78 - Link.configure({ 79 - openOnClick: false 80 - }) 81 - ]; 82 - 83 - if (placeholder) { 84 - extensions.push( 85 - Placeholder.configure({ 86 - placeholder: placeholder 87 - }) 88 - ); 89 - } 90 - 91 - editor = new Editor({ 92 - element: element, 93 - extensions: extensions, 94 - onTransaction: () => { 95 - editor = editor; 96 - }, 97 - onUpdate: () => { 98 - edited = true; 99 - }, 100 - 101 - content: json, 102 - 103 - editorProps: { 104 - attributes: { 105 - class: 'outline-none' 106 - } 107 - } 108 - }); 109 - 110 - updateFunctions.push(update); 111 - 112 - loaded = true; 113 - }); 114 - 115 - onDestroy(() => { 116 - if (editor) { 117 - editor.destroy(); 118 - } 119 - 120 - updateFunctions.splice(updateFunctions.indexOf(update), 1); 121 - }); 122 - </script> 123 - 124 - <div bind:this={element}></div> 125 - 126 - <style> 127 - :global(.tiptap p.is-editor-empty:first-child::before) { 128 - color: var(--color-base-800); 129 - content: attr(data-placeholder); 130 - float: left; 131 - height: 0; 132 - pointer-events: none; 133 - } 134 - :global(.dark .tiptap p.is-editor-empty:first-child::before) { 135 - color: var(--color-base-200); 136 - } 137 - </style>
-125
src/lib/website/components/markdown/extensions/RichTextLink.ts
··· 1 - import { InputRule, markInputRule, markPasteRule, PasteRule } from '@tiptap/core'; 2 - import { Link } from '@tiptap/extension-link'; 3 - 4 - import type { LinkOptions } from '@tiptap/extension-link'; 5 - 6 - /** 7 - * The input regex for Markdown links with title support, and multiple quotation marks (required 8 - * in case the `Typography` extension is being included). 9 - */ 10 - const inputRegex = /(?:^|\s)\[([^\]]*)?\]\((\S+)(?: ["“](.+)["”])?\)$/i; 11 - 12 - /** 13 - * The paste regex for Markdown links with title support, and multiple quotation marks (required 14 - * in case the `Typography` extension is being included). 15 - */ 16 - const pasteRegex = /(?:^|\s)\[([^\]]*)?\]\((\S+)(?: ["“](.+)["”])?\)/gi; 17 - 18 - /** 19 - * Input rule built specifically for the `Link` extension, which ignores the auto-linked URL in 20 - * parentheses (e.g., `(https://doist.dev)`). 21 - * 22 - * @see https://github.com/ueberdosis/tiptap/discussions/1865 23 - */ 24 - function linkInputRule(config: Parameters<typeof markInputRule>[0]) { 25 - const defaultMarkInputRule = markInputRule(config); 26 - 27 - return new InputRule({ 28 - find: config.find, 29 - handler(props) { 30 - const { tr } = props.state; 31 - 32 - defaultMarkInputRule.handler(props); 33 - tr.setMeta('preventAutolink', true); 34 - } 35 - }); 36 - } 37 - 38 - /** 39 - * Paste rule built specifically for the `Link` extension, which ignores the auto-linked URL in 40 - * parentheses (e.g., `(https://doist.dev)`). This extension was inspired from the multiple 41 - * implementations found in a Tiptap discussion at GitHub. 42 - * 43 - * @see https://github.com/ueberdosis/tiptap/discussions/1865 44 - */ 45 - function linkPasteRule(config: Parameters<typeof markPasteRule>[0]) { 46 - const defaultMarkPasteRule = markPasteRule(config); 47 - 48 - return new PasteRule({ 49 - find: config.find, 50 - handler(props) { 51 - const { tr } = props.state; 52 - 53 - defaultMarkPasteRule.handler(props); 54 - tr.setMeta('preventAutolink', true); 55 - } 56 - }); 57 - } 58 - 59 - /** 60 - * The options available to customize the `RichTextLink` extension. 61 - */ 62 - type RichTextLinkOptions = LinkOptions; 63 - 64 - /** 65 - * Custom extension that extends the built-in `Link` extension to add additional input/paste rules 66 - * for converting the Markdown link syntax (i.e. `[Doist](https://doist.com)`) into links, and also 67 - * adds support for the `title` attribute. 68 - */ 69 - const RichTextLink = Link.extend<RichTextLinkOptions>({ 70 - inclusive: false, 71 - addOptions() { 72 - return { 73 - ...this.parent?.(), 74 - openOnClick: 'whenNotEditable' 75 - }; 76 - }, 77 - addAttributes() { 78 - return { 79 - ...this.parent?.(), 80 - title: { 81 - default: null 82 - } 83 - }; 84 - }, 85 - addInputRules() { 86 - return [ 87 - linkInputRule({ 88 - find: inputRegex, 89 - type: this.type, 90 - 91 - // We need to use `pop()` to remove the last capture groups from the match to 92 - // satisfy Tiptap's `markPasteRule` expectation of having the content as the last 93 - // capture group in the match (this makes the attribute order important) 94 - getAttributes(match) { 95 - return { 96 - title: match.pop()?.trim(), 97 - href: match.pop()?.trim() 98 - }; 99 - } 100 - }) 101 - ]; 102 - }, 103 - addPasteRules() { 104 - return [ 105 - linkPasteRule({ 106 - find: pasteRegex, 107 - type: this.type, 108 - 109 - // We need to use `pop()` to remove the last capture groups from the match to 110 - // satisfy Tiptap's `markInputRule` expectation of having the content as the last 111 - // capture group in the match (this makes the attribute order important) 112 - getAttributes(match) { 113 - return { 114 - title: match.pop()?.trim(), 115 - href: match.pop()?.trim() 116 - }; 117 - } 118 - }) 119 - ]; 120 - } 121 - }); 122 - 123 - export { RichTextLink }; 124 - 125 - export type { RichTextLinkOptions };
-3
src/lib/website/components/markdown/index.ts
··· 1 - export { default as MarkdownItem } from './MarkdownItem.svelte'; 2 - export { default as MarkdownText } from './MarkdownText.svelte'; 3 - export { default as MarkdownTextEditor } from './MarkdownTextEditor.svelte';
-26
src/lib/website/components/plain-text/PlainText.svelte
··· 1 - <script lang="ts"> 2 - import { isEditing } from '$lib/website/context'; 3 - import { getContext } from 'svelte'; 4 - 5 - let { 6 - key, 7 - data, 8 - placeholder, 9 - defaultContent, 10 - class: className 11 - }: { 12 - key: string; 13 - data: Record<string, any>; 14 - placeholder?: string; 15 - defaultContent?: string; 16 - class?: string; 17 - } = $props(); 18 - </script> 19 - 20 - {#if isEditing()} 21 - {#await import('./PlainTextEditor.svelte') then { default: PlainTextEditor }} 22 - <PlainTextEditor class={className} {key} data={data.value} {placeholder} {defaultContent} /> 23 - {/await} 24 - {:else} 25 - <span class={className}>{data?.value?.[key] || defaultContent || placeholder}</span> 26 - {/if}
-98
src/lib/website/components/plain-text/PlainTextEditor.svelte
··· 1 - <script lang="ts"> 2 - import { onDestroy, onMount } from 'svelte'; 3 - import { Editor, type Extensions } from '@tiptap/core'; 4 - import Placeholder from '@tiptap/extension-placeholder'; 5 - import Paragraph from '@tiptap/extension-paragraph'; 6 - import Document from '@tiptap/extension-document'; 7 - import Text from '@tiptap/extension-text'; 8 - import { getUpdateRecordFunctionsContext } from '../../context'; 9 - 10 - let element: HTMLElement | undefined = $state(); 11 - let editor: Editor | null = $state(null); 12 - 13 - let edited = $state(false); 14 - 15 - const updateFunctions = getUpdateRecordFunctionsContext(); 16 - 17 - let { 18 - key, 19 - data, 20 - class: className, 21 - placeholder = '', 22 - defaultContent = '' 23 - }: { 24 - key: string; 25 - data: Record<string, any>; 26 - class?: string; 27 - placeholder?: string; 28 - defaultContent?: string; 29 - } = $props(); 30 - 31 - const update = async () => { 32 - if (!edited || !editor) return {}; 33 - 34 - edited = false; 35 - 36 - return { 37 - [key]: editor.getText() 38 - }; 39 - }; 40 - 41 - onMount(async () => { 42 - if (!element || editor) return; 43 - 44 - updateFunctions.push(update); 45 - 46 - let extensions: Extensions = [Document.configure(), Paragraph.configure(), Text.configure()]; 47 - 48 - if (placeholder) { 49 - extensions.push( 50 - Placeholder.configure({ 51 - placeholder: placeholder 52 - }) 53 - ); 54 - } 55 - 56 - editor = new Editor({ 57 - element: element, 58 - extensions: extensions, 59 - onTransaction: () => { 60 - editor = editor; 61 - }, 62 - onUpdate: () => { 63 - edited = true; 64 - }, 65 - 66 - content: data[key] ?? defaultContent, 67 - 68 - editorProps: { 69 - attributes: { 70 - class: 'outline-none pointer-events-auto' 71 - } 72 - } 73 - }); 74 - }); 75 - 76 - onDestroy(() => { 77 - if (editor) { 78 - editor.destroy(); 79 - } 80 - 81 - updateFunctions.splice(updateFunctions.indexOf(update), 1); 82 - }); 83 - </script> 84 - 85 - <span class={className} bind:this={element}></span> 86 - 87 - <style> 88 - :global(.tiptap p.is-editor-empty:first-child::before) { 89 - color: var(--color-base-800); 90 - content: attr(data-placeholder); 91 - float: left; 92 - height: 0; 93 - pointer-events: none; 94 - } 95 - :global(.dark .tiptap p.is-editor-empty:first-child::before) { 96 - color: var(--color-base-200); 97 - } 98 - </style>
-25
src/lib/website/components/plain-text/PlainTextItem.svelte
··· 1 - <script lang="ts"> 2 - import { type ElementType, type IndividualCollections } from '../../types'; 3 - import { SingleRecord } from '..'; 4 - import PlainText from './PlainText.svelte'; 5 - 6 - let { 7 - collection, 8 - rkey = 'self', 9 - key, 10 - placeholder, 11 - defaultContent 12 - }: { 13 - collection: IndividualCollections; 14 - rkey?: ElementType<IndividualCollections>; 15 - key: string; 16 - placeholder?: string; 17 - defaultContent?: string; 18 - } = $props(); 19 - </script> 20 - 21 - <SingleRecord {collection} {rkey}> 22 - {#snippet child(data)} 23 - <PlainText {key} {placeholder} {defaultContent} {data} /> 24 - {/snippet} 25 - </SingleRecord>
-3
src/lib/website/components/plain-text/index.ts
··· 1 - export { default as PlainTextItem } from './PlainTextItem.svelte'; 2 - export { default as PlainTextEditor } from './PlainTextEditor.svelte'; 3 - export { default as PlainText } from './PlainText.svelte';
-57
src/lib/website/components/single-record/EditSingleRecord.svelte
··· 1 - <script lang="ts"> 2 - import { onDestroy, onMount, type Snippet } from 'svelte'; 3 - import { 4 - getUpdateFunctionsContext, 5 - type UpdateRecordFunction, 6 - setUpdateRecordFunctionsContext 7 - } from '../../context'; 8 - import { putRecord } from '$lib/oauth/atproto'; 9 - import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 10 - import { parseUri } from '$lib/website/utils'; 11 - 12 - let { 13 - data, 14 - child 15 - }: { 16 - data: ListRecord; 17 - child: Snippet<[ListRecord]>; 18 - } = $props(); 19 - 20 - let updateRecordFunctions: UpdateRecordFunction[] = $state([]); 21 - setUpdateRecordFunctionsContext(updateRecordFunctions); 22 - 23 - const updateFunctions = getUpdateFunctionsContext(); 24 - const update = async () => { 25 - const updated: Record<string, any> = {}; 26 - 27 - for (const updateFunction of updateRecordFunctions) { 28 - const updatedPart = await updateFunction(); 29 - for (const key in updatedPart) { 30 - updated[key] = updatedPart[key]; 31 - data.value[key] = updatedPart[key]; 32 - } 33 - } 34 - 35 - if (Object.keys(updated).length > 0 || !data.cid) { 36 - if (!data.value.createdAt) { 37 - data.value.createdAt = new Date().toISOString(); 38 - } 39 - data.value.updatedAt = new Date().toISOString(); 40 - 41 - const { collection, rkey } = parseUri(data.uri); 42 - await putRecord({ collection, rkey, record: data.value }); 43 - return true; 44 - } 45 - 46 - return false; 47 - }; 48 - 49 - onMount(() => { 50 - updateFunctions.push(update); 51 - }); 52 - onDestroy(() => { 53 - updateFunctions.splice(updateFunctions.indexOf(update), 1); 54 - }); 55 - </script> 56 - 57 - {@render child(data)}
-48
src/lib/website/components/single-record/SingleRecord.svelte
··· 1 - <script lang="ts"> 2 - import { getRecord } from '$lib/oauth/atproto'; 3 - import { type Snippet } from 'svelte'; 4 - import { type ElementType, type IndividualCollections } from '../../types'; 5 - import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 6 - import { getDataContext, getDidContext, isEditing } from '$lib/website/context'; 7 - 8 - let { 9 - collection, 10 - rkey, 11 - child 12 - }: { 13 - collection: IndividualCollections; 14 - rkey: ElementType<IndividualCollections>; 15 - child: Snippet<[ListRecord]>; 16 - } = $props(); 17 - 18 - const data = getDataContext(); 19 - const did = getDidContext(); 20 - 21 - $inspect(data['app.bsky.actor.profile']); 22 - </script> 23 - 24 - {#if isEditing()} 25 - {#await import('./EditSingleRecord.svelte') then { default: EditSingleRecord }} 26 - {#await getRecord({ did, collection, rkey }) then record} 27 - <EditSingleRecord data={record}> 28 - {#snippet child(recordData)} 29 - {@render child(recordData)} 30 - {/snippet} 31 - </EditSingleRecord> 32 - {:catch error} 33 - <EditSingleRecord 34 - data={{ 35 - uri: 'at://' + did + '/' + collection + '/' + rkey, 36 - value: {}, 37 - cid: '' 38 - }} 39 - > 40 - {#snippet child(recordData)} 41 - {@render child(recordData)} 42 - {/snippet} 43 - </EditSingleRecord> 44 - {/await} 45 - {/await} 46 - {:else} 47 - {@render child(data?.[collection]?.[rkey])} 48 - {/if}
-2
src/lib/website/components/single-record/index.ts
··· 1 - export { default as EditSingleRecord } from './EditSingleRecord.svelte'; 2 - export { default as SingleRecord } from './SingleRecord.svelte';
-72
src/lib/website/foxui/avatar/Avatar.svelte
··· 1 - <script lang="ts"> 2 - import { cn } from '..'; 3 - import { Avatar as AvatarPrimitive, type WithoutChildrenOrChild } from 'bits-ui'; 4 - 5 - let { 6 - src, 7 - alt, 8 - fallback, 9 - ref = $bindable(null), 10 - 11 - imageRef = $bindable(null), 12 - imageClass, 13 - 14 - fallbackRef = $bindable(null), 15 - fallbackClass, 16 - 17 - useThemeColor = false, 18 - 19 - class: className, 20 - ...restProps 21 - }: WithoutChildrenOrChild<AvatarPrimitive.RootProps> & { 22 - fallback?: string; 23 - imageRef?: HTMLImageElement | null; 24 - imageClass?: string; 25 - fallbackRef?: HTMLElement | null; 26 - fallbackClass?: string; 27 - 28 - src?: string; 29 - alt?: string; 30 - 31 - useThemeColor?: boolean; 32 - } = $props(); 33 - </script> 34 - 35 - <div 36 - class={cn( 37 - 'border-base-300 bg-base-200 text-base-900 dark:border-base-800 dark:bg-base-900 dark:text-base-50 relative isolate flex size-10 shrink-0 overflow-hidden rounded-full border', 38 - className 39 - )} 40 - {...restProps} 41 - bind:this={ref} 42 - > 43 - {#if fallback} 44 - <span class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 font-medium" 45 - >{fallback}</span 46 - > 47 - {:else} 48 - <svg 49 - xmlns="http://www.w3.org/2000/svg" 50 - viewBox="0 0 24 24" 51 - fill="currentColor" 52 - class="text-base-400 dark:text-base-600 absolute left-1/2 top-1/2 mt-[15%] size-full -translate-x-1/2 -translate-y-1/2" 53 - > 54 - <path 55 - fill-rule="evenodd" 56 - d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" 57 - clip-rule="evenodd" 58 - /> 59 - </svg> 60 - {/if} 61 - {#if src} 62 - <img 63 - bind:this={imageRef} 64 - {src} 65 - alt={alt ?? ''} 66 - class="z-10 aspect-square size-full object-cover" 67 - onerror={() => { 68 - imageRef?.classList.add('hidden'); 69 - }} 70 - /> 71 - {/if} 72 - </div>
-3
src/lib/website/foxui/avatar/index.ts
··· 1 - import Avatar from './Avatar.svelte'; 2 - 3 - export { Avatar };
-23
src/lib/website/foxui/bluesky-login/BlueskyLogin.svelte
··· 1 - <script lang="ts"> 2 - import { Button } from '../button'; 3 - import { BlueskyLoginModal, blueskyLoginModalState, type BlueskyLoginProps } from '.'; 4 - 5 - let { login, formAction, formMethod }: BlueskyLoginProps = $props(); 6 - </script> 7 - 8 - <Button onclick={() => blueskyLoginModalState.show()}> 9 - <svg 10 - fill="currentColor" 11 - xmlns="http://www.w3.org/2000/svg" 12 - viewBox="-40 -40 680 620" 13 - version="1.1" 14 - aria-hidden="true" 15 - > 16 - <path 17 - d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z" 18 - /> 19 - </svg> 20 - Login 21 - </Button> 22 - 23 - <BlueskyLoginModal {login} {formAction} {formMethod} />
-141
src/lib/website/foxui/bluesky-login/BlueskyLoginModal.svelte
··· 1 - <script lang="ts" module> 2 - export const blueskyLoginModalState = $state({ 3 - open: false, 4 - show: () => (blueskyLoginModalState.open = true), 5 - hide: () => (blueskyLoginModalState.open = false) 6 - }); 7 - </script> 8 - 9 - <script lang="ts"> 10 - import { Button, Modal, Subheading, Label, Input, Avatar } from '..'; 11 - import type { BlueskyLoginProps } from '.'; 12 - 13 - let value = $state(''); 14 - let error: string | null = $state(null); 15 - let loading = $state(false); 16 - 17 - let { login, formAction, formMethod = 'get' }: BlueskyLoginProps = $props(); 18 - 19 - async function onLogin(handle: string) { 20 - if (loading || !login) return; 21 - 22 - loading = true; 23 - error = null; 24 - 25 - try { 26 - const hide = await login(handle); 27 - 28 - if (hide) { 29 - blueskyLoginModalState.hide(); 30 - value = ''; 31 - } 32 - } catch (err) { 33 - error = err instanceof Error ? err.message : String(err); 34 - } finally { 35 - loading = false; 36 - } 37 - } 38 - 39 - async function onSubmit(evt: Event) { 40 - if (formAction || !login) return; 41 - evt.preventDefault(); 42 - 43 - await onLogin(value); 44 - } 45 - 46 - let input: HTMLInputElement | null = $state(null); 47 - 48 - let lastLogin: { handle: string; avatar: string } | null = $state(null); 49 - 50 - $effect(() => { 51 - let lastLoginDid = localStorage.getItem('last-login'); 52 - 53 - if (lastLoginDid) { 54 - let profile = localStorage.getItem(`profile-${lastLoginDid}`); 55 - 56 - if (profile) { 57 - lastLogin = JSON.parse(profile); 58 - } 59 - } 60 - }); 61 - </script> 62 - 63 - <Modal 64 - bind:open={blueskyLoginModalState.open} 65 - class="max-w-sm gap-2 p-4 sm:p-6" 66 - onOpenAutoFocus={(e: Event) => { 67 - e.preventDefault(); 68 - input?.focus(); 69 - }} 70 - > 71 - <form onsubmit={onSubmit} action={formAction} method={formMethod} class="flex flex-col gap-2"> 72 - <Subheading class="mb-1 inline-flex items-center gap-2 text-xl font-bold"> 73 - <svg 74 - fill="currentColor" 75 - xmlns="http://www.w3.org/2000/svg" 76 - viewBox="-40 -40 680 620" 77 - version="1.1" 78 - class={['text-accent-600 dark:text-accent-400 size-6']} 79 - aria-hidden="true" 80 - > 81 - <path 82 - d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z" 83 - /> 84 - </svg> 85 - Login with Bluesky</Subheading 86 - > 87 - 88 - <div class="text-base-600 dark:text-base-400 text-xs leading-5"> 89 - Don't have an account? 90 - <br /> 91 - <a 92 - href="https://bsky.app" 93 - target="_blank" 94 - class="text-accent-600 dark:text-accent-400 dark:hover:text-accent-500 hover:text-accent-500 font-medium transition-colors" 95 - > 96 - Sign up on bluesky 97 - </a>, then come back here. 98 - </div> 99 - 100 - {#if lastLogin} 101 - <Label for="bluesky-handle" class="mt-4 text-sm">Recent login:</Label> 102 - <Button 103 - class="max-w-xs justify-start overflow-x-hidden truncate" 104 - variant="primary" 105 - onclick={() => onLogin(lastLogin?.handle ?? '')} 106 - disabled={loading} 107 - > 108 - <Avatar src={lastLogin.avatar} class="size-6" /> 109 - 110 - <div 111 - class="text-accent-600 dark:text-accent-400 text-md max-w-full overflow-x-hidden truncate font-semibold" 112 - > 113 - <p>{loading ? 'Loading...' : lastLogin.handle}</p> 114 - </div> 115 - </Button> 116 - {/if} 117 - 118 - <div class="mt-4 w-full"> 119 - <Label for="bluesky-handle" class="text-sm">Your handle</Label> 120 - <div class="mt-2"> 121 - <Input 122 - bind:ref={input} 123 - type="text" 124 - name="bluesky-handle" 125 - id="bluesky-handle" 126 - placeholder="yourname.bsky.social" 127 - class="w-full" 128 - bind:value 129 - /> 130 - </div> 131 - </div> 132 - 133 - {#if error} 134 - <p class="text-accent-500 mt-2 text-sm font-medium">{error}</p> 135 - {/if} 136 - 137 - <Button type="submit" class="ml-auto mt-2 w-full lg:w-auto" disabled={loading} 138 - >{loading ? 'Loading...' : 'Login'}</Button 139 - > 140 - </form> 141 - </Modal>
-9
src/lib/website/foxui/bluesky-login/index.ts
··· 1 - export { default as BlueskyLoginModal } from './BlueskyLoginModal.svelte'; 2 - export { blueskyLoginModalState } from './BlueskyLoginModal.svelte'; 3 - export { default as BlueskyLogin } from './BlueskyLogin.svelte'; 4 - 5 - export type BlueskyLoginProps = { 6 - login?: (handle: string) => Promise<boolean | undefined>; 7 - formAction?: string; 8 - formMethod?: 'dialog' | 'get' | 'post' | 'DIALOG' | 'GET' | 'POST' | null; 9 - };
-97
src/lib/website/foxui/button/Button.svelte
··· 1 - <script lang="ts" module> 2 - import type { WithElementRef } from 'bits-ui'; 3 - import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'; 4 - import { type VariantProps, tv } from 'tailwind-variants'; 5 - import { cn } from '../'; 6 - 7 - export const buttonVariants = tv({ 8 - base: 'touch-manipulation hover:cursor-pointer hover:scale-101 focus-visible:scale-101 disabled:hover:scale-100 motion-safe:focus-visible:transition-transform focus-visible:outline-2 outline-offset-2 inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-2xl active:scale-98 text-sm font-medium motion-safe:transition-all disabled:pointer-events-none disabled:opacity-60 duration-800 active:duration-100 hover:duration-300 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 9 - variants: { 10 - variant: { 11 - primary: 12 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-accent-500/5 dark:shadow-accent-500/2 disabled:shadow-md active:shadow-md inset-shadow-sm inset-shadow-accent-700/5 dark:inset-shadow-accent-500/2 focus-visible:outline-accent-500 border border-accent-500/15 dark:border-accent-500/15 hover:bg-accent-200/60 dark:hover:bg-accent-950/25 bg-accent-200/50 dark:bg-accent-950/20 text-accent-950 dark:text-accent-400', 13 - secondary: 14 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-base-500/5 dark:shadow-base-500/2 active:shadow-md inset-shadow-sm inset-shadow-base-700/2 dark:inset-shadow-base-500/2 focus-visible:outline-base-800 dark:focus-visible:outline-base-200 bg-base-300/40 dark:bg-base-800/30 text-base-900 dark:text-base-50 hover:bg-base-300/45 dark:hover:bg-base-800/35 border border-base-300/50 dark:border-base-700/30', 15 - link: 'focus-visible:outline-base-900 dark:focus-visible:outline-base-50 text-base-800 dark:text-base-200 font-semibold hover:text-accent-600 dark:hover:text-accent-400 data-[current=true]:text-accent-600 dark:data-[current=true]:text-accent-400', 16 - ghost: 17 - 'focus-visible:outline-base-900 dark:focus-visible:outline-base-50 text-base-800 dark:text-base-200 font-semibold hover:text-accent-600 hover:bg-accent-400/5 data-[current=true]:bg-accent-500/5 dark:hover:text-accent-400 dark:hover:bg-accent-700/5 data-[current=true]:text-accent-600 dark:data-[current=true]:text-accent-400 dark:data-[current=true]:bg-accent-500/5', 18 - 19 - red: 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-red-500/5 dark:shadow-red-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-red-500/15 inset-shadow-sm inset-shadow-red-700/5 dark:inset-shadow-red-500/2 focus-visible:outline-red-500 border border-red-500/15 dark:border-red-500/15 hover:bg-red-200/60 dark:hover:bg-red-950/30 bg-red-200/50 dark:bg-red-950/20 text-red-800 dark:text-red-400', 20 - orange: 21 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-orange-500/5 dark:shadow-orange-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-orange-500/15 inset-shadow-sm inset-shadow-orange-700/5 dark:inset-shadow-orange-500/2 focus-visible:outline-orange-500 border border-orange-500/15 dark:border-orange-500/15 hover:bg-orange-200/60 dark:hover:bg-orange-950/30 bg-orange-200/50 dark:bg-orange-950/20 text-orange-800 dark:text-orange-400', 22 - amber: 23 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-amber-500/5 dark:shadow-amber-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-amber-500/15 inset-shadow-sm inset-shadow-amber-700/5 dark:inset-shadow-amber-500/2 focus-visible:outline-amber-500 border border-amber-500/15 dark:border-amber-500/15 hover:bg-amber-200/60 dark:hover:bg-amber-950/30 bg-amber-200/50 dark:bg-amber-950/20 text-amber-800 dark:text-amber-400', 24 - yellow: 25 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-yellow-500/5 dark:shadow-yellow-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-yellow-500/15 inset-shadow-sm inset-shadow-yellow-700/5 dark:inset-shadow-yellow-500/2 focus-visible:outline-yellow-500 border border-yellow-500/15 dark:border-yellow-500/15 hover:bg-yellow-200/60 dark:hover:bg-yellow-950/30 bg-yellow-200/50 dark:bg-yellow-950/20 text-yellow-800 dark:text-yellow-400', 26 - lime: 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-lime-500/5 dark:shadow-lime-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-lime-500/15 inset-shadow-sm inset-shadow-lime-700/5 dark:inset-shadow-lime-500/2 focus-visible:outline-lime-500 border border-lime-500/15 dark:border-lime-500/15 hover:bg-lime-200/60 dark:hover:bg-lime-950/30 bg-lime-200/50 dark:bg-lime-950/20 text-lime-800 dark:text-lime-400', 27 - green: 28 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-green-500/5 dark:shadow-green-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-green-500/15 inset-shadow-sm inset-shadow-green-700/5 dark:inset-shadow-green-500/2 focus-visible:outline-green-500 border border-green-500/15 dark:border-green-500/15 hover:bg-green-200/60 dark:hover:bg-green-950/30 bg-green-200/50 dark:bg-green-950/20 text-green-800 dark:text-green-400', 29 - emerald: 30 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-emerald-500/5 dark:shadow-emerald-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-emerald-500/15 inset-shadow-sm inset-shadow-emerald-700/5 dark:inset-shadow-emerald-500/2 focus-visible:outline-emerald-500 border border-emerald-500/15 dark:border-emerald-500/15 hover:bg-emerald-200/60 dark:hover:bg-emerald-950/30 bg-emerald-200/50 dark:bg-emerald-950/20 text-emerald-800 dark:text-emerald-400', 31 - teal: 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-teal-500/5 dark:shadow-teal-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-teal-500/15 inset-shadow-sm inset-shadow-teal-700/5 dark:inset-shadow-teal-500/2 focus-visible:outline-teal-500 border border-teal-500/15 dark:border-teal-500/15 hover:bg-teal-200/60 dark:hover:bg-teal-950/30 bg-teal-200/50 dark:bg-teal-950/20 text-teal-800 dark:text-teal-400', 32 - cyan: 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-cyan-500/5 dark:shadow-cyan-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-cyan-500/15 inset-shadow-sm inset-shadow-cyan-700/5 dark:inset-shadow-cyan-500/2 focus-visible:outline-cyan-500 border border-cyan-500/15 dark:border-cyan-500/15 hover:bg-cyan-200/60 dark:hover:bg-cyan-950/30 bg-cyan-200/50 dark:bg-cyan-950/20 text-cyan-800 dark:text-cyan-400', 33 - sky: 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-sky-500/5 dark:shadow-sky-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-sky-500/15 inset-shadow-sm inset-shadow-sky-700/5 dark:inset-shadow-sky-500/2 focus-visible:outline-sky-500 border border-sky-500/15 dark:border-sky-500/15 hover:bg-sky-200/60 dark:hover:bg-sky-950/30 bg-sky-200/50 dark:bg-sky-950/20 text-sky-800 dark:text-sky-400', 34 - blue: 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-blue-500/5 dark:shadow-blue-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-blue-500/15 inset-shadow-sm inset-shadow-blue-700/5 dark:inset-shadow-blue-500/2 focus-visible:outline-blue-500 border border-blue-500/15 dark:border-blue-500/15 hover:bg-blue-200/60 dark:hover:bg-blue-950/30 bg-blue-200/50 dark:bg-blue-950/20 text-blue-800 dark:text-blue-400', 35 - indigo: 36 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-indigo-500/5 dark:shadow-indigo-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-indigo-500/15 inset-shadow-sm inset-shadow-indigo-700/5 dark:inset-shadow-indigo-500/2 focus-visible:outline-indigo-500 border border-indigo-500/15 dark:border-indigo-500/15 hover:bg-indigo-200/60 dark:hover:bg-indigo-950/30 bg-indigo-200/50 dark:bg-indigo-950/20 text-indigo-800 dark:text-indigo-400', 37 - violet: 38 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-violet-500/5 dark:shadow-violet-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-violet-500/15 inset-shadow-sm inset-shadow-violet-700/5 dark:inset-shadow-violet-500/2 focus-visible:outline-violet-500 border border-violet-500/15 dark:border-violet-500/15 hover:bg-violet-200/60 dark:hover:bg-violet-950/30 bg-violet-200/50 dark:bg-violet-950/20 text-violet-800 dark:text-violet-400', 39 - purple: 40 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-purple-500/5 dark:shadow-purple-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-purple-500/15 inset-shadow-sm inset-shadow-purple-700/5 dark:inset-shadow-purple-500/2 focus-visible:outline-purple-500 border border-purple-500/15 dark:border-purple-500/15 hover:bg-purple-200/60 dark:hover:bg-purple-950/30 bg-purple-200/50 dark:bg-purple-950/20 text-purple-800 dark:text-purple-400', 41 - fuchsia: 42 - 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-fuchsia-500/5 dark:shadow-fuchsia-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-fuchsia-500/15 inset-shadow-sm inset-shadow-fuchsia-700/5 dark:inset-shadow-fuchsia-500/2 focus-visible:outline-fuchsia-500 border border-fuchsia-500/15 dark:border-fuchsia-500/15 hover:bg-fuchsia-200/60 dark:hover:bg-fuchsia-950/30 bg-fuchsia-200/50 dark:bg-fuchsia-950/20 text-fuchsia-800 dark:text-fuchsia-400', 43 - pink: 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-pink-500/5 dark:shadow-pink-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-pink-500/15 inset-shadow-sm inset-shadow-pink-700/5 dark:inset-shadow-pink-500/2 focus-visible:outline-pink-500 border border-pink-500/15 dark:border-pink-500/15 hover:bg-pink-200/60 dark:hover:bg-pink-950/30 bg-pink-200/50 dark:bg-pink-950/20 text-pink-800 dark:text-pink-400', 44 - rose: 'backdrop-blur-md backdrop-brightness-105 shadow-lg transform-all shadow-rose-500/5 dark:shadow-rose-500/2 disabled:shadow-md active:shadow-md focus-visible:inset-shadow-rose-500/15 inset-shadow-sm inset-shadow-rose-700/5 dark:inset-shadow-rose-500/2 focus-visible:outline-rose-500 border border-rose-500/15 dark:border-rose-500/15 hover:bg-rose-200/60 dark:hover:bg-rose-950/30 bg-rose-200/50 dark:bg-rose-950/20 text-rose-800 dark:text-rose-400' 45 - }, 46 - size: { 47 - default: 'px-3 py-1.5', 48 - sm: 'px-2 text-xs py-1 font-base [&_svg]:size-3 gap-1.5', 49 - lg: 'px-4 gap-2.5 text-lg py-2 font-semibold [&_svg]:size-5 gap-2.5', 50 - icon: 'p-2', 51 - iconSm: 'p-1.5 [&_svg]:size-3', 52 - iconLg: 'p-3 [&_svg]:size-5' 53 - } 54 - }, 55 - defaultVariants: { 56 - variant: 'primary', 57 - size: 'default' 58 - } 59 - }); 60 - 61 - export type ButtonVariant = VariantProps<typeof buttonVariants>['variant']; 62 - export type ButtonSize = VariantProps<typeof buttonVariants>['size']; 63 - 64 - export type ButtonProps = WithElementRef<HTMLButtonAttributes> & 65 - WithElementRef<HTMLAnchorAttributes> & { 66 - variant?: ButtonVariant; 67 - size?: ButtonSize; 68 - }; 69 - </script> 70 - 71 - <script lang="ts"> 72 - let { 73 - class: className, 74 - variant = 'primary', 75 - size = 'default', 76 - ref = $bindable(null), 77 - href = undefined, 78 - type = 'button', 79 - children, 80 - ...restProps 81 - }: ButtonProps = $props(); 82 - </script> 83 - 84 - {#if href} 85 - <a bind:this={ref} class={cn(buttonVariants({ variant, size }), className)} {href} {...restProps}> 86 - {@render children?.()} 87 - </a> 88 - {:else} 89 - <button 90 - bind:this={ref} 91 - class={cn(buttonVariants({ variant, size }), className)} 92 - {type} 93 - {...restProps} 94 - > 95 - {@render children?.()} 96 - </button> 97 - {/if}
-7
src/lib/website/foxui/button/index.ts
··· 1 - export { 2 - default as Button, 3 - type ButtonProps, 4 - type ButtonSize, 5 - type ButtonVariant, 6 - buttonVariants 7 - } from './Button.svelte';
-24
src/lib/website/foxui/heading/Heading.svelte
··· 1 - <script lang="ts"> 2 - import type { WithElementRef } from 'bits-ui'; 3 - import type { HTMLBaseAttributes } from 'svelte/elements'; 4 - import { cn } from '..'; 5 - 6 - let { 7 - ref = $bindable(null), 8 - class: className, 9 - level = 1, 10 - children, 11 - ...restProps 12 - }: WithElementRef<HTMLBaseAttributes> & { 13 - level?: 1 | 2 | 3 | 4 | 5 | 6; 14 - } = $props(); 15 - </script> 16 - 17 - <svelte:element 18 - this={'h' + level} 19 - bind:this={ref} 20 - class={cn('text-base-900 dark:text-base-50 text-2xl font-semibold', className)} 21 - {...restProps} 22 - > 23 - {@render children?.()} 24 - </svelte:element>
-24
src/lib/website/foxui/heading/Subheading.svelte
··· 1 - <script lang="ts"> 2 - import type { WithElementRef } from 'bits-ui'; 3 - import type { HTMLBaseAttributes } from 'svelte/elements'; 4 - import { cn } from '..'; 5 - 6 - let { 7 - ref = $bindable(null), 8 - class: className, 9 - level = 2, 10 - children, 11 - ...restProps 12 - }: WithElementRef<HTMLBaseAttributes> & { 13 - level?: 1 | 2 | 3 | 4 | 5 | 6; 14 - } = $props(); 15 - </script> 16 - 17 - <svelte:element 18 - this={'h' + level} 19 - bind:this={ref} 20 - class={cn('text-base-900 dark:text-base-50 text-base font-semibold sm:text-lg', className)} 21 - {...restProps} 22 - > 23 - {@render children?.()} 24 - </svelte:element>
-4
src/lib/website/foxui/heading/index.ts
··· 1 - import Heading from './Heading.svelte'; 2 - import Subheading from './Subheading.svelte'; 3 - 4 - export { Heading, Subheading };
-16
src/lib/website/foxui/index.ts
··· 1 - import { type ClassValue, clsx } from 'clsx'; 2 - import { twMerge } from 'tailwind-merge'; 3 - 4 - export function cn(...inputs: ClassValue[]) { 5 - return twMerge(clsx(inputs)); 6 - } 7 - 8 - export * from './avatar'; 9 - export * from './bluesky-login'; 10 - export * from './button'; 11 - export * from './heading'; 12 - export * from './input'; 13 - export * from './label'; 14 - export * from './modal'; 15 - export * from './navbar'; 16 - export * from './sonner';
-60
src/lib/website/foxui/input/Input.svelte
··· 1 - <script lang="ts" module> 2 - import type { WithElementRef } from 'bits-ui'; 3 - import { type VariantProps, tv } from 'tailwind-variants'; 4 - import { cn } from '..'; 5 - 6 - import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements'; 7 - 8 - export const inputVariants = tv({ 9 - base: 'focus:ring-2 ring-1 ring-inset border-0 focus:transition-transform rounded-2xl text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed duration-300 active:duration-100', 10 - variants: { 11 - variant: { 12 - primary: 13 - 'focus:ring-accent-500 dark:focus:ring-accent-500 ring-accent-500/30 dark:ring-accent-500/20 bg-accent-400/5 dark:bg-accent-600/5 text-accent-700 dark:text-accent-400 placeholder:text-accent-700/50 dark:placeholder:text-accent-400/50', 14 - secondary: 15 - 'focus:ring-base-800 dark:focus:ring-base-200 bg-base-100/50 dark:bg-base-900/50 text-base-900 dark:text-base-50 ring-base-200 dark:ring-base-800 placeholder:text-base-900/50 dark:placeholder:text-base-50/50' 16 - }, 17 - sizeVariant: { 18 - default: 'px-3 py-1.5 text-base', 19 - sm: 'px-3 text-xs py-1.5 font-base', 20 - lg: 'px-4 text-lg py-2 font-semibold' 21 - } 22 - }, 23 - defaultVariants: { 24 - variant: 'primary', 25 - sizeVariant: 'default' 26 - } 27 - }); 28 - 29 - export type InputVariant = VariantProps<typeof inputVariants>['variant']; 30 - export type InputSize = VariantProps<typeof inputVariants>['sizeVariant']; 31 - 32 - type InputType = Exclude<HTMLInputTypeAttribute, 'file'>; 33 - 34 - export type InputProps = WithElementRef< 35 - Omit<HTMLInputAttributes, 'type'> & { type?: InputType } 36 - > & { 37 - variant?: InputVariant; 38 - sizeVariant?: InputSize; 39 - }; 40 - </script> 41 - 42 - <script lang="ts"> 43 - let { 44 - ref = $bindable(null), 45 - value = $bindable(), 46 - type, 47 - class: className, 48 - variant = 'primary', 49 - sizeVariant = 'default', 50 - ...restProps 51 - }: InputProps = $props(); 52 - </script> 53 - 54 - <input 55 - bind:this={ref} 56 - class={cn(inputVariants({ variant, sizeVariant }), className)} 57 - {type} 58 - bind:value 59 - {...restProps} 60 - />
-8
src/lib/website/foxui/input/index.ts
··· 1 - import Root, { 2 - type InputProps, 3 - type InputSize, 4 - type InputVariant, 5 - inputVariants 6 - } from './Input.svelte'; 7 - 8 - export { type InputProps, Root as Input, inputVariants, type InputSize, type InputVariant };
-19
src/lib/website/foxui/label/Label.svelte
··· 1 - <script lang="ts"> 2 - import { Label as LabelPrimitive } from 'bits-ui'; 3 - import { cn } from '..'; 4 - 5 - let { 6 - ref = $bindable(null), 7 - class: className, 8 - ...restProps 9 - }: LabelPrimitive.RootProps = $props(); 10 - </script> 11 - 12 - <LabelPrimitive.Root 13 - bind:ref 14 - class={cn( 15 - 'text-base-900 dark:text-base-50 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 16 - className 17 - )} 18 - {...restProps} 19 - />
-1
src/lib/website/foxui/label/index.ts
··· 1 - export { default as Label } from './Label.svelte';
-167
src/lib/website/foxui/modal/Modal.svelte
··· 1 - <script lang="ts" module> 2 - import type { Snippet } from 'svelte'; 3 - import { Dialog, type WithoutChild } from 'bits-ui'; 4 - import { Button, type ButtonProps } from '../button'; 5 - 6 - export type ModalProps = Dialog.RootProps & { 7 - title?: string; 8 - titleSnippet?: Snippet; 9 - titleClass?: string; 10 - description?: string; 11 - descriptionSnippet?: Snippet; 12 - descriptionClass?: string; 13 - interactOutsideBehavior?: 'close' | 'ignore'; 14 - closeButton?: boolean; 15 - contentProps?: WithoutChild<Dialog.ContentProps>; 16 - 17 - yesButton?: 18 - | boolean 19 - | { 20 - label?: string; 21 - onclick?: () => void; 22 - variant?: ButtonProps['variant']; 23 - disabled?: boolean; 24 - class?: string; 25 - }; 26 - 27 - noButton?: 28 - | boolean 29 - | { 30 - label?: string; 31 - onclick?: () => void; 32 - variant?: ButtonProps['variant']; 33 - disabled?: boolean; 34 - class?: string; 35 - }; 36 - 37 - class?: string; 38 - 39 - onOpenAutoFocus?: (event: Event) => void; 40 - }; 41 - </script> 42 - 43 - <script lang="ts"> 44 - import { cn } from '..'; 45 - 46 - let { 47 - open = $bindable(false), 48 - children, 49 - contentProps, 50 - title, 51 - titleSnippet, 52 - titleClass, 53 - description, 54 - descriptionSnippet, 55 - descriptionClass, 56 - interactOutsideBehavior = 'close', 57 - closeButton = true, 58 - yesButton, 59 - noButton, 60 - class: className, 61 - onOpenAutoFocus, 62 - ...restProps 63 - }: ModalProps = $props(); 64 - 65 - let yesButtonRef = $state<HTMLButtonElement | null>(null); 66 - </script> 67 - 68 - <Dialog.Root bind:open {...restProps}> 69 - <Dialog.Portal> 70 - <Dialog.Overlay 71 - class="motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-base-200/10 dark:bg-base-900/10 fixed inset-0 z-50 backdrop-blur-sm" 72 - /> 73 - <Dialog.Content 74 - {onOpenAutoFocus} 75 - {interactOutsideBehavior} 76 - {...contentProps} 77 - class={cn( 78 - 'motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-bottom-1/2 data-[state=open]:slide-in-from-bottom-1/2', 79 - 'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-1rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] shadow-lg', 80 - 'bg-base-50/60 dark:bg-base-900/60 border-base-200/80 dark:border-base-800 gap-4 rounded-2xl border p-6 backdrop-blur-xl', 81 - className 82 - )} 83 - > 84 - {#if title} 85 - <Dialog.Title class="text-base-900 dark:text-base-100 text-lg font-semibold" 86 - >{title}</Dialog.Title 87 - > 88 - {/if} 89 - {#if titleSnippet} 90 - <Dialog.Title class={titleClass}> 91 - {@render titleSnippet()} 92 - </Dialog.Title> 93 - {/if} 94 - {#if description} 95 - <Dialog.Description class="text-base-600 dark:text-base-400 text-sm" 96 - >{description}</Dialog.Description 97 - > 98 - {/if} 99 - {#if descriptionSnippet} 100 - <Dialog.Description class={descriptionClass}> 101 - {@render descriptionSnippet?.()} 102 - </Dialog.Description> 103 - {/if} 104 - 105 - {#if yesButton || noButton} 106 - <div class="flex flex-col items-stretch justify-end gap-2 lg:flex-row lg:items-center"> 107 - {#if yesButton} 108 - <Button 109 - bind:ref={yesButtonRef} 110 - onclick={() => { 111 - open = false; 112 - if (typeof yesButton === 'object') { 113 - yesButton.onclick?.(); 114 - } 115 - }} 116 - class={cn('order-last', typeof yesButton === 'object' ? yesButton.class : '')} 117 - variant={typeof yesButton === 'object' ? yesButton?.variant || 'primary' : 'primary'} 118 - disabled={typeof yesButton === 'object' ? yesButton?.disabled : false} 119 - > 120 - {typeof yesButton === 'object' ? yesButton.label || 'Yes' : 'Yes'} 121 - </Button> 122 - {/if} 123 - {#if noButton} 124 - <Button 125 - onclick={() => { 126 - open = false; 127 - if (typeof noButton === 'object') { 128 - noButton.onclick?.(); 129 - } 130 - }} 131 - class={cn(typeof noButton === 'object' ? noButton.class : '')} 132 - variant={typeof noButton === 'object' 133 - ? noButton?.variant || 'secondary' 134 - : 'secondary'} 135 - disabled={typeof noButton === 'object' ? noButton?.disabled : false} 136 - > 137 - {typeof noButton === 'object' ? noButton.label || 'No' : 'No'} 138 - </Button> 139 - {/if} 140 - </div> 141 - {/if} 142 - 143 - {@render children?.()} 144 - 145 - {#if closeButton} 146 - <Dialog.Close 147 - class="text-base-900 dark:text-base-500 hover:text-base-800 dark:hover:text-base-200 hover:bg-base-200 dark:hover:bg-base-800 focus:outline-base-900 dark:focus:outline-base-50 focus:bg-base-200 dark:focus:bg-base-800 focus:text-base-800 dark:focus:text-base-200 absolute right-2 top-2 cursor-pointer rounded-xl p-1 transition-colors focus:outline-2 focus:outline-offset-2" 148 - > 149 - <svg 150 - xmlns="http://www.w3.org/2000/svg" 151 - viewBox="0 0 24 24" 152 - fill="currentColor" 153 - class="size-4" 154 - > 155 - <path 156 - fill-rule="evenodd" 157 - d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z" 158 - clip-rule="evenodd" 159 - /> 160 - </svg> 161 - 162 - <span class="sr-only">Close</span> 163 - </Dialog.Close> 164 - {/if} 165 - </Dialog.Content> 166 - </Dialog.Portal> 167 - </Dialog.Root>
-2
src/lib/website/foxui/modal/index.ts
··· 1 - export { default as Modal } from './Modal.svelte'; 2 - export type { ModalProps } from './Modal.svelte';
-23
src/lib/website/foxui/navbar/Navbar.svelte
··· 1 - <script lang="ts"> 2 - import type { WithElementRef } from 'bits-ui'; 3 - import type { HTMLAttributes } from 'svelte/elements'; 4 - import { cn } from '..'; 5 - 6 - const { 7 - class: className, 8 - children, 9 - hasSidebar = false, 10 - ...restProps 11 - }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { hasSidebar?: boolean } = $props(); 12 - </script> 13 - 14 - <div 15 - class={cn( 16 - 'border-base-400/30 dark:border-base-300/10 fixed left-0 right-0 top-1 z-50 mx-1 flex h-16 items-center justify-between overflow-hidden rounded-2xl border p-2 shadow-2xl', 17 - hasSidebar ? 'lg:left-74' : '', 18 - className 19 - )} 20 - {...restProps} 21 - > 22 - {@render children?.()} 23 - </div>
-1
src/lib/website/foxui/navbar/index.ts
··· 1 - export { default as Navbar } from './Navbar.svelte';
-73
src/lib/website/foxui/sonner/Toaster.svelte
··· 1 - <script lang="ts"> 2 - import { Toaster as SonnerToaster } from 'svelte-sonner'; 3 - 4 - const colorClasses = { 5 - blue: 'bg-blue-200/50 dark:bg-blue-950/50 text-blue-600 [&_.title]:text-blue-800 [&_.description]:text-blue-700 dark:text-blue-500 border-blue-700/20 dark:border-blue-500/20 dark:[&_.title]:text-blue-300 dark:[&_.description]:text-blue-400', 6 - red: 'bg-red-200/50 dark:bg-red-950/50 text-red-600 [&_.title]:text-red-800 [&_.description]:text-red-700 dark:text-red-500 border-red-700/20 dark:border-red-500/20 dark:[&_.title]:text-red-300 dark:[&_.description]:text-red-400', 7 - yellow: 8 - 'bg-yellow-200/50 dark:bg-yellow-950/50 text-yellow-600 [&_.title]:text-yellow-800 [&_.description]:text-yellow-700 dark:text-yellow-500 border-yellow-700/20 dark:border-yellow-500/20 dark:[&_.title]:text-yellow-300 dark:[&_.description]:text-yellow-400', 9 - green: 10 - 'bg-green-200/50 dark:bg-green-950/50 text-green-600 [&_.title]:text-green-800 [&_.description]:text-green-700 dark:text-green-500 border-green-700/20 dark:border-green-500/20 dark:[&_.title]:text-green-300 dark:[&_.description]:text-green-400', 11 - indigo: 12 - 'bg-indigo-200/50 dark:bg-indigo-950/50 text-indigo-600 [&_.title]:text-indigo-800 [&_.description]:text-indigo-700 dark:text-indigo-500 border-indigo-700/20 dark:border-indigo-500/20 dark:[&_.title]:text-indigo-300 dark:[&_.description]:text-indigo-400', 13 - purple: 14 - 'bg-purple-200/50 dark:bg-purple-950/50 text-purple-600 [&_.title]:text-purple-800 [&_.description]:text-purple-700 dark:text-purple-500 border-purple-700/20 dark:border-purple-500/20 dark:[&_.title]:text-purple-300 dark:[&_.description]:text-purple-400', 15 - pink: 'bg-pink-200/50 dark:bg-pink-950/50 text-pink-600 [&_.title]:text-pink-800 [&_.description]:text-pink-700 dark:text-pink-500 border-pink-700/20 dark:border-pink-500/20 dark:[&_.title]:text-pink-300 dark:[&_.description]:text-pink-400', 16 - orange: 17 - 'bg-orange-200/50 dark:bg-orange-950/50 text-orange-600 [&_.title]:text-orange-800 [&_.description]:text-orange-700 dark:text-orange-500 border-orange-700/20 dark:border-orange-500/20 dark:[&_.title]:text-orange-300 dark:[&_.description]:text-orange-400', 18 - teal: 'bg-teal-200/50 dark:bg-teal-950/50 text-teal-600 [&_.title]:text-teal-800 [&_.description]:text-teal-700 dark:text-teal-500 border-teal-700/20 dark:border-teal-500/20 dark:[&_.title]:text-teal-300 dark:[&_.description]:text-teal-400', 19 - emerald: 20 - 'bg-emerald-200/50 dark:bg-emerald-950/50 text-emerald-600 [&_.title]:text-emerald-800 [&_.description]:text-emerald-700 dark:text-emerald-500 border-emerald-700/20 dark:border-emerald-500/20 dark:[&_.title]:text-emerald-300 dark:[&_.description]:text-emerald-400', 21 - lime: 'bg-lime-200/50 dark:bg-lime-950/50 text-lime-600 [&_.title]:text-lime-800 [&_.description]:text-lime-700 dark:text-lime-500 border-lime-700/20 dark:border-lime-500/20 dark:[&_.title]:text-lime-300 dark:[&_.description]:text-lime-400', 22 - cyan: 'bg-cyan-200/50 dark:bg-cyan-950/50 text-cyan-600 [&_.title]:text-cyan-800 [&_.description]:text-cyan-700 dark:text-cyan-500 border-cyan-700/20 dark:border-cyan-500/20 dark:[&_.title]:text-cyan-300 dark:[&_.description]:text-cyan-400', 23 - sky: 'bg-sky-200/50 dark:bg-sky-950/50 text-sky-600 [&_.title]:text-sky-800 [&_.description]:text-sky-700 dark:text-sky-500 border-sky-700/20 dark:border-sky-500/20 dark:[&_.title]:text-sky-300 dark:[&_.description]:text-sky-400', 24 - rose: 'bg-rose-200/50 dark:bg-rose-950/50 text-rose-600 [&_.title]:text-rose-800 [&_.description]:text-rose-700 dark:text-rose-500 border-rose-700/20 dark:border-rose-500/20 dark:[&_.title]:text-rose-300 dark:[&_.description]:text-rose-400', 25 - amber: 26 - 'bg-amber-200/50 dark:bg-amber-950/50 text-amber-600 [&_.title]:text-amber-800 [&_.description]:text-amber-700 dark:text-amber-500 border-amber-700/20 dark:border-amber-500/20 dark:[&_.title]:text-amber-300 dark:[&_.description]:text-amber-400', 27 - violet: 28 - 'bg-violet-200/50 dark:bg-violet-950/50 text-violet-600 [&_.title]:text-violet-800 [&_.description]:text-violet-700 dark:text-violet-500 border-violet-700/20 dark:border-violet-500/20 dark:[&_.title]:text-violet-300 dark:[&_.description]:text-violet-400', 29 - fuchsia: 30 - 'bg-fuchsia-200/50 dark:bg-fuchsia-950/50 text-fuchsia-600 [&_.title]:text-fuchsia-800 [&_.description]:text-fuchsia-700 dark:text-fuchsia-500 border-fuchsia-700/20 dark:border-fuchsia-500/20 dark:[&_.title]:text-fuchsia-300 dark:[&_.description]:text-fuchsia-400', 31 - base: 'bg-base-50/80 border-base-200 dark:bg-base-900/50 dark:border-base-800 [&_.title]:text-base-900 dark:[&_.title]:text-base-50 [&_.description]:text-base-800 dark:[&_.description]:text-base-100', 32 - accent: 33 - 'bg-accent-200/50 dark:bg-accent-950/50 text-accent-600 [&_.title]:text-accent-800 [&_.description]:text-accent-700 dark:text-accent-500 border-accent-700/20 dark:border-accent-500/20 dark:[&_.title]:text-accent-300 dark:[&_.description]:text-accent-400' 34 - }; 35 - 36 - type Colors = keyof typeof colorClasses; 37 - 38 - const { 39 - colors = { 40 - default: 'accent', 41 - success: 'green', 42 - error: 'red', 43 - info: 'blue' 44 - } 45 - }: { 46 - colors?: { 47 - default?: Colors; 48 - success?: Colors; 49 - error?: Colors; 50 - info?: Colors; 51 - }; 52 - } = $props(); 53 - </script> 54 - 55 - <SonnerToaster 56 - toastOptions={{ 57 - unstyled: true, 58 - classes: { 59 - toast: 60 - 'group toast min-w-12 w-fit sm:min-w-64 backdrop-blur-lg border rounded-2xl p-4 flex items-center gap-2 sm:fixed sm:bottom-4 sm:right-0 mx-2', 61 - title: 'text-base title', 62 - description: 'text-xs mt-1 description', 63 - 64 - default: colorClasses[colors?.default ?? 'accent'], 65 - loading: colorClasses[colors?.default ?? 'accent'], 66 - 67 - success: colorClasses[colors?.success ?? 'green'], 68 - error: colorClasses[colors?.error ?? 'red'], 69 - info: colorClasses[colors?.info ?? 'blue'] 70 - } 71 - }} 72 - position="bottom-right" 73 - />
-2
src/lib/website/foxui/sonner/index.ts
··· 1 - export { default as Toaster } from './Toaster.svelte'; 2 - export { toast } from 'svelte-sonner';
-6
src/lib/website/foxui/utils.ts
··· 1 - import { type ClassValue, clsx } from 'clsx'; 2 - import { twMerge } from 'tailwind-merge'; 3 - 4 - export function cn(...inputs: ClassValue[]) { 5 - return twMerge(clsx(inputs)); 6 - }
+21 -8
src/lib/website/utils.ts
··· 1 - import { type Collection, type DownloadedData } from './types'; 1 + import { 2 + type Collection, 3 + type DownloadedData, 4 + type IndividualCollections, 5 + type ListCollections 6 + } from './types'; 2 7 import { getRecord, listRecords, resolveHandle } from '$lib/oauth/atproto'; 3 8 import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 4 9 import { data } from './data'; 5 10 6 11 export function parseUri(uri: string) { 7 - // at://did:plc:257wekqxg4hyapkq6k47igmp/link.flo-bit.dev/3lnblfznvhr2a 8 12 const [did, collection, rkey] = uri.split('/').slice(2); 9 13 return { did, collection, rkey } as { 10 14 collection: `${string}.${string}.${string}`; ··· 18 22 19 23 const downloadedData = {} as DownloadedData; 20 24 21 - const promises: { collection: string; rkey?: string; record: ListRecord }[] = []; 25 + const promises: { 26 + collection: string; 27 + rkey?: string; 28 + record: Promise<ListRecord> | Promise<Record<string, ListRecord>>; 29 + }[] = []; 22 30 23 31 for (const collection of Object.keys(data) as Collection[]) { 24 32 const cfg = data[collection]; ··· 46 54 47 55 for (const promise of promises) { 48 56 if (promise.rkey) { 49 - downloadedData[promise.collection] ??= {} as Record<string, ListRecord>; 50 - downloadedData[promise.collection][promise.rkey] = await promise.record; 57 + downloadedData[promise.collection as IndividualCollections] ??= {} as Record< 58 + string, 59 + ListRecord 60 + >; 61 + downloadedData[promise.collection as IndividualCollections][promise.rkey] = 62 + (await promise.record) as ListRecord; 51 63 } else { 52 - downloadedData[promise.collection] ??= await promise.record; 64 + downloadedData[promise.collection as ListCollections] ??= (await promise.record) as Record< 65 + string, 66 + ListRecord 67 + >; 53 68 } 54 69 } 55 - 56 - console.log(downloadedData); 57 70 58 71 return { did, data: JSON.parse(JSON.stringify(downloadedData)) as DownloadedData }; 59 72 }
+2 -7
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 + import '../app.css'; 3 + 2 4 import { ThemeToggle } from '@foxui/core'; 3 - 4 - import '../app.css'; 5 5 import { onMount } from 'svelte'; 6 6 import { initClient } from '$lib/oauth'; 7 - import { gsap } from 'gsap'; 8 - 9 - import { Flip } from 'gsap/Flip'; 10 - 11 - gsap.registerPlugin(Flip); 12 7 13 8 let { children } = $props(); 14 9
+7
src/routes/+page.server.ts
··· 1 + import { loadData } from '$lib/website/utils'; 2 + 3 + export async function load() { 4 + const mainHandle = 'flo-bit.dev'; 5 + const data = await loadData(mainHandle); 6 + return { ...data, handle: mainHandle }; 7 + }
+11 -9
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from "svelte"; 2 + import { page } from '$app/state'; 3 + import { type Item } from '$lib/types.js'; 4 + import Website from '$lib/Website.svelte'; 3 5 4 - onMount(async () => { 5 - const data = await fetch('/api/links?link=https://bento.me/flo-bit'); 6 - const json = await data.json(); 7 - console.log(json); 8 - }) 6 + let { data } = $props(); 7 + $inspect(data); 9 8 </script> 10 9 11 - <div class="flex h-screen items-center justify-center"> 12 - <h1 class="text-7xl font-bold">blento</h1> 13 - </div> 10 + <Website 11 + {data} 12 + handle={data.handle} 13 + did={data.did} 14 + items={Object.values(data.data['com.example.bento']).map((i) => i.value) as Item[]} 15 + />
+7 -8
src/routes/[handle]/+page.svelte
··· 2 2 import { page } from '$app/state'; 3 3 import { type Item } from '$lib/types.js'; 4 4 import Website from '$lib/Website.svelte'; 5 - import WebsiteWrapper from '$lib/website/WebsiteWrapper.svelte'; 6 5 7 6 let { data } = $props(); 7 + $inspect(data); 8 8 </script> 9 9 10 - <WebsiteWrapper {data} handle={page.params.handle}> 11 - <Website 12 - handle={page.params.handle} 13 - did={data.did} 14 - items={Object.values(data.data['com.example.bento']).map((i) => i.value) as Item[]} 15 - /> 16 - </WebsiteWrapper> 10 + <Website 11 + {data} 12 + handle={page.params.handle} 13 + did={data.did} 14 + items={Object.values(data.data['com.example.bento']).map((i) => i.value) as Item[]} 15 + />
+7
src/routes/edit/+page.server.ts
··· 1 + import { loadData } from '$lib/website/utils'; 2 + 3 + export async function load() { 4 + const mainHandle = 'flo-bit.dev'; 5 + const data = await loadData(mainHandle); 6 + return { ...data, handle: mainHandle }; 7 + }
+14
src/routes/edit/+page.svelte
··· 1 + <script lang="ts"> 2 + import { page } from '$app/state'; 3 + import EditableWebsite from '$lib/EditableWebsite.svelte'; 4 + import { type Item } from '$lib/types.js'; 5 + 6 + let { data } = $props(); 7 + </script> 8 + 9 + <EditableWebsite 10 + handle={data.handle} 11 + did={data.did} 12 + {data} 13 + items={Object.values(data.data['com.example.bento']).map((i) => i.value) as Item[]} 14 + />