[READ-ONLY] Mirror of https://github.com/flo-bit/ui-kit. 🦊 fox ui, svelte 5 and tailwind 4 flo-bit.dev/ui-kit/
svelte tailwindcss ui-components
0

Configure Feed

Select the types of activity you want to include in your feed.

fixes, add social package

Florian (May 3, 2025, 12:40 AM +0200) 00234673 12f90254

+3131 -277
+1
package.json
··· 4 4 "scripts": { 5 5 "build": "turbo run build", 6 6 "dev": "turbo run dev", 7 + "dev:docs": "turbo run dev --filter=docs", 7 8 "lint": "turbo run lint", 8 9 "format": "prettier --write .", 9 10 "check-types": "turbo run check-types",
+199 -2
pnpm-lock.yaml
··· 57 57 '@fuxui/colors': 58 58 specifier: workspace:* 59 59 version: link:../../packages/colors 60 + '@fuxui/social': 61 + specifier: workspace:* 62 + version: link:../../packages/social 60 63 '@fuxui/time': 61 64 specifier: workspace:* 62 65 version: link:../../packages/time ··· 391 394 specifier: ^1.4.1 392 395 version: 1.4.1(vite@6.3.2) 393 396 397 + packages/social: 398 + dependencies: 399 + '@atproto/api': 400 + specifier: ^0.15.5 401 + version: 0.15.5 402 + '@fuxui/base': 403 + specifier: workspace:* 404 + version: link:../base 405 + '@use-gesture/vanilla': 406 + specifier: ^10.3.1 407 + version: 10.3.1 408 + bits-ui: 409 + specifier: ^1.4.3 410 + version: 1.4.3(svelte@5.28.1) 411 + hls.js: 412 + specifier: ^1.6.2 413 + version: 1.6.2 414 + plyr: 415 + specifier: ^3.7.8 416 + version: 3.7.8 417 + devDependencies: 418 + '@eslint/compat': 419 + specifier: ^1.2.5 420 + version: 1.2.5(eslint@9.25.0) 421 + '@eslint/js': 422 + specifier: ^9.18.0 423 + version: 9.25.0 424 + '@sveltejs/adapter-auto': 425 + specifier: ^6.0.0 426 + version: 6.0.0(@sveltejs/kit@2.20.0) 427 + '@sveltejs/adapter-static': 428 + specifier: ^3.0.8 429 + version: 3.0.8(@sveltejs/kit@2.20.0) 430 + '@sveltejs/kit': 431 + specifier: ^2.16.0 432 + version: 2.20.0(@sveltejs/vite-plugin-svelte@5.0.3)(svelte@5.28.1)(vite@6.3.2) 433 + '@sveltejs/package': 434 + specifier: ^2.3.11 435 + version: 2.3.11(svelte@5.28.1)(typescript@5.8.2) 436 + '@sveltejs/vite-plugin-svelte': 437 + specifier: ^5.0.0 438 + version: 5.0.3(svelte@5.28.1)(vite@6.3.2) 439 + '@tailwindcss/forms': 440 + specifier: ^0.5.9 441 + version: 0.5.9(tailwindcss@4.1.5) 442 + '@tailwindcss/typography': 443 + specifier: ^0.5.15 444 + version: 0.5.15(tailwindcss@4.1.5) 445 + '@tailwindcss/vite': 446 + specifier: ^4.1.5 447 + version: 4.1.5(vite@6.3.2) 448 + eslint: 449 + specifier: ^9.18.0 450 + version: 9.25.0 451 + eslint-config-prettier: 452 + specifier: ^10.0.1 453 + version: 10.1.1(eslint@9.25.0) 454 + eslint-plugin-svelte: 455 + specifier: ^3.0.0 456 + version: 3.0.0(eslint@9.25.0)(svelte@5.28.1) 457 + globals: 458 + specifier: ^16.0.0 459 + version: 16.0.0 460 + prettier: 461 + specifier: ^3.4.2 462 + version: 3.5.2 463 + prettier-plugin-svelte: 464 + specifier: ^3.3.3 465 + version: 3.3.3(prettier@3.5.2)(svelte@5.28.1) 466 + prettier-plugin-tailwindcss: 467 + specifier: ^0.6.11 468 + version: 0.6.11(prettier-plugin-svelte@3.3.3)(prettier@3.5.2) 469 + svelte: 470 + specifier: ^5.0.0 471 + version: 5.28.1 472 + svelte-check: 473 + specifier: ^4.0.0 474 + version: 4.1.4(svelte@5.28.1)(typescript@5.8.2) 475 + tailwindcss: 476 + specifier: ^4.1.5 477 + version: 4.1.5 478 + typescript: 479 + specifier: ^5.0.0 480 + version: 5.8.2 481 + typescript-eslint: 482 + specifier: ^8.20.0 483 + version: 8.30.1(eslint@9.25.0)(typescript@5.8.2) 484 + vite: 485 + specifier: ^6.2.6 486 + version: 6.3.2 487 + 394 488 packages/time: 395 489 dependencies: 396 490 '@fuxui/base': ··· 563 657 dependencies: 564 658 '@jridgewell/gen-mapping': 0.3.8 565 659 '@jridgewell/trace-mapping': 0.3.25 660 + 661 + /@atproto/api@0.15.5: 662 + resolution: {integrity: sha512-GiKOrjSXMm8OSpc+pfjFTBYQGX62jmorECkTx2VZbS6KtFKFY0cRQAI+JnQoOLF/8TvzpaAZB7+it73uIqDM7A==, tarball: https://registry.npmjs.org/@atproto/api/-/api-0.15.5.tgz} 663 + dependencies: 664 + '@atproto/common-web': 0.4.1 665 + '@atproto/lexicon': 0.4.10 666 + '@atproto/syntax': 0.4.0 667 + '@atproto/xrpc': 0.6.12 668 + await-lock: 2.2.2 669 + multiformats: 9.9.0 670 + tlds: 1.258.0 671 + zod: 3.24.3 672 + dev: false 673 + 674 + /@atproto/common-web@0.4.1: 675 + resolution: {integrity: sha512-Ghh+djHYMAUCktLKwr2IuGgtjcwSWGudp+K7+N7KBA9pDDloOXUEY8Agjc5SHSo9B1QIEFkegClU5n+apn2e0w==, tarball: https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.1.tgz} 676 + dependencies: 677 + graphemer: 1.4.0 678 + multiformats: 9.9.0 679 + uint8arrays: 3.0.0 680 + zod: 3.24.3 681 + dev: false 682 + 683 + /@atproto/lexicon@0.4.10: 684 + resolution: {integrity: sha512-uDbP20vetBgtXPuxoyRcvOGBt2gNe1dFc9yYKcb6jWmXfseHiGTnIlORJOLBXIT2Pz15Eap4fLxAu6zFAykD5A==, tarball: https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.10.tgz} 685 + dependencies: 686 + '@atproto/common-web': 0.4.1 687 + '@atproto/syntax': 0.4.0 688 + iso-datestring-validator: 2.2.2 689 + multiformats: 9.9.0 690 + zod: 3.24.3 691 + dev: false 692 + 693 + /@atproto/syntax@0.4.0: 694 + resolution: {integrity: sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA==, tarball: https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.0.tgz} 695 + dev: false 696 + 697 + /@atproto/xrpc@0.6.12: 698 + resolution: {integrity: sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w==, tarball: https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.12.tgz} 699 + dependencies: 700 + '@atproto/lexicon': 0.4.10 701 + zod: 3.24.3 702 + dev: false 566 703 567 704 /@babel/runtime@7.27.1: 568 705 resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==, tarball: https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz} ··· 1841 1978 engines: {node: '>=8'} 1842 1979 dev: true 1843 1980 1981 + /await-lock@2.2.2: 1982 + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==, tarball: https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz} 1983 + dev: false 1984 + 1844 1985 /axobject-query@4.1.0: 1845 1986 resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==, tarball: https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz} 1846 1987 engines: {node: '>= 0.4'} ··· 1990 2131 resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==, tarball: https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz} 1991 2132 engines: {node: '>= 0.6'} 1992 2133 2134 + /core-js@3.42.0: 2135 + resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==, tarball: https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz} 2136 + dev: false 2137 + 1993 2138 /cross-spawn@7.0.6: 1994 2139 resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 1995 2140 engines: {node: '>= 8'} ··· 2018 2163 resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 2019 2164 engines: {node: '>=4'} 2020 2165 dev: true 2166 + 2167 + /custom-event-polyfill@1.0.7: 2168 + resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==, tarball: https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz} 2169 + dev: false 2021 2170 2022 2171 /dataloader@1.4.0: 2023 2172 resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==, tarball: https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz} ··· 2501 2650 2502 2651 /graphemer@1.4.0: 2503 2652 resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 2504 - dev: true 2505 2653 2506 2654 /has-flag@4.0.0: 2507 2655 resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 2508 2656 engines: {node: '>=8'} 2509 2657 dev: true 2658 + 2659 + /hls.js@1.6.2: 2660 + resolution: {integrity: sha512-rx+pETSCJEDThm/JCm8CuadcAC410cVjb1XVXFNDKFuylaayHk1+tFxhkjvnMDAfqsJHxZXDAJ3Uc2d5xQyWlQ==, tarball: https://registry.npmjs.org/hls.js/-/hls.js-1.6.2.tgz} 2661 + dev: false 2510 2662 2511 2663 /htmlparser2@9.1.0: 2512 2664 resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==, tarball: https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz} ··· 2598 2750 /isexe@2.0.0: 2599 2751 resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 2600 2752 dev: true 2753 + 2754 + /iso-datestring-validator@2.2.2: 2755 + resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==, tarball: https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz} 2756 + dev: false 2601 2757 2602 2758 /jiti@2.4.2: 2603 2759 resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==, tarball: https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz} ··· 2761 2917 engines: {node: '>=10'} 2762 2918 dev: true 2763 2919 2920 + /loadjs@4.3.0: 2921 + resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==, tarball: https://registry.npmjs.org/loadjs/-/loadjs-4.3.0.tgz} 2922 + dev: false 2923 + 2764 2924 /locate-character@3.0.0: 2765 2925 resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} 2766 2926 ··· 2858 3018 dev: true 2859 3019 2860 3020 /minimatch@9.0.5: 2861 - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 3021 + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz} 2862 3022 engines: {node: '>=16 || 14 >=14.17'} 2863 3023 dependencies: 2864 3024 brace-expansion: 2.0.1 ··· 2878 3038 2879 3039 /ms@2.1.3: 2880 3040 resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 3041 + 3042 + /multiformats@9.9.0: 3043 + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==, tarball: https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz} 3044 + dev: false 2881 3045 2882 3046 /nanoid@3.3.8: 2883 3047 resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} ··· 3055 3219 engines: {node: '>=6'} 3056 3220 dev: true 3057 3221 3222 + /plyr@3.7.8: 3223 + resolution: {integrity: sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA==, tarball: https://registry.npmjs.org/plyr/-/plyr-3.7.8.tgz} 3224 + dependencies: 3225 + core-js: 3.42.0 3226 + custom-event-polyfill: 1.0.7 3227 + loadjs: 4.3.0 3228 + rangetouch: 2.0.1 3229 + url-polyfill: 1.1.13 3230 + dev: false 3231 + 3058 3232 /postcss-load-config@3.1.4(postcss@8.5.3): 3059 3233 resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} 3060 3234 engines: {node: '>= 10'} ··· 3220 3394 /queue-microtask@1.2.3: 3221 3395 resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 3222 3396 dev: true 3397 + 3398 + /rangetouch@2.0.1: 3399 + resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==, tarball: https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz} 3400 + dev: false 3223 3401 3224 3402 /read-yaml-file@1.1.0: 3225 3403 resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==, tarball: https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz} ··· 3550 3728 fdir: 6.4.4(picomatch@4.0.2) 3551 3729 picomatch: 4.0.2 3552 3730 3731 + /tlds@1.258.0: 3732 + resolution: {integrity: sha512-XGhStWuOlBA5D8QnyN2xtgB2cUOdJ3ztisne1DYVWMcVH29qh8eQIpRmP3HnuJLdgyzG0HpdGzRMu1lm/Oictw==, tarball: https://registry.npmjs.org/tlds/-/tlds-1.258.0.tgz} 3733 + hasBin: true 3734 + dev: false 3735 + 3553 3736 /tmp@0.0.33: 3554 3737 resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==, tarball: https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz} 3555 3738 engines: {node: '>=0.6.0'} ··· 3694 3877 engines: {node: '>=14.17'} 3695 3878 dev: true 3696 3879 3880 + /uint8arrays@3.0.0: 3881 + resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==, tarball: https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz} 3882 + dependencies: 3883 + multiformats: 9.9.0 3884 + dev: false 3885 + 3697 3886 /undici@6.21.2: 3698 3887 resolution: {integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==, tarball: https://registry.npmjs.org/undici/-/undici-6.21.2.tgz} 3699 3888 engines: {node: '>=18.17'} ··· 3734 3923 dependencies: 3735 3924 punycode: 2.3.1 3736 3925 dev: true 3926 + 3927 + /url-polyfill@1.1.13: 3928 + resolution: {integrity: sha512-tXzkojrv2SujumYthZ/WjF7jaSfNhSXlYMpE5AYdL2I3D7DCeo+mch8KtW2rUuKjDg+3VXODXHVgipt8yGY/eQ==, tarball: https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.13.tgz} 3929 + dev: false 3737 3930 3738 3931 /util-deprecate@1.0.2: 3739 3932 resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} ··· 3867 4060 3868 4061 /zimmerframe@1.1.2: 3869 4062 resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} 4063 + 4064 + /zod@3.24.3: 4065 + resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==, tarball: https://registry.npmjs.org/zod/-/zod-3.24.3.tgz} 4066 + dev: false 3870 4067 3871 4068 github.com/jerzakm/three-perf/322d7d38a17069f2b6e9734913edad0fc4a29169(three@0.176.0): 3872 4069 resolution: {tarball: https://codeload.github.com/jerzakm/three-perf/tar.gz/322d7d38a17069f2b6e9734913edad0fc4a29169}
+1
apps/docs/package.json
··· 21 21 "@fuxui/3d": "workspace:*", 22 22 "@fuxui/time": "workspace:*", 23 23 "@fuxui/visual": "workspace:*", 24 + "@fuxui/social": "workspace:*", 24 25 "@sveltejs/adapter-auto": "^6.0.0", 25 26 "@sveltejs/kit": "^2.16.0", 26 27 "@sveltejs/vite-plugin-svelte": "^5.0.0",
+23
packages/social/.gitignore
··· 1 + node_modules 2 + 3 + # Output 4 + .output 5 + .vercel 6 + .netlify 7 + .wrangler 8 + /.svelte-kit 9 + /build 10 + 11 + # OS 12 + .DS_Store 13 + Thumbs.db 14 + 15 + # Env 16 + .env 17 + .env.* 18 + !.env.example 19 + !.env.test 20 + 21 + # Vite 22 + vite.config.js.timestamp-* 23 + vite.config.ts.timestamp-*
+1
packages/social/.npmrc
··· 1 + engine-strict=true
+6
packages/social/.prettierignore
··· 1 + # Package Managers 2 + package-lock.json 3 + pnpm-lock.yaml 4 + yarn.lock 5 + bun.lock 6 + bun.lockb
+15
packages/social/.prettierrc
··· 1 + { 2 + "useTabs": true, 3 + "singleQuote": true, 4 + "trailingComma": "none", 5 + "printWidth": 100, 6 + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 + "overrides": [ 8 + { 9 + "files": "*.svelte", 10 + "options": { 11 + "parser": "svelte" 12 + } 13 + } 14 + ] 15 + }
+80
packages/social/README.md
··· 1 + # 🦊 fox ui 2 + 3 + > **This is a public alpha release. Expect bugs and breaking changes.** 4 + 5 + svelte v5 + tailwind v4 6 + 7 + [See all components here](https://flo-bit.dev/ui-kit) 8 + 9 + Read more about [the philosophy/aim of this project here](https://flo-bit.dev/ui-kit/docs/philosophy). 10 + 11 + ## Quickstart 12 + 13 + ### 1. Create a new svelte project with tailwind css (including `@tailwindcss/typography` and `@tailwindcss/forms` plugins) 14 + 15 + ```bash 16 + npx sv create my-project 17 + ``` 18 + 19 + ### 2. Install fuchs-ui 20 + 21 + ```bash 22 + npm install @fuxui/base 23 + ``` 24 + 25 + ### 3. set theme variables in your app.css (changing `zinc` and `emerald` to your preferred colors, using find and replace). 26 + 27 + ```css 28 + @source "../node_modules/@fuxui"; 29 + 30 + @theme { 31 + --color-base-50: var(--color-zinc-50); 32 + --color-base-100: var(--color-zinc-100); 33 + --color-base-200: var(--color-zinc-200); 34 + --color-base-300: var(--color-zinc-300); 35 + --color-base-400: var(--color-zinc-400); 36 + --color-base-500: var(--color-zinc-500); 37 + --color-base-600: var(--color-zinc-600); 38 + --color-base-700: var(--color-zinc-700); 39 + --color-base-800: var(--color-zinc-800); 40 + --color-base-900: var(--color-zinc-900); 41 + --color-base-950: var(--color-zinc-950); 42 + 43 + --color-accent-50: var(--color-emerald-50); 44 + --color-accent-100: var(--color-emerald-100); 45 + --color-accent-200: var(--color-emerald-200); 46 + --color-accent-300: var(--color-emerald-300); 47 + --color-accent-400: var(--color-emerald-400); 48 + --color-accent-500: var(--color-emerald-500); 49 + --color-accent-600: var(--color-emerald-600); 50 + --color-accent-700: var(--color-emerald-700); 51 + --color-accent-800: var(--color-emerald-800); 52 + --color-accent-900: var(--color-emerald-900); 53 + --color-accent-950: var(--color-emerald-950); 54 + } 55 + ``` 56 + 57 + ### 4. Use the components 58 + 59 + ```svelte 60 + <script> 61 + import { Button } from '@fuxui/base'; 62 + </script> 63 + 64 + <Button onclick={() => alert('clicked')}>Click me</Button> 65 + ``` 66 + 67 + ## Development 68 + 69 + If you want to contribute to the project, please open an issue first describing the feature you want to add. 70 + 71 + Clone the repo, install dependencies and run the dev server 72 + 73 + ```bash 74 + git clone https://github.com/flo-bit/ui-kit.git 75 + cd ui-kit 76 + pnpm install 77 + pnpm run dev 78 + ``` 79 + 80 + The ui-kit library is located in `packages/` (`packages/base` for the base package), the documentation is in `apps/docs`.
+36
packages/social/eslint.config.js
··· 1 + import prettier from 'eslint-config-prettier'; 2 + import js from '@eslint/js'; 3 + import { includeIgnoreFile } from '@eslint/compat'; 4 + import svelte from 'eslint-plugin-svelte'; 5 + import globals from 'globals'; 6 + import { fileURLToPath } from 'node:url'; 7 + import ts from 'typescript-eslint'; 8 + import svelteConfig from './svelte.config.js'; 9 + 10 + const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 11 + 12 + export default ts.config( 13 + includeIgnoreFile(gitignorePath), 14 + js.configs.recommended, 15 + ...ts.configs.recommended, 16 + ...svelte.configs.recommended, 17 + prettier, 18 + ...svelte.configs.prettier, 19 + { 20 + languageOptions: { 21 + globals: { ...globals.browser, ...globals.node } 22 + }, 23 + rules: { 'no-undef': 'off' } 24 + }, 25 + { 26 + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 27 + languageOptions: { 28 + parserOptions: { 29 + projectService: true, 30 + extraFileExtensions: ['.svelte'], 31 + parser: ts.parser, 32 + svelteConfig 33 + } 34 + } 35 + } 36 + );
+84
packages/social/package.json
··· 1 + { 2 + "name": "@fuxui/social", 3 + "private": false, 4 + "version": "0.0.1", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite dev", 8 + "build": "vite build && npm run prepack", 9 + "build:package": "vite build && npm run prepack", 10 + "preview": "vite preview", 11 + "prepare": "svelte-kit sync || echo ''", 12 + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 13 + "prepack": "svelte-kit sync && svelte-package && node ./scripts/cleanup.js", 14 + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 15 + "format": "prettier --write .", 16 + "lint": "prettier --check . && eslint ." 17 + }, 18 + "files": [ 19 + "dist", 20 + "!dist/**/*.test.*", 21 + "!dist/**/*.spec.*" 22 + ], 23 + "sideEffects": [ 24 + "**/*.css" 25 + ], 26 + "exports": { 27 + ".": { 28 + "types": "./src/lib/types.d.ts", 29 + "svelte": "./src/lib/index.ts" 30 + } 31 + }, 32 + "types": "./src/lib/types.d.ts", 33 + "svelte": "./src/lib/index.ts", 34 + "publishOverrides": { 35 + "files": [ 36 + "dist" 37 + ], 38 + "svelte": "./dist/index.js", 39 + "exports": { 40 + ".": { 41 + "types": "./dist/types.d.ts", 42 + "svelte": "./dist/index.js" 43 + } 44 + }, 45 + "types": "./dist/types.d.ts" 46 + }, 47 + "devDependencies": { 48 + "@eslint/compat": "^1.2.5", 49 + "@eslint/js": "^9.18.0", 50 + "@sveltejs/adapter-auto": "^6.0.0", 51 + "@sveltejs/adapter-static": "^3.0.8", 52 + "@sveltejs/kit": "^2.16.0", 53 + "@sveltejs/package": "^2.3.11", 54 + "@sveltejs/vite-plugin-svelte": "^5.0.0", 55 + "@tailwindcss/forms": "^0.5.9", 56 + "@tailwindcss/typography": "^0.5.15", 57 + "@tailwindcss/vite": "^4.1.5", 58 + "eslint": "^9.18.0", 59 + "eslint-config-prettier": "^10.0.1", 60 + "eslint-plugin-svelte": "^3.0.0", 61 + "globals": "^16.0.0", 62 + "prettier": "^3.4.2", 63 + "prettier-plugin-svelte": "^3.3.3", 64 + "prettier-plugin-tailwindcss": "^0.6.11", 65 + "svelte": "^5.0.0", 66 + "svelte-check": "^4.0.0", 67 + "tailwindcss": "^4.1.5", 68 + "typescript": "^5.0.0", 69 + "typescript-eslint": "^8.20.0", 70 + "vite": "^6.2.6" 71 + }, 72 + "dependencies": { 73 + "@atproto/api": "^0.15.5", 74 + "@fuxui/base": "workspace:*", 75 + "@use-gesture/vanilla": "^10.3.1", 76 + "bits-ui": "^1.4.3", 77 + "hls.js": "^1.6.2", 78 + "plyr": "^3.7.8" 79 + }, 80 + "peerDependencies": { 81 + "svelte": ">=5", 82 + "tailwindcss": ">=3" 83 + } 84 + }
+18
packages/social/svelte.config.js
··· 1 + import adapter from '@sveltejs/adapter-static'; 2 + import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 + 4 + /** @type {import('@sveltejs/kit').Config} */ 5 + const config = { 6 + // Consult https://svelte.dev/docs/kit/integrations 7 + // for more information about preprocessors 8 + preprocess: vitePreprocess(), 9 + 10 + kit: { 11 + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 + // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 + adapter: adapter() 15 + } 16 + }; 17 + 18 + export default config;
+19
packages/social/tsconfig.json
··· 1 + { 2 + "extends": "./.svelte-kit/tsconfig.json", 3 + "compilerOptions": { 4 + "allowJs": true, 5 + "checkJs": true, 6 + "esModuleInterop": true, 7 + "forceConsistentCasingInFileNames": true, 8 + "resolveJsonModule": true, 9 + "skipLibCheck": true, 10 + "sourceMap": true, 11 + "strict": true, 12 + "moduleResolution": "bundler" 13 + } 14 + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 + // 17 + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 + // from the referenced tsconfig.json - TypeScript does not merge them in 19 + }
+7
packages/social/vite.config.ts
··· 1 + import tailwindcss from '@tailwindcss/vite'; 2 + import { sveltekit } from '@sveltejs/kit/vite'; 3 + import { defineConfig } from 'vite'; 4 + 5 + export default defineConfig({ 6 + plugins: [tailwindcss(), sveltekit()] 7 + });
apps/docs/static/opengraph.png

This is a binary file and will not be displayed.

+26
packages/social/scripts/cleanup.js
··· 1 + import { readFileSync, writeFileSync } from 'fs'; 2 + 3 + /** 4 + * This script utilizes a property called "publishOverrides" in the package.json 5 + * to override specific properties in the package.json when publishing to npm. 6 + */ 7 + const cleanPackage = () => { 8 + let packageJson = JSON.parse( 9 + readFileSync('./package.json', { 10 + encoding: 'utf-8' 11 + }) 12 + ); 13 + 14 + packageJson = { 15 + ...packageJson, 16 + ...packageJson.publishOverrides 17 + }; 18 + 19 + delete packageJson.publishOverrides; 20 + 21 + writeFileSync('./package.json', JSON.stringify(packageJson, null, 2), { 22 + encoding: 'utf-8' 23 + }); 24 + }; 25 + 26 + cleanPackage();
+29
packages/social/src/app.css
··· 1 + @import 'tailwindcss'; 2 + @plugin '@tailwindcss/forms'; 3 + @plugin '@tailwindcss/typography'; 4 + 5 + @theme { 6 + --color-base-50: var(--color-zinc-50); 7 + --color-base-100: var(--color-zinc-100); 8 + --color-base-200: var(--color-zinc-200); 9 + --color-base-300: var(--color-zinc-300); 10 + --color-base-400: var(--color-zinc-400); 11 + --color-base-500: var(--color-zinc-500); 12 + --color-base-600: var(--color-zinc-600); 13 + --color-base-700: var(--color-zinc-700); 14 + --color-base-800: var(--color-zinc-800); 15 + --color-base-900: var(--color-zinc-900); 16 + --color-base-950: var(--color-zinc-950); 17 + 18 + --color-accent-50: var(--color-pink-50); 19 + --color-accent-100: var(--color-pink-100); 20 + --color-accent-200: var(--color-pink-200); 21 + --color-accent-300: var(--color-pink-300); 22 + --color-accent-400: var(--color-pink-400); 23 + --color-accent-500: var(--color-pink-500); 24 + --color-accent-600: var(--color-pink-600); 25 + --color-accent-700: var(--color-pink-700); 26 + --color-accent-800: var(--color-pink-800); 27 + --color-accent-900: var(--color-pink-900); 28 + --color-accent-950: var(--color-pink-950); 29 + }
+13
packages/social/src/app.d.ts
··· 1 + // See https://svelte.dev/docs/kit/types#app.d.ts 2 + // for information about these interfaces 3 + declare global { 4 + namespace App { 5 + // interface Error {} 6 + // interface Locals {} 7 + // interface PageData {} 8 + // interface PageState {} 9 + // interface Platform {} 10 + } 11 + } 12 + 13 + export {};
+11
packages/social/src/app.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + %sveltekit.head% 7 + </head> 8 + <body data-sveltekit-preload-data="hover"> 9 + <div style="display: contents">%sveltekit.body%</div> 10 + </body> 11 + </html>
+8 -1
apps/docs/src/routes/+layout.svelte
··· 2 2 import { Head, Toaster } from '@fuxui/base'; 3 3 import '../app.css'; 4 4 import '../prism.css'; 5 + import { base } from '$app/paths'; 5 6 6 7 let { children } = $props(); 7 8 </script> 8 9 9 10 {@render children()} 10 11 11 - <Head title="fox ui" emojiFavicon="🦊" /> 12 + <Head 13 + title="fox ui" 14 + description="🦊 fox ui, svelte 5 & tailwind 4" 15 + image={base + '/opengraph.png'} 16 + url="https://flo-bit.dev/ui-kit" 17 + emojiFavicon="🦊" 18 + /> 12 19 13 20 <Toaster />
+1
packages/social/src/lib/index.ts
··· 1 + export * from './components';
+1
packages/social/src/lib/types.d.ts
··· 1 + export * from './index';
+7
packages/social/src/routes/+layout.svelte
··· 1 + <script lang="ts"> 2 + import '../app.css'; 3 + 4 + let { children } = $props(); 5 + </script> 6 + 7 + {@render children()}
+1
packages/social/src/routes/+layout.ts
··· 1 + export const prerender = true;
packages/social/src/routes/+page.svelte
+6
apps/docs/src/lib/site-components/components_all.ts
··· 1 1 import { threeDComponents } from './components_3d'; 2 2 import { type ComponentCard, baseComponents } from './components_base'; 3 3 import { colorsComponents } from './components_colors'; 4 + import { socialComponents } from './components_social'; 4 5 import { timeComponents } from './components_time'; 5 6 import { visualComponents } from './components_visual'; 6 7 ··· 18 19 name: 'Colors', 19 20 components: colorsComponents, 20 21 href: 'colors' 22 + }, 23 + { 24 + name: 'Social', 25 + components: socialComponents, 26 + href: 'social' 21 27 }, 22 28 { 23 29 name: '3D',
+34
apps/docs/src/lib/site-components/components_social.ts
··· 1 + import type { ComponentCard } from './components_base'; 2 + import CardSwiperCards from '$lib/cards/social/CardSwiperCards.svelte'; 3 + import CardUserProfile from '$lib/cards/social/CardUserProfile.svelte'; 4 + import CardGithubCorner from '$lib/cards/social/CardGithubCorner.svelte'; 5 + import CardBlueskyLogin from '$lib/cards/social/CardBlueskyLogin.svelte'; 6 + import CardStarRating from '$lib/cards/social/CardStarRating.svelte'; 7 + 8 + export const socialComponents: ComponentCard[] = [ 9 + { 10 + component: CardSwiperCards, 11 + label: 'Card Swiper', 12 + href: 'card-swiper' 13 + }, 14 + { 15 + component: CardUserProfile, 16 + label: 'User Profile', 17 + href: 'user-profile' 18 + }, 19 + { 20 + component: CardGithubCorner, 21 + label: 'Github Corner', 22 + href: 'github-corner' 23 + }, 24 + { 25 + component: CardBlueskyLogin, 26 + label: 'Bluesky Login', 27 + href: 'bluesky-login' 28 + }, 29 + { 30 + component: CardStarRating, 31 + label: 'Star Rating', 32 + href: 'star-rating' 33 + } 34 + ].sort((a, b) => a.label.localeCompare(b.label));
+6
apps/docs/src/lib/site-components/components_visual.ts
··· 3 3 import CardPhone from '$lib/cards/visual/CardPhone.svelte'; 4 4 import CardQuote from '$lib/cards/visual/CardQuote.svelte'; 5 5 import CardUndraw from '$lib/cards/visual/CardUndraw.svelte'; 6 + import CardImageMasonry from '$lib/cards/visual/CardImageMasonry.svelte'; 6 7 7 8 export const visualComponents: ComponentCard[] = [ 8 9 { ··· 24 25 component: CardUndraw, 25 26 label: 'Undraw', 26 27 href: 'undraw' 28 + }, 29 + { 30 + component: CardImageMasonry, 31 + label: 'Image Masonry', 32 + href: 'image-masonry' 27 33 } 28 34 ].sort((a, b) => a.label.localeCompare(b.label));
+32 -47
packages/base/src/lib/components/index.ts
··· 1 - export { Accordion, AccordionItem } from './accordion'; 2 - export { Alert, AlertDescription, AlertTitle } from './alert'; 3 - export { Avatar, AvatarGroup } from './avatar'; 4 - export { Badge } from './badge'; 5 - 6 - export { Button } from './button'; 7 - export { type ButtonProps } from './button/Button.svelte'; 8 - export { type ButtonSize, type ButtonVariant, buttonVariants } from './button/'; 9 - 10 - export { default as Box } from './box/Box.svelte'; 11 - export { default as ImageCard } from './card/ImageCard.svelte'; 12 - export { default as ChatBubble } from './chat-bubble/ChatBubble.svelte'; 13 - export { Checkbox } from './checkbox'; 14 - export { default as Head } from './head/Head.svelte'; 15 - export { default as Heading } from './heading/Heading.svelte'; 16 - export { default as Image } from './image/Image.svelte'; 17 - export { type ImageProps } from './image/Image.svelte'; 18 - export { default as ImageContainer } from './image-container/ImageContainer.svelte'; 19 - export { Input } from './input/'; 20 - export { Label } from './label/'; 21 - export { default as Modal } from './modal/Modal.svelte'; 22 - export { default as NumberInput } from './number-input/NumberInput.svelte'; 23 - export { default as Prose } from './prose/Prose.svelte'; 24 - export { default as ScrollArea } from './scroll-area/ScrollArea.svelte'; 25 - export { Slider } from './slider'; 26 - export { Select } from './select/'; 27 - export { default as SliderNumber } from './slider/SliderNumber.svelte'; 28 - export { Toaster } from './sonner/'; 29 - export { toast } from 'svelte-sonner'; 30 - export { Switch } from './switch/'; 31 - export { Paragraph } from './paragraph/'; 32 - export { Textarea } from './textarea'; 33 - export { Tooltip } from './tooltip'; 34 - export { default as Popover } from './popover/Popover.svelte'; 35 - export { default as Subheading } from './heading/Subheading.svelte'; 36 - 37 - export { default as AddCopyCodeButtons } from './copy-code-button/AddCopyCodeButtons.svelte'; 38 - 39 - export { Toggle } from './toggle'; 40 - export { ToggleGroup, ToggleGroupItem } from './toggle-group'; 41 - 42 - export { default as BlueskyLoginModal } from './modal/BlueskyLoginModal.svelte'; 43 - export { blueskyLoginModalState } from './modal/BlueskyLoginModal.svelte'; 44 - 45 - export { default as ThemeToggle } from './theme-toggle/ThemeToggle.svelte'; 46 - export { default as Navbar } from './navbar/Navbar.svelte'; 47 - export { default as Sidebar } from './sidebar/Sidebar.svelte'; 1 + export * from './accordion'; 2 + export * from './alert'; 3 + export * from './avatar'; 4 + export * from './badge'; 5 + export * from './box'; 6 + export * from './button'; 7 + export * from './card'; 8 + export * from './chat-bubble'; 9 + export * from './checkbox'; 10 + export * from './head'; 11 + export * from './heading'; 12 + export * from './image'; 13 + export * from './image-container'; 14 + export * from './input'; 15 + export * from './label'; 16 + export * from './modal'; 17 + export * from './navbar'; 18 + export * from './number-input'; 19 + export * from './paragraph'; 20 + export * from './popover'; 21 + export * from './prose'; 22 + export * from './scroll-area'; 23 + export * from './select'; 24 + export * from './sidebar'; 25 + export * from './slider'; 26 + export * from './sonner'; 27 + export * from './switch'; 28 + export * from './textarea'; 29 + export * from './theme-toggle'; 30 + export * from './toggle'; 31 + export * from './toggle-group'; 32 + export * from './tooltip';
+7
packages/social/src/lib/components/index.ts
··· 1 + export * from './bluesky-login'; 2 + export * from './post'; 3 + export * from './star-rating'; 4 + export * from './social-icons'; 5 + export * from './swiper-cards'; 6 + export * from './user-profile'; 7 + export * from './github-corner';
+1
packages/visual/src/lib/components/index.ts
··· 3 3 export * from './phone'; 4 4 export * from './quote'; 5 5 export * from './undraw'; 6 + export * from './image-masonry';
+38
apps/docs/src/lib/cards/social/CardBlueskyLogin.svelte
··· 1 + <script lang="ts"> 2 + import { Subheading, Label, Input, Button } from '@fuxui/base'; 3 + </script> 4 + 5 + <div class="flex items-start flex-col"> 6 + <Subheading class="mb-1 inline-flex items-center gap-2 text-xl font-bold"> 7 + <svg 8 + fill="currentColor" 9 + xmlns="http://www.w3.org/2000/svg" 10 + viewBox="-40 -40 680 620" 11 + version="1.1" 12 + class={['text-accent-600 dark:text-accent-400 size-6']} 13 + aria-hidden="true" 14 + > 15 + <path 16 + 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" 17 + /> 18 + </svg> 19 + Login with Bluesky</Subheading 20 + > 21 + 22 + <div class="mt-2 w-full"> 23 + <Label for="bluesky-handle" class="text-sm">Your handle</Label> 24 + <div class="mt-2"> 25 + <Input 26 + sizeVariant="sm" 27 + type="text" 28 + name="bluesky-handle" 29 + id="bluesky-handle" 30 + placeholder="yourname.bsky.social" 31 + class="w-full" 32 + /> 33 + </div> 34 + </div> 35 + 36 + <Button type="submit" size="sm" class="ml-auto mt-2 w-full md:w-auto">{'Login'}</Button> 37 + 38 + </div>
+5
apps/docs/src/lib/cards/social/CardGithubCorner.svelte
··· 1 + <script> 2 + import { GithubCorner } from '@fuxui/social'; 3 + </script> 4 + 5 + <GithubCorner class="absolute top-0 right-0" />
+8
apps/docs/src/lib/cards/social/CardStarRating.svelte
··· 1 + <script lang="ts"> 2 + import { StarRating } from '@fuxui/social'; 3 + </script> 4 + 5 + <div class="flex h-full w-full items-center justify-center"> 6 + <StarRating rating={4} changeable={false} /> 7 + </div> 8 +
+39
apps/docs/src/lib/cards/social/CardSwiperCards.svelte
··· 1 + <script> 2 + import { Button } from '@fuxui/base'; 3 + </script> 4 + 5 + <div class="relative flex p-8"> 6 + <div 7 + class="bg-base-200/50 dark:bg-base-800/50 border-base-300/50 dark:border-base-700/50 h-24 w-16 rounded-xl border p-4" 8 + ></div> 9 + <div 10 + class="bg-base-200 dark:bg-base-800 border-base-300 dark:border-base-700 -ml-8 h-24 w-16 rotate-12 rounded-xl border p-4" 11 + ></div> 12 + 13 + <div class="absolute bottom-2 -ml-10 flex w-full justify-between px-2"> 14 + <Button size="iconSm"> 15 + <svg 16 + xmlns="http://www.w3.org/2000/svg" 17 + fill="none" 18 + viewBox="0 0 24 24" 19 + stroke-width="2.5" 20 + stroke="currentColor" 21 + class="size-6" 22 + > 23 + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 24 + </svg> 25 + </Button> 26 + <Button size="iconSm"> 27 + <svg 28 + xmlns="http://www.w3.org/2000/svg" 29 + viewBox="0 0 24 24" 30 + fill="currentColor" 31 + class="size-6" 32 + > 33 + <path 34 + d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" 35 + /> 36 + </svg> 37 + </Button> 38 + </div> 39 + </div>
+18
apps/docs/src/lib/cards/social/CardUserProfile.svelte
··· 1 + <script lang="ts"> 2 + import { UserProfile } from '@fuxui/social'; 3 + </script> 4 + 5 + <div class="flex h-full w-full items-center justify-center"> 6 + <div class="scale-65 -mt-8"> 7 + <UserProfile 8 + class="h-48 w-96" 9 + profile={{ 10 + handle: 'flo-bit', 11 + displayName: 'Flo Bit', 12 + avatar: 'https://github.com/flo-bit.png', 13 + banner: 14 + 'https://cdn.bsky.app/img/banner/plain/did:plc:257wekqxg4hyapkq6k47igmp/bafkreidekzsdfuwjphnfbyobzfniy4mijpfwshtagdqp2jibea7ojwoc2e@jpeg' 15 + }} 16 + /> 17 + </div> 18 + </div>
+64
apps/docs/src/lib/cards/visual/CardImageMasonry.svelte
··· 1 + <script> 2 + import { ImageMasonry } from '@fuxui/visual'; 3 + 4 + import profile1 from '$lib/assets/profiles/0.webp'; 5 + import profile2 from '$lib/assets/profiles/1.webp'; 6 + import profile3 from '$lib/assets/profiles/2.webp'; 7 + import profile4 from '$lib/assets/profiles/3.webp'; 8 + import profile5 from '$lib/assets/profiles/4.webp'; 9 + import profile6 from '$lib/assets/profiles/5.webp'; 10 + import profile7 from '$lib/assets/profiles/6.webp'; 11 + import profile8 from '$lib/assets/profiles/7.webp'; 12 + import profile9 from '$lib/assets/profiles/8.webp'; 13 + import profile10 from '$lib/assets/profiles/9.webp'; 14 + import { toast } from '@fuxui/base'; 15 + 16 + const images = [ 17 + { 18 + src: profile1, 19 + name: 'Image 1', 20 + width: 200, 21 + height: 300 22 + }, 23 + { 24 + src: profile2, 25 + name: 'Image 2', 26 + width: 200, 27 + height: 130 28 + }, 29 + { 30 + src: profile3, 31 + name: 'Image 3', 32 + width: 200, 33 + height: 200 34 + }, 35 + { 36 + src: profile4, 37 + name: 'Image 4', 38 + width: 0.8 39 + }, 40 + { 41 + src: profile5, 42 + name: 'Image 5', 43 + height: 1.5 44 + }, 45 + { 46 + src: profile6, 47 + name: 'Image 6' 48 + }, 49 + { 50 + src: profile7, 51 + name: 'Image 7', 52 + height: 0.7 53 + }, 54 + { 55 + src: profile8, 56 + name: 'Image 8', 57 + height: 1.2 58 + } 59 + ]; 60 + </script> 61 + 62 + <div class="inset-0 flex h-full w-full items-center justify-center"> 63 + <ImageMasonry {images} class="scale-90 h-[200%] w-[200%] columns-4 max-h-fit" showNames={false} /> 64 + </div>
-5
packages/base/src/lib/components/accordion/index.ts
··· 5 5 const Root = AccordionPrimitive.Root; 6 6 7 7 export { 8 - Root, 9 - Content, 10 - Item, 11 - Trigger, 12 - // 13 8 Root as Accordion, 14 9 Content as AccordionContent, 15 10 Item as AccordionItem,
+1 -9
packages/base/src/lib/components/alert/index.ts
··· 3 3 import Title from './alert-title.svelte'; 4 4 export { alertVariants, type AlertVariant } from './alert.svelte'; 5 5 6 - export { 7 - Root, 8 - Description, 9 - Title, 10 - // 11 - Root as Alert, 12 - Description as AlertDescription, 13 - Title as AlertTitle 14 - }; 6 + export { Root as Alert, Description as AlertDescription, Title as AlertTitle };
+1 -1
packages/base/src/lib/components/badge/Badge.svelte
··· 3 3 import { cn } from '../../utils'; 4 4 5 5 export const badgeVariants = tv({ 6 - base: 'inline-flex items-center justify-center whitespace-nowrap rounded-2xl text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 duration-300 active:duration-100 [&_svg]:pointer-events-none [&_svg]:shrink-0', 6 + base: 'inline-flex items-center backdrop-blur-sm justify-center whitespace-nowrap rounded-2xl text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 duration-300 active:duration-100 [&_svg]:pointer-events-none [&_svg]:shrink-0', 7 7 variants: { 8 8 variant: { 9 9 primary:
+1
packages/base/src/lib/components/box/index.ts
··· 1 + export { default as Box } from './Box.svelte';
+2 -11
packages/base/src/lib/components/button/index.ts
··· 1 - import Root, { 1 + export { 2 + default as Button, 2 3 type ButtonProps, 3 4 type ButtonSize, 4 5 type ButtonVariant, 5 6 buttonVariants 6 7 } from './Button.svelte'; 7 - 8 - export { 9 - Root, 10 - type ButtonProps as Props, 11 - Root as Button, 12 - buttonVariants, 13 - type ButtonProps, 14 - type ButtonSize, 15 - type ButtonVariant 16 - };
+1
packages/base/src/lib/components/chat-bubble/index.ts
··· 1 + export { default as ChatBubble } from './ChatBubble.svelte';
+1 -6
packages/base/src/lib/components/checkbox/index.ts
··· 1 - import Root from './checkbox.svelte'; 2 - export { 3 - Root, 4 - // 5 - Root as Checkbox 6 - }; 1 + export { default as Checkbox } from './checkbox.svelte';
+1
packages/base/src/lib/components/head/index.ts
··· 1 + export { default as Head } from './Head.svelte';
+1 -9
packages/base/src/lib/components/input/index.ts
··· 5 5 inputVariants 6 6 } from './Input.svelte'; 7 7 8 - export { 9 - Root, 10 - type InputProps as Props, 11 - Root as Input, 12 - inputVariants, 13 - type InputProps, 14 - type InputSize, 15 - type InputVariant 16 - }; 8 + export { type InputProps, Root as Input, inputVariants, type InputSize, type InputVariant };
+1 -7
packages/base/src/lib/components/label/index.ts
··· 1 - import Root from './Label.svelte'; 2 - 3 - export { 4 - Root, 5 - // 6 - Root as Label 7 - }; 1 + export { default as Label } from './Label.svelte';
-105
packages/base/src/lib/components/modal/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 } from '../button'; 11 - import Modal from './Modal.svelte'; 12 - import { Subheading } from '../heading'; 13 - import { Label } from '../label'; 14 - import Input from '../input/Input.svelte'; 15 - 16 - let value = $state(''); 17 - let error: string | null = $state(null); 18 - let loading = $state(false); 19 - 20 - let { 21 - login 22 - }: { 23 - login: (handle: string) => Promise<void>; 24 - } = $props(); 25 - 26 - async function onSubmit(evt: Event) { 27 - evt.preventDefault(); 28 - if (loading) return; 29 - 30 - error = null; 31 - loading = true; 32 - 33 - try { 34 - await login(value); 35 - } catch (err) { 36 - error = err instanceof Error ? err.message : String(err); 37 - } finally { 38 - loading = false; 39 - } 40 - } 41 - 42 - let input: HTMLInputElement | null = $state(null); 43 - </script> 44 - 45 - <Modal 46 - bind:open={blueskyLoginModalState.open} 47 - class="max-w-sm gap-2 p-4 sm:p-6" 48 - onOpenAutoFocus={(e) => { 49 - e.preventDefault(); 50 - input?.focus(); 51 - }} 52 - > 53 - <form onsubmit={onSubmit} class="flex flex-col gap-2"> 54 - <Subheading class="mb-1 inline-flex items-center gap-2 text-xl font-bold"> 55 - <svg 56 - fill="currentColor" 57 - xmlns="http://www.w3.org/2000/svg" 58 - viewBox="-40 -40 680 620" 59 - version="1.1" 60 - class={['text-accent-600 dark:text-accent-400 size-6']} 61 - aria-hidden="true" 62 - > 63 - <path 64 - 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" 65 - /> 66 - </svg> 67 - Login with Bluesky</Subheading 68 - > 69 - 70 - <div class="text-base-600 dark:text-base-400 text-xs leading-5"> 71 - Don't have an account? 72 - <br /> 73 - <a 74 - href="https://bsky.app" 75 - target="_blank" 76 - class="text-accent-600 dark:text-accent-400 dark:hover:text-accent-500 hover:text-accent-500 font-medium transition-colors" 77 - > 78 - Sign up on bluesky 79 - </a>, then come back here. 80 - </div> 81 - 82 - <div class="mt-4 w-full"> 83 - <Label for="bluesky-handle" class="text-sm">Your handle</Label> 84 - <div class="mt-2"> 85 - <Input 86 - bind:ref={input} 87 - type="text" 88 - name="bluesky-handle" 89 - id="bluesky-handle" 90 - placeholder="yourname.bsky.social" 91 - class="w-full" 92 - bind:value 93 - /> 94 - </div> 95 - </div> 96 - 97 - {#if error} 98 - <p class="text-accent-500 mt-2 text-sm font-medium">{error}</p> 99 - {/if} 100 - 101 - <Button type="submit" class="mt-2 ml-auto w-full md:w-auto" disabled={loading} 102 - >{loading ? 'Loading...' : 'Login'}</Button 103 - > 104 - </form> 105 - </Modal>
+9 -6
packages/base/src/lib/components/modal/Modal.svelte
··· 1 - <script lang="ts"> 1 + <script lang="ts" module> 2 2 import type { Snippet } from 'svelte'; 3 3 import { Dialog, type WithoutChild } from 'bits-ui'; 4 4 import { Button, type ButtonProps } from '../button'; 5 - import { cn } from '../../utils'; 6 5 7 - type Props = Dialog.RootProps & { 6 + export type ModalProps = Dialog.RootProps & { 8 7 title?: string; 9 8 titleSnippet?: Snippet; 10 9 titleClass?: string; ··· 39 38 40 39 onOpenAutoFocus?: (event: Event) => void; 41 40 }; 41 + </script> 42 + 43 + <script lang="ts"> 44 + import { cn } from '../../utils'; 42 45 43 46 let { 44 47 open = $bindable(false), ··· 57 60 class: className, 58 61 onOpenAutoFocus, 59 62 ...restProps 60 - }: Props = $props(); 63 + }: ModalProps = $props(); 61 64 62 65 let yesButtonRef = $state<HTMLButtonElement | null>(null); 63 66 </script> ··· 73 76 {...contentProps} 74 77 class={cn( 75 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', 76 - 'fixed top-[50%] left-[50%] z-50 grid w-[calc(100%-1rem)] max-w-lg translate-x-[-50%] translate-y-[-50%]', 79 + 'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-1rem)] max-w-lg translate-x-[-50%] translate-y-[-50%]', 77 80 'bg-base-50 dark:bg-base-900 border-base-200/80 dark:border-base-800 gap-4 rounded-2xl border p-6', 78 81 className 79 82 )} ··· 141 144 142 145 {#if closeButton} 143 146 <Dialog.Close 144 - 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 top-2 right-2 cursor-pointer rounded-xl p-1 transition-colors focus:outline-2 focus:outline-offset-2" 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" 145 148 > 146 149 <svg 147 150 xmlns="http://www.w3.org/2000/svg"
+2
packages/base/src/lib/components/modal/index.ts
··· 1 + export { default as Modal } from './Modal.svelte'; 2 + export type { ModalProps } from './Modal.svelte';
+1
packages/base/src/lib/components/navbar/index.ts
··· 1 + export { default as Navbar } from './Navbar.svelte';
+2
packages/base/src/lib/components/number-input/index.ts
··· 1 + export { default as NumberInput } from './NumberInput.svelte'; 2 + export type { NumberInputProps, NumberInputVariant, NumberInputSize } from './NumberInput.svelte';
+12 -10
packages/base/src/lib/components/popover/Popover.svelte
··· 1 - <script lang="ts"> 2 - import { buttonVariants, type ButtonSize, type ButtonVariant } from '../button'; 3 - import { Popover as PopoverPrimitive } from 'bits-ui'; 4 - import { Content } from '.'; 5 - import { cn } from '../../utils'; 6 - 1 + <script lang="ts" module> 7 2 const Root = PopoverPrimitive.Root; 8 3 const Trigger = PopoverPrimitive.Trigger; 9 4 10 - type Props = PopoverPrimitive.RootProps & { 5 + export type PopoverProps = PopoverPrimitive.RootProps & { 11 6 triggerProps?: PopoverPrimitive.TriggerProps; 12 7 text?: string; 13 8 ··· 19 14 triggerRef?: HTMLButtonElement | null; 20 15 } & PopoverPrimitive.ContentProps & 21 16 PopoverPrimitive.TriggerProps; 17 + </script> 18 + 19 + <script lang="ts"> 20 + import { buttonVariants, type ButtonSize, type ButtonVariant } from '../button'; 21 + import { Popover as PopoverPrimitive } from 'bits-ui'; 22 + import { PopoverContent } from '.'; 23 + import { cn } from '../../utils'; 22 24 23 25 let { 24 26 open = $bindable(false), ··· 37 39 child: myChild, 38 40 class: className, 39 41 ...restProps 40 - }: Props = $props(); 42 + }: PopoverProps = $props(); 41 43 </script> 42 44 43 45 <Root bind:open {onOpenChange}> ··· 55 57 {triggerText} 56 58 </Trigger> 57 59 {/if} 58 - <Content {side} {sideOffset} class={className} {...restProps}> 60 + <PopoverContent {side} {sideOffset} class={className} {...restProps}> 59 61 {@render children?.()} 60 - </Content> 62 + </PopoverContent> 61 63 </Root>
+1 -5
packages/base/src/lib/components/popover/index.ts
··· 5 5 const Close = PopoverPrimitive.Close; 6 6 7 7 export { 8 - Root, 9 - Content, 10 - Trigger, 11 - Close, 12 - // 13 8 Root as PopoverRoot, 14 9 Content as PopoverContent, 15 10 Trigger as PopoverTrigger, ··· 17 12 }; 18 13 19 14 export { default as Popover } from './Popover.svelte'; 15 + export type { PopoverProps } from './Popover.svelte';
+1
packages/base/src/lib/components/prose/index.ts
··· 1 + export { default as Prose } from './Prose.svelte';
+1 -5
packages/base/src/lib/components/slider/index.ts
··· 1 1 import Root from './Slider.svelte'; 2 2 3 - export { 4 - Root, 5 - // 6 - Root as Slider 7 - }; 3 + export { Root as Slider };
+1
packages/base/src/lib/components/sonner/index.ts
··· 1 1 export { default as Toaster } from './Toaster.svelte'; 2 + export { toast } from 'svelte-sonner';
+1 -5
packages/base/src/lib/components/switch/index.ts
··· 1 1 import Root from './switch.svelte'; 2 2 3 - export { 4 - Root, 5 - // 6 - Root as Switch 7 - }; 3 + export { Root as Switch };
+1 -7
packages/base/src/lib/components/textarea/index.ts
··· 19 19 input: FormTextareaEvent<InputEvent>; 20 20 }; 21 21 22 - export { 23 - Root, 24 - // 25 - Root as Textarea, 26 - type TextareaEvents, 27 - type FormTextareaEvent 28 - }; 22 + export { Root as Textarea, type TextareaEvents, type FormTextareaEvent };
+2 -7
packages/base/src/lib/components/theme-toggle/index.ts
··· 1 - import Root from './ThemeToggle.svelte'; 2 - 3 - export { 4 - Root, 5 - // 6 - Root as ThemeToggle 7 - }; 1 + export { default as ThemeToggle } from './ThemeToggle.svelte'; 2 + export { theme } from './ThemeToggle.svelte';
+1 -7
packages/base/src/lib/components/toggle-group/index.ts
··· 1 1 import Root from "./toggle-group.svelte"; 2 2 import Item from "./toggle-group-item.svelte"; 3 3 4 - export { 5 - Root, 6 - Item, 7 - // 8 - Root as ToggleGroup, 9 - Item as ToggleGroupItem, 10 - }; 4 + export { Root as ToggleGroup, Item as ToggleGroupItem, Item as ToggleItem };
+1 -5
packages/base/src/lib/components/toggle/index.ts
··· 6 6 type ToggleVariants, 7 7 } from "./toggle.svelte"; 8 8 9 - export { 10 - Root, 11 - // 12 - Root as Toggle, 13 - }; 9 + export { Root as Toggle };
+1 -5
packages/base/src/lib/components/tooltip/index.ts
··· 7 7 const Provider = TooltipPrimitive.Provider; 8 8 9 9 export { 10 - Root, 11 - Trigger, 12 - Content, 13 - Provider, 14 - // 10 + Root as TooltipRoot, 15 11 Content as TooltipContent, 16 12 Trigger as TooltipTrigger, 17 13 Provider as TooltipProvider,
+80
packages/social/src/lib/components/blog-entry/BlogEntry.svelte
··· 1 + <script lang="ts"> 2 + import { Badge, Button } from '@fuxui/base'; 3 + 4 + const { 5 + title, 6 + description, 7 + date, 8 + image, 9 + tags, 10 + href 11 + }: { 12 + title: string; 13 + description: string; 14 + date: Date; 15 + image?: string; 16 + tags?: ( 17 + | { 18 + tag: string; 19 + href: string; 20 + } 21 + | string 22 + )[]; 23 + href: string; 24 + } = $props(); 25 + </script> 26 + 27 + <article class="group relative isolate flex w-full flex-col gap-8 md:flex-row"> 28 + {#if image} 29 + <div class="relative aspect-[16/9] md:w-64 md:shrink-0"> 30 + <img 31 + src={image} 32 + alt="" 33 + class="bg-base-50 dark:bg-base-900 absolute inset-0 h-full w-full rounded-2xl object-cover" 34 + /> 35 + 36 + <div 37 + class="ring-base-900/10 dark:ring-base-100/10 absolute inset-0 rounded-2xl ring-1 ring-inset" 38 + ></div> 39 + </div> 40 + {/if} 41 + <div> 42 + <div class={'flex max-h-8.5 flex-wrap items-center gap-2 overflow-hidden p-1 text-xs'}> 43 + <time datetime={date.toISOString()} class="text-base-500 mr-2 shrink-0"> 44 + {date.toLocaleDateString('en-us', { 45 + year: 'numeric', 46 + month: 'short', 47 + day: 'numeric' 48 + })} 49 + </time> 50 + {#if tags} 51 + {#each tags as tag} 52 + {#if typeof tag === 'object'} 53 + <Button href={tag.href} size="sm" class="z-10 max-w-64 justify-start overflow-hidden"> 54 + {tag.tag} 55 + </Button> 56 + {:else} 57 + <Badge size="sm" class="z-10 max-w-64 justify-start overflow-hidden"> 58 + {tag} 59 + </Badge> 60 + {/if} 61 + {/each} 62 + {/if} 63 + </div> 64 + <div class="max-w-xl"> 65 + <div class="text-base-900 dark:text-base-50 mt-3 text-lg leading-6 font-semibold"> 66 + <a {href}> 67 + <span class="absolute inset-0"></span> 68 + {title} 69 + </a> 70 + 71 + <div 72 + class="bg-base-200/30 dark:bg-base-800/30 absolute -inset-2 -z-10 scale-95 opacity-0 transition-all duration-150 group-hover:scale-100 group-hover:opacity-100 sm:rounded-2xl md:-inset-4" 73 + ></div> 74 + </div> 75 + <p class="text-base-600 dark:text-base-400 mt-5 line-clamp-2 text-sm leading-6"> 76 + {@html description} 77 + </p> 78 + </div> 79 + </div> 80 + </article>
+1
packages/social/src/lib/components/blog-entry/index.ts
··· 1 + export { default as BlogEntry } from './BlogEntry.svelte';
+26
packages/social/src/lib/components/bluesky-login/BlueskyLogin.svelte
··· 1 + <script lang="ts"> 2 + import { Button } from '@fuxui/base'; 3 + import { BlueskyLoginModal, blueskyLoginModalState } from '.'; 4 + 5 + let { 6 + login 7 + }: { 8 + login: (handle: string) => Promise<boolean | undefined>; 9 + } = $props(); 10 + </script> 11 + 12 + <Button onclick={() => blueskyLoginModalState.show()}> 13 + <svg 14 + fill="currentColor" 15 + xmlns="http://www.w3.org/2000/svg" 16 + viewBox="-40 -40 680 620" 17 + version="1.1" 18 + aria-hidden="true" 19 + > 20 + <path 21 + 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" 22 + /> 23 + </svg> 24 + Login</Button> 25 + 26 + <BlueskyLoginModal {login} />
+106
packages/social/src/lib/components/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 } from '@fuxui/base'; 11 + 12 + let value = $state(''); 13 + let error: string | null = $state(null); 14 + let loading = $state(false); 15 + 16 + let { 17 + login 18 + }: { 19 + login: (handle: string) => Promise<boolean | undefined>; 20 + } = $props(); 21 + 22 + async function onSubmit(evt: Event) { 23 + evt.preventDefault(); 24 + if (loading) return; 25 + 26 + error = null; 27 + loading = true; 28 + 29 + try { 30 + const hide = await login(value); 31 + 32 + if (hide) { 33 + blueskyLoginModalState.hide(); 34 + value = ''; 35 + } 36 + } catch (err) { 37 + error = err instanceof Error ? err.message : String(err); 38 + } finally { 39 + loading = false; 40 + } 41 + } 42 + 43 + let input: HTMLInputElement | null = $state(null); 44 + </script> 45 + 46 + <Modal 47 + bind:open={blueskyLoginModalState.open} 48 + class="max-w-sm gap-2 p-4 sm:p-6" 49 + onOpenAutoFocus={(e: Event) => { 50 + e.preventDefault(); 51 + input?.focus(); 52 + }} 53 + > 54 + <form onsubmit={onSubmit} class="flex flex-col gap-2"> 55 + <Subheading class="mb-1 inline-flex items-center gap-2 text-xl font-bold"> 56 + <svg 57 + fill="currentColor" 58 + xmlns="http://www.w3.org/2000/svg" 59 + viewBox="-40 -40 680 620" 60 + version="1.1" 61 + class={['text-accent-600 dark:text-accent-400 size-6']} 62 + aria-hidden="true" 63 + > 64 + <path 65 + 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" 66 + /> 67 + </svg> 68 + Login with Bluesky</Subheading 69 + > 70 + 71 + <div class="text-base-600 dark:text-base-400 text-xs leading-5"> 72 + Don't have an account? 73 + <br /> 74 + <a 75 + href="https://bsky.app" 76 + target="_blank" 77 + class="text-accent-600 dark:text-accent-400 dark:hover:text-accent-500 hover:text-accent-500 font-medium transition-colors" 78 + > 79 + Sign up on bluesky 80 + </a>, then come back here. 81 + </div> 82 + 83 + <div class="mt-4 w-full"> 84 + <Label for="bluesky-handle" class="text-sm">Your handle</Label> 85 + <div class="mt-2"> 86 + <Input 87 + bind:ref={input} 88 + type="text" 89 + name="bluesky-handle" 90 + id="bluesky-handle" 91 + placeholder="yourname.bsky.social" 92 + class="w-full" 93 + bind:value 94 + /> 95 + </div> 96 + </div> 97 + 98 + {#if error} 99 + <p class="text-accent-500 mt-2 text-sm font-medium">{error}</p> 100 + {/if} 101 + 102 + <Button type="submit" class="mt-2 ml-auto w-full md:w-auto" disabled={loading} 103 + >{loading ? 'Loading...' : 'Login'}</Button 104 + > 105 + </form> 106 + </Modal>
+3
packages/social/src/lib/components/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';
+62
packages/social/src/lib/components/github-corner/GithubCorner.svelte
··· 1 + <script lang="ts"> 2 + import { cn } from '@fuxui/base'; 3 + import type { HTMLAnchorAttributes } from 'svelte/elements'; 4 + 5 + type Props = HTMLAnchorAttributes; 6 + let { target = '_blank', class: className, ...restProps }: Props = $props(); 7 + </script> 8 + 9 + <a 10 + {target} 11 + class={cn( 12 + 'github-corner fill-accent-600 text-base-50 dark:fill-accent-500 dark:text-base-950 fixed -top-1 -right-1 z-50', 13 + className 14 + )} 15 + {...restProps} 16 + > 17 + <svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="true"> 18 + <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> 19 + <path 20 + d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" 21 + fill="currentColor" 22 + style="transform-origin: 130px 106px;" 23 + class="octo-arm" 24 + ></path> 25 + <path 26 + d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" 27 + fill="currentColor" 28 + class="octo-body" 29 + ></path> 30 + </svg> 31 + <span class="sr-only">View source on GitHub</span> 32 + </a> 33 + 34 + <style> 35 + .github-corner:hover .octo-arm { 36 + animation: octocat-wave 560ms ease-in-out; 37 + } 38 + 39 + @keyframes octocat-wave { 40 + 0%, 41 + 100% { 42 + transform: rotate(0); 43 + } 44 + 20%, 45 + 60% { 46 + transform: rotate(-25deg); 47 + } 48 + 40%, 49 + 80% { 50 + transform: rotate(10deg); 51 + } 52 + } 53 + 54 + @media (max-width: 500px) { 55 + .github-corner:hover .octo-arm { 56 + animation: none; 57 + } 58 + .github-corner .octo-arm { 59 + animation: octocat-wave 560ms ease-in-out; 60 + } 61 + } 62 + </style>
+1
packages/social/src/lib/components/github-corner/index.ts
··· 1 + export { default as GithubCorner } from './GithubCorner.svelte';
+259
packages/social/src/lib/components/post/Post.svelte
··· 1 + <script lang="ts"> 2 + import RelativeTime from './relative-time'; 3 + import Embed from './embeds/Embed.svelte'; 4 + import { cn, Avatar } from '@fuxui/base'; 5 + import type { WithChildren, WithElementRef } from 'bits-ui'; 6 + import type { HTMLAttributes } from 'svelte/elements'; 7 + import type { PostData } from '.'; 8 + 9 + let { 10 + ref, 11 + data, 12 + class: className, 13 + bookmarked = $bindable(false), 14 + liked = $bindable(false), 15 + children 16 + }: WithElementRef<WithChildren<HTMLAttributes<HTMLDivElement>>> & { 17 + data: PostData; 18 + class?: string; 19 + intersect?: () => void; 20 + bookmarked?: boolean; 21 + liked?: boolean; 22 + } = $props(); 23 + 24 + export function numberToHumanReadable(number: number) { 25 + if (number < 1000) { 26 + return number; 27 + } 28 + if (number < 1000000) { 29 + return `${Math.floor(number / 1000)}k`; 30 + } 31 + return `${Math.floor(number / 1000000)}m`; 32 + } 33 + </script> 34 + 35 + <div 36 + bind:this={ref} 37 + class={cn('text-base-950 dark:text-base-50 transition-colors duration-200', className)} 38 + > 39 + {#if data.reposted} 40 + <div class="mb-3 inline-flex items-center gap-2 pl-4 text-xs font-medium"> 41 + <svg 42 + xmlns="http://www.w3.org/2000/svg" 43 + viewBox="0 0 24 24" 44 + fill="currentColor" 45 + class="size-3" 46 + > 47 + <path 48 + fill-rule="evenodd" 49 + d="M4.755 10.059a7.5 7.5 0 0 1 12.548-3.364l1.903 1.903h-3.183a.75.75 0 1 0 0 1.5h4.992a.75.75 0 0 0 .75-.75V4.356a.75.75 0 0 0-1.5 0v3.18l-1.9-1.9A9 9 0 0 0 3.306 9.67a.75.75 0 1 0 1.45.388Zm15.408 3.352a.75.75 0 0 0-.919.53 7.5 7.5 0 0 1-12.548 3.364l-1.902-1.903h3.183a.75.75 0 0 0 0-1.5H2.984a.75.75 0 0 0-.75.75v4.992a.75.75 0 0 0 1.5 0v-3.18l1.9 1.9a9 9 0 0 0 15.059-4.035.75.75 0 0 0-.53-.918Z" 50 + clip-rule="evenodd" 51 + /> 52 + </svg> 53 + 54 + <div class="inline-flex gap-1"> 55 + reposted by 56 + <a href={data.reposted.href} class="hover:text-accent-400 font-bold"> 57 + @{data.reposted.handle} 58 + </a> 59 + </div> 60 + </div> 61 + {/if} 62 + <div class="flex gap-4"> 63 + <Avatar src={data.author.avatar} /> 64 + 65 + <div class="w-full"> 66 + <div class="mb-1 flex items-start justify-between gap-2"> 67 + <a 68 + class="group hover:bg-accent-900/5 -mx-2 -my-0.5 flex flex-col items-baseline gap-x-2 gap-y-0.5 rounded-xl px-2 py-0.5 sm:flex-row" 69 + href={data.author.href} 70 + > 71 + <div 72 + class="text-base-900 group-hover:text-accent-600 dark:text-base-50 dark:group-hover:text-accent-300 text-sm leading-tight font-semibold" 73 + > 74 + {data.author.displayName} 75 + </div> 76 + <div 77 + class="text-base-600 group-hover:text-accent-600 dark:text-base-400 dark:group-hover:text-accent-400 text-sm" 78 + > 79 + @{data.author.handle} 80 + </div> 81 + </a> 82 + 83 + {#if data.createdAt} 84 + <a 85 + href={'#'} 86 + class="text-base-600 hover:text-accent-700 dark:text-base-400 dark:hover:text-accent-300 block text-sm no-underline" 87 + > 88 + <RelativeTime date={new Date(data.createdAt)} locale="en" /> 89 + </a> 90 + {/if} 91 + </div> 92 + 93 + <div class="prose-base"> 94 + <div 95 + class="prose dark:prose-invert prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-500" 96 + > 97 + {#if data.htmlContent} 98 + {@html data.htmlContent} 99 + {:else} 100 + {@render children?.()} 101 + {/if} 102 + </div> 103 + </div> 104 + 105 + <Embed {data} /> 106 + 107 + <div class="text-base-500 dark:text-base-400 mt-4 flex justify-between gap-2"> 108 + <button class="group inline-flex items-center gap-2 text-sm"> 109 + <svg 110 + xmlns="http://www.w3.org/2000/svg" 111 + fill="none" 112 + viewBox="0 0 24 24" 113 + stroke-width="1.5" 114 + stroke="currentColor" 115 + class="group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100" 116 + > 117 + <path 118 + stroke-linecap="round" 119 + stroke-linejoin="round" 120 + d="M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 0 1-.923 1.785A5.969 5.969 0 0 0 6 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337Z" 121 + /> 122 + </svg> 123 + {#if data.replyCount} 124 + {numberToHumanReadable(data.replyCount)} 125 + {/if} 126 + </button> 127 + 128 + <button class="group inline-flex items-center gap-2 text-sm"> 129 + <svg 130 + xmlns="http://www.w3.org/2000/svg" 131 + fill="none" 132 + viewBox="0 0 24 24" 133 + stroke-width="1.5" 134 + stroke="currentColor" 135 + class="group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100" 136 + > 137 + <path 138 + stroke-linecap="round" 139 + stroke-linejoin="round" 140 + d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" 141 + /> 142 + </svg> 143 + {#if data.repostCount} 144 + {numberToHumanReadable(data.repostCount)} 145 + {/if} 146 + </button> 147 + 148 + <button class="group inline-flex items-center gap-2 text-sm" onclick={() => {}}> 149 + {#if liked} 150 + <svg 151 + xmlns="http://www.w3.org/2000/svg" 152 + viewBox="0 0 24 24" 153 + fill="currentColor" 154 + class="group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100" 155 + > 156 + <path 157 + d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" 158 + /> 159 + </svg> 160 + {:else} 161 + <svg 162 + xmlns="http://www.w3.org/2000/svg" 163 + fill="none" 164 + viewBox="0 0 24 24" 165 + stroke-width="1.5" 166 + stroke="currentColor" 167 + class="group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100" 168 + > 169 + <path 170 + stroke-linecap="round" 171 + stroke-linejoin="round" 172 + d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z" 173 + /> 174 + </svg> 175 + {/if} 176 + {#if data.likeCount} 177 + {numberToHumanReadable(data.likeCount)} 178 + {/if} 179 + </button> 180 + 181 + <button onclick={async () => {}} class="group inline-flex items-center gap-2 text-sm"> 182 + <span class="sr-only">Bookmark</span> 183 + 184 + {#if bookmarked} 185 + <svg 186 + xmlns="http://www.w3.org/2000/svg" 187 + viewBox="0 0 24 24" 188 + fill="currentColor" 189 + class="group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100" 190 + > 191 + <path 192 + fill-rule="evenodd" 193 + d="M6.32 2.577a49.255 49.255 0 0 1 11.36 0c1.497.174 2.57 1.46 2.57 2.93V21a.75.75 0 0 1-1.085.67L12 18.089l-7.165 3.583A.75.75 0 0 1 3.75 21V5.507c0-1.47 1.073-2.756 2.57-2.93Z" 194 + clip-rule="evenodd" 195 + /> 196 + </svg> 197 + {:else} 198 + <svg 199 + xmlns="http://www.w3.org/2000/svg" 200 + fill="none" 201 + viewBox="0 0 24 24" 202 + stroke-width="1.5" 203 + stroke="currentColor" 204 + class="group-hover:bg-accent-500/10 group-hover:text-accent-700 dark:group-hover:text-accent-400 -m-1.5 size-7 rounded-full p-1.5 transition-all duration-100" 205 + > 206 + <path 207 + stroke-linecap="round" 208 + stroke-linejoin="round" 209 + d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" 210 + /> 211 + </svg> 212 + {/if} 213 + </button> 214 + </div> 215 + </div> 216 + </div> 217 + </div> 218 + 219 + <style> 220 + .prose-base { 221 + --tw-prose-body: var(--color-base-700); 222 + --tw-prose-headings: var(--color-base-900); 223 + --tw-prose-lead: var(--color-base-600); 224 + --tw-prose-links: var(--color-base-900); 225 + --tw-prose-bold: var(--color-base-900); 226 + --tw-prose-counters: var(--color-base-500); 227 + --tw-prose-bullets: var(--color-base-300); 228 + --tw-prose-hr: var(--color-base-200); 229 + --tw-prose-quotes: var(--color-base-900); 230 + --tw-prose-quote-borders: var(--color-base-200); 231 + --tw-prose-captions: var(--color-base-500); 232 + --tw-prose-kbd: var(--color-base-900); 233 + --tw-prose-kbd-shadows: var(--color-base-900); 234 + --tw-prose-code: var(--color-base-900); 235 + --tw-prose-pre-code: var(--color-base-200); 236 + --tw-prose-pre-bg: var(--color-base-800); 237 + --tw-prose-th-borders: var(--color-base-300); 238 + --tw-prose-td-borders: var(--color-base-200); 239 + 240 + --tw-prose-invert-body: var(--color-base-300); 241 + --tw-prose-invert-headings: var(--color-white); 242 + --tw-prose-invert-lead: var(--color-base-400); 243 + --tw-prose-invert-links: var(--color-white); 244 + --tw-prose-invert-bold: var(--color-white); 245 + --tw-prose-invert-counters: var(--color-base-400); 246 + --tw-prose-invert-bullets: var(--color-base-600); 247 + --tw-prose-invert-hr: var(--color-base-700); 248 + --tw-prose-invert-quotes: var(--color-base-100); 249 + --tw-prose-invert-quote-borders: var(--color-base-700); 250 + --tw-prose-invert-captions: var(--color-base-400); 251 + --tw-prose-invert-kbd: var(--color-white); 252 + --tw-prose-invert-kbd-shadows: var(--color-white); 253 + --tw-prose-invert-code: var(--color-white); 254 + --tw-prose-invert-pre-code: var(--color-base-300); 255 + --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%); 256 + --tw-prose-invert-th-borders: var(--color-base-600); 257 + --tw-prose-invert-td-borders: var(--color-base-700); 258 + } 259 + </style>
+158
packages/social/src/lib/components/post/index.ts
··· 1 + import { RichText } from '@atproto/api'; 2 + 3 + export type PostEmbedImage = { 4 + type: 'images'; 5 + 6 + images: { 7 + alt: string; 8 + thumb: string; 9 + fullsize: string; 10 + aspectRatio?: { 11 + width: number; 12 + height: number; 13 + }; 14 + }[]; 15 + }; 16 + 17 + export type PostEmbedExternal = { 18 + type: 'external'; 19 + 20 + external: { 21 + href: string; 22 + thumb: string; 23 + title: string; 24 + description: string; 25 + }; 26 + }; 27 + 28 + export type PostEmbedVideo = { 29 + type: 'video'; 30 + 31 + video: { 32 + playlist: string; 33 + 34 + thumb: string; 35 + alt: string; 36 + 37 + aspectRatio?: { 38 + width: number; 39 + height: number; 40 + }; 41 + }; 42 + }; 43 + 44 + export type UnknownEmbed = { 45 + type: 'unknown'; 46 + } & Record<string, unknown>; 47 + 48 + function blueskyEmbedTypeToEmbedType(type: string) { 49 + switch (type) { 50 + case 'app.bsky.embed.external#view': 51 + case 'app.bsky.embed.external': 52 + return 'external'; 53 + case 'app.bsky.embed.images#view': 54 + case 'app.bsky.embed.images': 55 + return 'images'; 56 + case 'app.bsky.embed.video#view': 57 + case 'app.bsky.embed.video': 58 + return 'video'; 59 + default: 60 + return 'unknown'; 61 + } 62 + } 63 + 64 + export type PostData = { 65 + reposted?: { handle: string; href: string }; 66 + 67 + author: { 68 + displayName: string; 69 + handle: string; 70 + avatar: string; 71 + href: string; 72 + }; 73 + 74 + replyCount: number; 75 + repostCount: number; 76 + likeCount: number; 77 + 78 + createdAt: string; 79 + 80 + embed?: PostEmbedImage | PostEmbedExternal | PostEmbedVideo | UnknownEmbed; 81 + 82 + htmlContent?: string; 83 + }; 84 + 85 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 86 + export function blueskyPostToPostData(post: any): PostData { 87 + return { 88 + author: { 89 + displayName: post.author.displayName, 90 + handle: post.author.handle, 91 + avatar: post.author.avatar, 92 + href: '#' 93 + }, 94 + replyCount: post.replyCount, 95 + repostCount: post.repostCount, 96 + likeCount: post.likeCount, 97 + createdAt: post.createdAt, 98 + 99 + embed: { 100 + type: blueskyEmbedTypeToEmbedType(post.embed?.$type), 101 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 102 + images: post.embed?.images?.map((image: any) => ({ 103 + alt: image.alt, 104 + thumb: image.thumb, 105 + aspectRatio: image.aspectRatio, 106 + fullsize: image.fullsize 107 + })), 108 + external: post.embed?.external 109 + ? { 110 + href: post.embed.external.uri, 111 + title: post.embed.external.title, 112 + description: post.embed.external.description, 113 + thumb: post.embed.external.thumb 114 + } 115 + : undefined, 116 + video: post.embed 117 + ? { 118 + playlist: post.embed.playlist, 119 + thumb: post.embed.thumbnail, 120 + alt: post.embed.alt, 121 + aspectRatio: post.embed.aspectRatio 122 + } 123 + : undefined 124 + } as PostEmbedExternal | PostEmbedVideo | PostEmbedImage | UnknownEmbed, 125 + 126 + htmlContent: blueskyPostToHTML(post) 127 + }; 128 + } 129 + 130 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 131 + export function blueskyPostToHTML(post: any, baseBskyUrl: string = 'https://bsky.app') { 132 + if (!post?.record) { 133 + return ''; 134 + } 135 + const rt = new RichText(post.record); 136 + let html = ''; 137 + 138 + const createLink = (href: string, text: string) => { 139 + return `<a target="_blank" rel="noopener noreferrer nofollow" href="${encodeURI(href)}">${encodeURI(text)}</a>`; 140 + }; 141 + 142 + for (const segment of rt.segments()) { 143 + if (!segment) continue; 144 + if (segment.isLink() && segment.link?.uri) { 145 + html += createLink(segment.link?.uri, segment.text); 146 + } else if (segment.isMention() && segment.mention?.did) { 147 + html += createLink(`${baseBskyUrl}/profile/${segment.mention?.did}`, segment.text); 148 + } else if (segment.isTag() && segment.tag?.tag) { 149 + html += createLink(`${baseBskyUrl}/hashtag/${segment.tag?.tag}`, segment.text); 150 + } else { 151 + html += segment.text; 152 + } 153 + } 154 + 155 + return html.replace(/\n/g, '<br>'); 156 + } 157 + 158 + export { default as Post } from './Post.svelte';
+47
packages/social/src/lib/components/social-icons/All.svelte
··· 1 + <script lang="ts"> 2 + import Discord from './Discord.svelte'; 3 + import Github from './Github.svelte'; 4 + import Twitter from './Twitter.svelte'; 5 + import Youtube from './Youtube.svelte'; 6 + import Bluesky from './Bluesky.svelte'; 7 + import Facebook from './Facebook.svelte'; 8 + 9 + const { 10 + discord, 11 + github, 12 + twitter, 13 + youtube, 14 + bluesky, 15 + facebook, 16 + svgClasses 17 + }: { 18 + discord?: string; 19 + github?: string; 20 + twitter?: string; 21 + youtube?: string; 22 + bluesky?: string; 23 + facebook?: string; 24 + svgClasses?: string; 25 + } = $props(); 26 + </script> 27 + 28 + <div class="flex flex-wrap items-center gap-4"> 29 + {#if discord} 30 + <Discord href={discord} {svgClasses} /> 31 + {/if} 32 + {#if github} 33 + <Github href={github} {svgClasses} /> 34 + {/if} 35 + {#if twitter} 36 + <Twitter href={twitter} {svgClasses} /> 37 + {/if} 38 + {#if youtube} 39 + <Youtube href={youtube} {svgClasses} /> 40 + {/if} 41 + {#if bluesky} 42 + <Bluesky href={bluesky} {svgClasses} /> 43 + {/if} 44 + {#if facebook} 45 + <Facebook href={facebook} {svgClasses} /> 46 + {/if} 47 + </div>
+37
packages/social/src/lib/components/social-icons/Bluesky.svelte
··· 1 + <script lang="ts"> 2 + import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 3 + import type { HTMLAnchorAttributes } from 'svelte/elements'; 4 + import { cn } from '@fuxui/base'; 5 + 6 + const { 7 + class: className, 8 + target = '_blank', 9 + svgClasses, 10 + ...restProps 11 + }: WithoutChildrenOrChild<WithElementRef<HTMLAnchorAttributes>> & { 12 + svgClasses?: string; 13 + } = $props(); 14 + </script> 15 + 16 + <a 17 + {target} 18 + class={cn( 19 + 'text-base-800 hover:text-accent-600 dark:text-base-300 dark:hover:text-accent-400 transition-colors', 20 + className 21 + )} 22 + {...restProps} 23 + > 24 + <span class="sr-only">Bluesky</span> 25 + 26 + <svg 27 + role="img" 28 + viewBox="0 0 24 24" 29 + xmlns="http://www.w3.org/2000/svg" 30 + class={['size-6', svgClasses]} 31 + aria-hidden="true" 32 + fill="currentColor" 33 + ><path 34 + d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z" 35 + /></svg 36 + > 37 + </a>
+37
packages/social/src/lib/components/social-icons/Discord.svelte
··· 1 + <script lang="ts"> 2 + import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 3 + import type { HTMLAnchorAttributes } from 'svelte/elements'; 4 + import { cn } from '@fuxui/base'; 5 + 6 + const { 7 + class: className, 8 + target = '_blank', 9 + svgClasses, 10 + ...restProps 11 + }: WithoutChildrenOrChild<WithElementRef<HTMLAnchorAttributes>> & { 12 + svgClasses?: string; 13 + } = $props(); 14 + </script> 15 + 16 + <a 17 + {target} 18 + class={cn( 19 + 'text-base-800 hover:text-accent-600 dark:text-base-300 dark:hover:text-accent-400 transition-colors', 20 + className 21 + )} 22 + {...restProps} 23 + > 24 + <span class="sr-only">Discord</span> 25 + 26 + <svg 27 + role="img" 28 + viewBox="0 0 24 24" 29 + xmlns="http://www.w3.org/2000/svg" 30 + class={['size-6', svgClasses]} 31 + fill="currentColor" 32 + aria-hidden="true" 33 + ><path 34 + d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" 35 + /></svg 36 + > 37 + </a>
+37
packages/social/src/lib/components/social-icons/Facebook.svelte
··· 1 + <script lang="ts"> 2 + import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 3 + import type { HTMLAnchorAttributes } from 'svelte/elements'; 4 + import { cn } from '@fuxui/base'; 5 + 6 + const { 7 + class: className, 8 + target = '_blank', 9 + svgClasses, 10 + ...restProps 11 + }: WithoutChildrenOrChild<WithElementRef<HTMLAnchorAttributes>> & { 12 + svgClasses?: string; 13 + } = $props(); 14 + </script> 15 + 16 + <a 17 + {target} 18 + class={cn( 19 + 'text-base-800 hover:text-accent-600 dark:text-base-300 dark:hover:text-accent-400 transition-colors', 20 + className 21 + )} 22 + {...restProps} 23 + > 24 + <span class="sr-only">Facebook</span> 25 + 26 + <svg 27 + role="img" 28 + viewBox="0 0 24 24" 29 + xmlns="http://www.w3.org/2000/svg" 30 + class={['size-6', svgClasses]} 31 + fill="currentColor" 32 + aria-hidden="true" 33 + ><path 34 + d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z" 35 + /></svg 36 + > 37 + </a>
+38
packages/social/src/lib/components/social-icons/Github.svelte
··· 1 + <script lang="ts"> 2 + import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 3 + import type { HTMLAnchorAttributes } from 'svelte/elements'; 4 + import { cn } from '@fuxui/base'; 5 + 6 + const { 7 + class: className, 8 + target = '_blank', 9 + svgClasses, 10 + ...restProps 11 + }: WithoutChildrenOrChild<WithElementRef<HTMLAnchorAttributes>> & { 12 + svgClasses?: string; 13 + } = $props(); 14 + </script> 15 + 16 + <a 17 + {target} 18 + class={cn( 19 + 'text-base-800 hover:text-accent-600 dark:text-base-300 dark:hover:text-accent-400 rounded-2xl transition-colors', 20 + 'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 focus-visible:outline-2 focus-visible:outline-offset-2', 21 + className 22 + )} 23 + {...restProps} 24 + > 25 + <span class="sr-only">GitHub</span> 26 + 27 + <svg 28 + role="img" 29 + viewBox="0 0 24 24" 30 + xmlns="http://www.w3.org/2000/svg" 31 + fill="currentColor" 32 + aria-hidden="true" 33 + class={['size-5', svgClasses]} 34 + ><path 35 + d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" 36 + /></svg 37 + > 38 + </a>
+37
packages/social/src/lib/components/social-icons/Twitter.svelte
··· 1 + <script lang="ts"> 2 + import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 3 + import type { HTMLAnchorAttributes } from 'svelte/elements'; 4 + import { cn } from '@fuxui/base'; 5 + 6 + const { 7 + class: className, 8 + target = '_blank', 9 + svgClasses, 10 + ...restProps 11 + }: WithoutChildrenOrChild<WithElementRef<HTMLAnchorAttributes>> & { 12 + svgClasses?: string; 13 + } = $props(); 14 + </script> 15 + 16 + <a 17 + {target} 18 + class={cn( 19 + 'text-base-800 hover:text-accent-600 dark:text-base-300 dark:hover:text-accent-400 transition-colors', 20 + className 21 + )} 22 + {...restProps} 23 + > 24 + <span class="sr-only">X</span> 25 + 26 + <svg 27 + role="img" 28 + viewBox="0 0 24 24" 29 + class={['size-6', svgClasses]} 30 + fill="currentColor" 31 + aria-hidden="true" 32 + xmlns="http://www.w3.org/2000/svg" 33 + ><title>X</title><path 34 + d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" 35 + /></svg 36 + > 37 + </a>
+36
packages/social/src/lib/components/social-icons/Youtube.svelte
··· 1 + <script lang="ts"> 2 + import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 3 + import type { HTMLAnchorAttributes } from 'svelte/elements'; 4 + import { cn } from '@fuxui/base'; 5 + 6 + const { 7 + class: className, 8 + target = '_blank', 9 + svgClasses, 10 + ...restProps 11 + }: WithoutChildrenOrChild<WithElementRef<HTMLAnchorAttributes>> & { 12 + svgClasses?: string; 13 + } = $props(); 14 + </script> 15 + 16 + <a 17 + {target} 18 + class={cn( 19 + 'text-base-800 hover:text-accent-600 dark:text-base-300 dark:hover:text-accent-400 transition-colors', 20 + className 21 + )} 22 + {...restProps} 23 + > 24 + <span class="sr-only">YouTube</span> 25 + <svg 26 + class={['size-7', svgClasses]} 27 + fill="currentColor" 28 + aria-hidden="true" 29 + role="img" 30 + viewBox="0 0 24 24" 31 + xmlns="http://www.w3.org/2000/svg" 32 + ><path 33 + d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" 34 + /></svg 35 + > 36 + </a>
+9
packages/social/src/lib/components/social-icons/index.ts
··· 1 + // all icons from https://simpleicons.org/ 2 + 3 + export { default as SocialIcons } from './All.svelte'; 4 + export { default as Discord } from './Discord.svelte'; 5 + export { default as Github } from './Github.svelte'; 6 + export { default as Twitter } from './Twitter.svelte'; 7 + export { default as Youtube } from './Youtube.svelte'; 8 + export { default as Bluesky } from './Bluesky.svelte'; 9 + export { default as Facebook } from './Facebook.svelte';
+104
packages/social/src/lib/components/star-rating/StarRating.svelte
··· 1 + <script lang="ts"> 2 + // todo convert to radio group 3 + import { cn } from '@fuxui/base'; 4 + import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 5 + import type { HTMLAttributes } from 'svelte/elements'; 6 + 7 + let { 8 + rating = $bindable(), 9 + size = 'size-8', 10 + changeable = true, 11 + class: className, 12 + ref = $bindable(null), 13 + strokeWidth = 0.8, 14 + buttonClasses, 15 + svgClasses, 16 + ...restProps 17 + }: WithElementRef<WithoutChildrenOrChild<HTMLAttributes<HTMLDivElement>>> & { 18 + rating: number; 19 + size?: string; 20 + changeable?: boolean; 21 + 22 + strokeWidth?: number; 23 + 24 + buttonClasses?: string; 25 + svgClasses?: string; 26 + } = $props(); 27 + 28 + let hoverRating = $state(rating); 29 + 30 + $effect(() => { 31 + hoverRating = rating; 32 + }); 33 + </script> 34 + 35 + <div class={cn('flex items-center', className)} {...restProps} bind:this={ref}> 36 + {#if changeable} 37 + {#each Array.from({ length: 5 }).map((_, i) => i + 1) as i} 38 + <button 39 + class={cn( 40 + 'group focus-visible:outline-base-900 dark:focus-visible:outline-base-100 cursor-pointer rounded-xl focus-visible:outline-2', 41 + buttonClasses 42 + )} 43 + onclick={() => (rating = i)} 44 + onfocus={() => (hoverRating = i)} 45 + onblur={() => (hoverRating = rating)} 46 + onmouseenter={() => (hoverRating = i)} 47 + onmouseleave={() => (hoverRating = rating)} 48 + > 49 + <svg 50 + class={cn( 51 + size, 52 + 'shrink-0', 53 + i > rating && 54 + i > hoverRating && 55 + 'stroke-base-400 text-base-100 dark:stroke-base-700/70 dark:text-base-900', 56 + i <= rating && 57 + i <= hoverRating && 58 + 'stroke-accent-600 text-accent-400 dark:stroke-accent-500/80 dark:text-accent-500/40', 59 + 60 + ((i > rating && i <= hoverRating) || (i > hoverRating && i <= rating)) && 61 + 'stroke-accent-600/50 text-accent-400/50 dark:stroke-accent-500/40 dark:text-accent-500/20', 62 + svgClasses 63 + )} 64 + viewBox="0 0 24 24" 65 + fill="currentColor" 66 + aria-hidden="true" 67 + stroke-width={strokeWidth} 68 + data-slot="icon" 69 + > 70 + <path 71 + fill-rule="evenodd" 72 + d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" 73 + clip-rule="evenodd" 74 + /> 75 + </svg> 76 + 77 + <span class="sr-only">rate {i} stars</span> 78 + </button> 79 + {/each} 80 + {:else} 81 + {#each Array.from({ length: 5 }).map((_, i) => i + 1) as i} 82 + <svg 83 + class={cn( 84 + size, 85 + 'stroke-base-400 text-base-100 dark:stroke-base-700 dark:text-base-800 shrink-0', 86 + i <= rating && 87 + 'stroke-accent-600 text-accent-400 dark:stroke-accent-500/80 dark:text-accent-500/50', 88 + svgClasses 89 + )} 90 + viewBox="0 0 24 24" 91 + fill="currentColor" 92 + aria-hidden="true" 93 + stroke-width={strokeWidth} 94 + data-slot="icon" 95 + > 96 + <path 97 + fill-rule="evenodd" 98 + d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" 99 + clip-rule="evenodd" 100 + /> 101 + </svg> 102 + {/each} 103 + {/if} 104 + </div>
+1
packages/social/src/lib/components/star-rating/index.ts
··· 1 + export { default as StarRating } from './StarRating.svelte';
+235
packages/social/src/lib/components/swiper-cards/CardSwiper.svelte
··· 1 + <script lang="ts"> 2 + // TODO: fix types 3 + 4 + import { onMount, type Snippet } from 'svelte'; 5 + import { DragGesture, type FullGestureState } from '@use-gesture/vanilla'; 6 + import type { CardData, Direction, SwipeEventData } from '.'; 7 + import { Image } from '@fuxui/base'; 8 + 9 + let { 10 + card = DefaultCard, 11 + cardData, 12 + onswipe, 13 + swipe = $bindable(), 14 + 15 + minSwipeDistance = 0.5, 16 + minSwipeVelocity = 0.5, 17 + arrowKeys = true, 18 + thresholdPassed = $bindable(0), 19 + anchor = null, 20 + rotate = true, 21 + cardCount = 5 22 + }: { 23 + card?: Snippet<[CardData]>; 24 + 25 + cardData: (index: number) => CardData; 26 + onswipe?: (cardInfo: SwipeEventData) => void; 27 + swipe?: (direction: Direction) => void; 28 + 29 + minSwipeDistance?: number; 30 + minSwipeVelocity?: number; 31 + arrowKeys?: boolean; 32 + thresholdPassed?: number; 33 + anchor?: number | null; 34 + rotate?: boolean; 35 + cardCount?: number; 36 + } = $props(); 37 + 38 + swipe = (direction: Direction = 'right') => { 39 + if (thresholdPassed !== 0) return; 40 + 41 + let dir = direction === 'left' ? -1 : 1; 42 + 43 + if (!topCard) throw new Error('No top card found'); 44 + cardSwiped(topCard, [dir, 0.1], [dir, 1]); 45 + }; 46 + 47 + let container: HTMLElement; 48 + 49 + let cardIndex = 0; 50 + let topCard: HTMLElement | undefined = $state(); 51 + let topCardIndex = 0; 52 + 53 + let currentZ = 100000; 54 + 55 + let cards: HTMLElement[] = $state(new Array(cardCount)); 56 + let cardsData: CardData[] = $state([]); 57 + 58 + onMount(() => { 59 + for (let i = 0; i < cardCount; i++) { 60 + cardsData.push(cardData(cardIndex++)); 61 + cards.push(); 62 + } 63 + 64 + let gestures: DragGesture[] = []; 65 + 66 + cards.forEach((el) => { 67 + if (!el) return; 68 + 69 + el.style.zIndex = currentZ.toString(); 70 + currentZ--; 71 + 72 + let gesture = new DragGesture(el, (state) => { 73 + ondrag(el, state); 74 + }); 75 + 76 + gestures.push(gesture); 77 + }); 78 + 79 + topCardIndex = 0; 80 + topCard = cards[topCardIndex]; 81 + 82 + container.classList.remove('hidden'); 83 + 84 + return () => { 85 + gestures.forEach((gesture) => gesture.destroy()); 86 + }; 87 + }); 88 + 89 + const cardSwiped = (el: HTMLElement, velocity: [number, number], movement: [number, number]) => { 90 + el.classList.add('transition-transform', 'duration-300'); 91 + let index = cards.indexOf(el); 92 + 93 + let direction: Direction = movement[0] > 0 ? 'right' : 'left'; 94 + let data = cardsData[index]; 95 + 96 + onswipe?.({ direction, element: el, data, index: cardIndex - 2 }); 97 + 98 + thresholdPassed = movement[0] > 0 ? 1 : -1; 99 + 100 + let moveOutWidth = document.body.clientWidth; 101 + 102 + let endX = Math.max(Math.abs(velocity[0]) * moveOutWidth, moveOutWidth); 103 + let toX = movement[0] > 0 ? endX : -endX; 104 + let endY = Math.abs(velocity[1]) * moveOutWidth; 105 + let toY = movement[1] > 0 ? endY : -endY; 106 + 107 + let rotate = movement[0] * 0.03 * (movement[1] / 80); 108 + 109 + el.style.transform = `translate(${toX}px, ${toY + movement[1]}px) rotate(${rotate}deg)`; 110 + 111 + setTimeout(async () => { 112 + thresholdPassed = 0; 113 + 114 + cardsData[index] = cardData(cardIndex++); 115 + 116 + // next card 117 + topCardIndex = (topCardIndex + 1) % cardCount; 118 + topCard = cards[topCardIndex]; 119 + 120 + currentZ--; 121 + el.style.zIndex = currentZ.toString(); 122 + 123 + el.classList.remove('transition-transform', 'duration-300'); 124 + el.style.transform = ''; 125 + }, 350); 126 + }; 127 + 128 + const ondrag = ( 129 + el: HTMLElement, 130 + state: Omit<FullGestureState<'drag'>, 'event'> & { 131 + event: PointerEvent | MouseEvent | TouchEvent | KeyboardEvent; 132 + } 133 + ) => { 134 + let elWidth = el.offsetWidth; 135 + 136 + if (state.active) { 137 + let angle = state.movement[0] * 0.03 * (state.movement[1] / 80); 138 + 139 + // fix movement on a curved path if anchor is set 140 + if (anchor) { 141 + let vec = [state.movement[0], state.movement[1] - anchor]; 142 + let len = Math.sqrt(vec[0] ** 2 + vec[1] ** 2); 143 + vec = [(vec[0] / len) * anchor, (vec[1] / len) * anchor]; 144 + 145 + state.movement[0] = vec[0]; 146 + state.movement[1] = vec[1] + anchor; 147 + } 148 + 149 + el.style.transform = `translate(${state.movement[0]}px, ${state.movement[1]}px)`; 150 + 151 + if (rotate) { 152 + el.style.transform += ` rotate(${angle}deg)`; 153 + } 154 + 155 + if (Math.abs(state.movement[0]) / elWidth > minSwipeDistance) { 156 + thresholdPassed = state.movement[0] > 0 ? 1 : -1; 157 + } else { 158 + thresholdPassed = 0; 159 + } 160 + return; 161 + } 162 + // if dragging is finished 163 + let keep = 164 + Math.abs(state.movement[0]) / elWidth < minSwipeDistance && 165 + Math.abs(state.velocity[0]) < minSwipeVelocity; 166 + 167 + if (keep) { 168 + thresholdPassed = 0; 169 + el.classList.add('transition-transform', 'duration-300'); 170 + el.style.transform = ''; 171 + setTimeout(() => { 172 + el.classList.remove('transition-transform', 'duration-300'); 173 + }, 300); 174 + } else { 175 + cardSwiped(el, state.velocity, state.movement); 176 + } 177 + }; 178 + </script> 179 + 180 + <svelte:body 181 + onkeydown={(e) => { 182 + if (!arrowKeys) return; 183 + if (e.key === 'ArrowLeft') { 184 + swipe('left'); 185 + } else if (e.key === 'ArrowRight') { 186 + swipe('right'); 187 + } 188 + }} 189 + /> 190 + 191 + {#snippet DefaultCard({ image, title, description }: CardData)} 192 + <div 193 + class="border-base-400 dark:border-base-600 bg-base-50 dark:bg-base-900 relative h-full w-full overflow-hidden rounded-2xl border" 194 + > 195 + {#key image} 196 + {#if image} 197 + <Image 198 + containerClasses="absolute inset-0 h-full w-full rounded-2xl" 199 + src={image} 200 + alt={title ?? ''} 201 + loading="eager" 202 + class="h-full w-full object-cover" 203 + /> 204 + {/if} 205 + {/key} 206 + <div 207 + class="from-base-50/80 dark:from-base-950/80 absolute inset-0 rounded-b-xl bg-gradient-to-t via-transparent" 208 + ></div> 209 + <div class="absolute bottom-0 flex w-full justify-start px-3 py-16 sm:px-12"> 210 + <div class="flex flex-col"> 211 + <h3 class="text-base-900 dark:text-base-50 pb-2 text-3xl font-bold">{title}</h3> 212 + <p class="text-base-800 dark:text-base-200 text-sm">{description}</p> 213 + </div> 214 + </div> 215 + </div> 216 + {/snippet} 217 + 218 + <div class="isolate h-full w-full touch-none select-none"> 219 + <div class="relative z-0 hidden h-full w-full" bind:this={container}> 220 + {#each cards as _, i} 221 + <div 222 + bind:this={cards[i]} 223 + class="absolute h-full w-full cursor-grab touch-none overflow-hidden ease-in-out select-none" 224 + > 225 + {#if cardsData[i]} 226 + {@render card({ 227 + image: cardsData[i].image, 228 + title: cardsData[i].title, 229 + description: cardsData[i].description 230 + })} 231 + {/if} 232 + </div> 233 + {/each} 234 + </div> 235 + </div>
+18
packages/social/src/lib/components/swiper-cards/index.ts
··· 1 + export type CardData = { 2 + title?: string; 3 + description?: string; 4 + image?: string; 5 + // anything else 6 + [key: string]: unknown; 7 + }; 8 + 9 + export type SwipeEventData = { 10 + direction: Direction; 11 + data: CardData; 12 + index: number; 13 + element: HTMLElement; 14 + }; 15 + 16 + export type Direction = 'left' | 'right'; 17 + 18 + export { default as CardSwiper } from './CardSwiper.svelte';
+60
packages/social/src/lib/components/user-profile/UserProfile.svelte
··· 1 + <script lang="ts"> 2 + import { Avatar, Button, cn } from '@fuxui/base'; 3 + 4 + let { profile, class: className }: { profile: { 5 + banner?: string; 6 + avatar?: string; 7 + displayName?: string; 8 + handle?: string; 9 + description?: string; 10 + }, class: string; } = $props(); 11 + </script> 12 + 13 + {#if profile} 14 + <div class={cn("mx-auto max-w-full sm:max-w-2xl sm:py-6", className)}> 15 + <div> 16 + {#if profile.banner} 17 + <img 18 + class="aspect-[3/1] w-full border-b border-base-800 object-cover sm:rounded-xl sm:border" 19 + src={profile.banner} 20 + alt="" 21 + /> 22 + {:else} 23 + <div class="aspect-[8/1] w-full"></div> 24 + {/if} 25 + </div> 26 + <div 27 + class={cn( 28 + profile.banner ? '-mt-11' : '-mt-8', 29 + 'flex max-w-full items-end space-x-5 px-4 sm:-mt-16 sm:px-6 lg:px-8' 30 + )} 31 + > 32 + <Avatar src={profile.avatar} class="border-base-50 dark:border-base-950 border-2 size-24 sm:size-32" /> 33 + <div 34 + class="flex min-w-0 flex-1 flex-row sm:flex-row sm:items-center sm:justify-end sm:space-x-6 sm:pb-1" 35 + > 36 + <div 37 + class={cn( 38 + profile.banner ? 'mt-4 sm:mt-0' : '-mt-[4.5rem] sm:-mt-[6.5rem]', 39 + 'flex min-w-0 max-w-full flex-1 flex-col items-baseline' 40 + )} 41 + > 42 + <h1 43 + class="max-w-full truncate text-lg font-bold text-base-900 dark:text-base-100 sm:text-xl" 44 + > 45 + {profile.displayName || profile.handle} 46 + </h1> 47 + <div class="truncate text-xs text-base-900 dark:text-base-400 sm:text-sm"> 48 + {profile.handle} 49 + </div> 50 + </div> 51 + </div> 52 + 53 + <!-- <Button>Follow</Button> --> 54 + </div> 55 + 56 + <div class="px-4 sm:px-6 lg:px-8 py-4 text-xs sm:text-sm text-base-800 dark:text-base-200"> 57 + {@html profile.description?.replaceAll('\n', '<br/>')} 58 + </div> 59 + </div> 60 + {/if}
+1
packages/social/src/lib/components/user-profile/index.ts
··· 1 + export { default as UserProfile } from './UserProfile.svelte';
+95
packages/visual/src/lib/components/image-masonry/ImageMasonry.svelte
··· 1 + <script lang="ts"> 2 + import { Badge, cn } from '@fuxui/base'; 3 + 4 + type Props = { 5 + images: { 6 + src: string; 7 + 8 + name: string; 9 + href?: string; 10 + width?: number; 11 + height?: number; 12 + onclick?: () => void; 13 + alt?: string; 14 + }[]; 15 + interactive?: boolean; 16 + maxColumns?: number; 17 + showNames?: boolean; 18 + class?: string; 19 + }; 20 + 21 + const { images, interactive = true, maxColumns = 4, showNames = true, class: className }: Props = $props(); 22 + </script> 23 + 24 + {#snippet backgroundTitle(name?: string)} 25 + {#if showNames} 26 + <div 27 + class={cn( 28 + 'from-base-950/80 absolute inset-0 z-10 h-full w-full bg-gradient-to-t via-transparent opacity-80', 29 + interactive 30 + ? 'transition-opacity duration-200 group-hover:opacity-90 group-focus:opacity-100 [@media(pointer:fine)]:opacity-0' 31 + : '' 32 + )} 33 + ></div> 34 + 35 + <div 36 + class={cn( 37 + 'pointer-events-none absolute inset-0 z-20 flex h-full w-full items-end overflow-hidden px-3 py-2 text-xl font-semibold leading-6 tracking-tight text-white md:px-4 md:py-3', 38 + interactive 39 + ? 'transition-translation duration-200 group-hover:translate-y-0 [@media(pointer:fine)]:translate-y-20' 40 + : '' 41 + )} 42 + > 43 + <div class="mt-1 text-sm sm:text-lg">{name}</div> 44 + </div> 45 + {:else} 46 + <div class="absolute inset-0 z-10 h-full w-full"></div> 47 + <span class="sr-only">{name}</span> 48 + {/if} 49 + {/snippet} 50 + 51 + <div 52 + class={cn( 53 + 'not-prose gap-x-4', 54 + maxColumns === 2 ? 'columns-2' : '', 55 + maxColumns === 3 ? 'columns-2 sm:columns-3' : '', 56 + maxColumns === 4 ? 'columns-2 sm:columns-3 md:columns-4' : '', 57 + className 58 + )} 59 + > 60 + {#each images as image} 61 + <div 62 + class="group relative mb-4 w-full break-inside-avoid-column overflow-hidden rounded-2xl border border-white/15" 63 + style={`aspect-ratio: ${image.width ?? 1} / ${image.height ?? 1}`} 64 + > 65 + <img 66 + src={image.src} 67 + alt={image.alt} 68 + class={cn( 69 + 'absolute inset-0 h-full w-full rounded-2xl object-cover', 70 + image.onclick || image.href ? 'transition-all duration-300 group-hover:scale-105' : '' 71 + )} 72 + loading="lazy" 73 + width="270" 74 + /> 75 + 76 + {#if image.href} 77 + <a href={image.href} class="z-20"> 78 + {@render backgroundTitle(image.name)} 79 + </a> 80 + {:else if image.onclick} 81 + <button 82 + class="z-20 cursor-pointer" 83 + onclick={() => { 84 + console.log('clicked'); 85 + image.onclick?.(); 86 + }} 87 + > 88 + {@render backgroundTitle(image.name)} 89 + </button> 90 + {:else} 91 + {@render backgroundTitle(image.name)} 92 + {/if} 93 + </div> 94 + {/each} 95 + </div>
+1
packages/visual/src/lib/components/image-masonry/index.ts
··· 1 + export { default as ImageMasonry } from './ImageMasonry.svelte';
+2 -8
apps/docs/src/routes/(main)/docs/philosophy/Philosophy.md
··· 31 31 32 32 So the plan is to have both options (usually you start by just using the package and if you 33 33 eventually need to customize a component you add it to your project), 34 - currently the recommended way to use it is installing as a package, 35 - but a cli tool is in the works and can already be tested with the base components by running: 36 - 37 - ```bash 38 - npx fuchs add [component-name] 39 - ``` 40 - 41 - (Note: this still needs you to install `fuchs` as a package first). 34 + currently the only way to use it is installing as a package, 35 + but a cli tool is in the works. 42 36 43 37 ## Lots of components 44 38
+26
packages/social/src/lib/components/post/embeds/Embed.svelte
··· 1 + <script lang="ts"> 2 + import type { PostData } from '../'; 3 + import External from './External.svelte'; 4 + import Images from './Images.svelte'; 5 + import Video from './Video.svelte'; 6 + 7 + const { data }: { data: PostData } = $props(); 8 + </script> 9 + 10 + {#if data.embed} 11 + <div class="flex flex-col gap-2 pt-3 text-sm"> 12 + {#if data.embed.type === 'images'} 13 + <Images data={data.embed} /> 14 + {:else if data.embed.type === 'external' && data.embed.external} 15 + <External data={data.embed} /> 16 + {:else if data.embed.type === 'video' && data.embed.video} 17 + <Video data={data.embed} /> 18 + {:else if data.embed.type === 'unknown'} 19 + <div 20 + class="text-base-700 dark:text-base-300 bg-base-200/50 dark:bg-base-900/50 border-base-300 dark:border-base-600/30 rounded-2xl border p-4 text-sm" 21 + > 22 + Unknown embed type 23 + </div> 24 + {/if} 25 + </div> 26 + {/if}
+39
packages/social/src/lib/components/post/embeds/External.svelte
··· 1 + <script lang="ts"> 2 + import type { PostEmbedExternal } from '..'; 3 + 4 + const { data }: { data: PostEmbedExternal } = $props(); 5 + 6 + const domain = new URL(data.external.href).hostname.replace('www.', ''); 7 + </script> 8 + 9 + <article 10 + class={[ 11 + 'group dark:bg-base-900 bg-base-200 border-base-300 dark:border-base-600/30 relative isolate flex flex-col justify-end overflow-hidden rounded-2xl border p-4', 12 + data.external.thumb ? 'aspect-[16/9]' : '' 13 + ]} 14 + > 15 + {#if data.external.thumb} 16 + <img 17 + src={data.external.thumb} 18 + alt={data.external.description} 19 + class="absolute inset-0 -z-10 size-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105" 20 + /> 21 + {/if} 22 + <div 23 + class="dark:from-base-950/90 dark:via-base-950/40 from-base-50/90 via-base-50/40 absolute inset-0 -z-10 bg-gradient-to-t" 24 + ></div> 25 + 26 + <div 27 + class="text-base-700 dark:text-base-300 flex flex-wrap items-center gap-y-1 overflow-hidden text-sm" 28 + > 29 + <div>{domain}</div> 30 + </div> 31 + <h3 32 + class="dark:text-base-50 text-base-900 group-hover:text-accent-600 dark:group-hover:text-accent-400 mt-1 text-lg/6 font-semibold transition-colors duration-200" 33 + > 34 + <a href={data.external.href} target="_blank" rel="noopener noreferrer nofollow"> 35 + <span class="absolute inset-0"></span> 36 + {data.external.title} 37 + </a> 38 + </h3> 39 + </article>
+38
packages/social/src/lib/components/post/embeds/Images.svelte
··· 1 + <script lang="ts"> 2 + import type { PostEmbedImage } from '..'; 3 + 4 + const { data }: { data: PostEmbedImage } = $props(); 5 + </script> 6 + 7 + {#snippet imageSnippet( 8 + image: { 9 + alt: string; 10 + thumb: string; 11 + fullsize: string; 12 + aspectRatio?: { width: number; height: number }; 13 + }, 14 + className?: string 15 + )} 16 + <img 17 + loading="lazy" 18 + src={image.thumb} 19 + alt={image.alt} 20 + style={image.aspectRatio 21 + ? `aspect-ratio: ${image.aspectRatio.width} / ${image.aspectRatio.height}` 22 + : 'aspect-ratio: 1 / 1'} 23 + class={[ 24 + 'border-base-500/20 dark:border-base-400/20 w-fit max-w-full rounded-2xl border max-h-[40rem] object-contain', 25 + className 26 + ]} 27 + /> 28 + {/snippet} 29 + 30 + {#if data.images.length === 1} 31 + {@render imageSnippet(data.images[0])} 32 + {:else} 33 + <div class="columns-2 gap-4"> 34 + {#each data.images as image} 35 + {@render imageSnippet(image, 'mb-4')} 36 + {/each} 37 + </div> 38 + {/if}
+45
packages/social/src/lib/components/post/embeds/Video.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import Hls from 'hls.js'; 4 + import type { PostEmbedVideo } from '..'; 5 + 6 + const { data }: { data: PostEmbedVideo } = $props(); 7 + 8 + onMount(async () => { 9 + const Plyr = (await import('plyr')).default; 10 + if (Hls.isSupported()) { 11 + var hls = new Hls(); 12 + hls.loadSource(data.video.playlist); 13 + hls.attachMedia(element); 14 + } 15 + 16 + new Plyr(element, { 17 + controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'fullscreen'], 18 + ratio: data.video.aspectRatio 19 + ? `${data.video.aspectRatio.width}:${data.video.aspectRatio.height}` 20 + : '16:9' 21 + }); 22 + }); 23 + 24 + let element: HTMLMediaElement; 25 + </script> 26 + 27 + <svelte:head> 28 + <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" /> 29 + </svelte:head> 30 + 31 + <div 32 + style={data.video.aspectRatio 33 + ? `aspect-ratio: ${data.video.aspectRatio.width} / ${data.video.aspectRatio.height}` 34 + : 'aspect-ratio: 16 / 9'} 35 + class="border-base-300 dark:border-base-400/40 w-full max-w-full overflow-hidden rounded-2xl border" 36 + > 37 + <!-- svelte-ignore a11y_media_has_caption --> 38 + <video bind:this={element} class="h-full w-full" aria-label={data.video.alt}></video> 39 + </div> 40 + 41 + <style> 42 + * { 43 + --plyr-color-main: var(--color-accent-500); 44 + } 45 + </style>
+17
packages/social/src/lib/components/post/relative-time/RelativeTime.svelte
··· 1 + <script lang="ts"> 2 + import { onDestroy } from 'svelte'; 3 + import { register, unregister } from './state'; 4 + 5 + export let date: Date | number; 6 + export let locale: string; 7 + export let live = true; 8 + 9 + let instance = new Object(); 10 + let text = ''; 11 + 12 + register(instance, date, locale, live, (value) => ({ text } = value)); 13 + 14 + onDestroy(() => unregister(instance)); 15 + </script> 16 + 17 + <span class={$$props.class}>{text}</span>
+31
packages/social/src/lib/components/post/relative-time/action.ts
··· 1 + import type { Callback } from './render'; 2 + import { register, unregister } from './state'; 3 + 4 + export interface Options { 5 + date: Date | number; 6 + locale?: string; 7 + live?: boolean; 8 + } 9 + 10 + export function relativeTime(node: HTMLElement, options: Options) { 11 + const callback: Callback = ({ text }) => (node.textContent = text); 12 + 13 + function init(options: Options) { 14 + const date = options.date; 15 + const locale = options.locale ?? navigator.language; 16 + const live = (options.live = true); 17 + 18 + register(node, date, locale, live, callback); 19 + } 20 + 21 + init(options); 22 + 23 + return { 24 + update(options: Options) { 25 + init(options); 26 + }, 27 + destroy() { 28 + unregister(node); 29 + } 30 + }; 31 + }
+12
packages/social/src/lib/components/post/relative-time/formatter.ts
··· 1 + // keep a cache of formatter per locale, to avoid re-creating them (GC) 2 + const formatters = new Map<string, Intl.RelativeTimeFormat>(); 3 + 4 + // get the Intl.RelativeTimeFormat formatter for the given locale 5 + export function getFormatter(locale: string) { 6 + if (formatters.has(locale)) { 7 + return formatters.get(locale)!; 8 + } 9 + const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'always', style: 'narrow' }); 10 + formatters.set(locale, formatter); 11 + return formatter; 12 + }
+6
packages/social/src/lib/components/post/relative-time/index.ts
··· 1 + // adapted from https://github.com/CaptainCodeman/svelte-relative-time 2 + 3 + export * from './action'; 4 + export type { Callback } from './render'; 5 + export { register, unregister } from './state'; 6 + export { default } from './RelativeTime.svelte';
+80
packages/social/src/lib/components/post/relative-time/render.ts
··· 1 + export type Callback = (result: { 2 + seconds: number; 3 + count: number; 4 + units: Intl.RelativeTimeFormatUnit; 5 + text: string; 6 + }) => void; 7 + 8 + export interface RenderState { 9 + date: Date | number; 10 + callback: Callback; 11 + formatter: Intl.RelativeTimeFormat; 12 + } 13 + 14 + // Array reprsenting one minute, hour, day, week, month, etc in seconds 15 + const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; 16 + 17 + // Array equivalent to the above but in the string representation of the units 18 + const formatUnits: Intl.RelativeTimeFormatUnit[] = [ 19 + 'seconds', 20 + 'minutes', 21 + 'hours', 22 + 'days', 23 + 'weeks', 24 + 'months', 25 + 'years' 26 + ]; 27 + 28 + // function to render relative time into 29 + export function render(state: RenderState, now: number) { 30 + const { date, callback, formatter } = state; 31 + 32 + // Allow dates or times to be passed 33 + const timeMs = typeof date === 'number' ? date : date.getTime(); 34 + 35 + // Get the amount of seconds between the given date and now 36 + const delta = timeMs - now; 37 + const seconds = Math.round(delta / 1000); 38 + 39 + // Grab the ideal cutoff unit 40 + const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(seconds)); 41 + 42 + // units 43 + const units = formatUnits[unitIndex]; 44 + 45 + // Get the divisor to divide from the seconds. E.g. if our unit is 'day' our divisor 46 + // is one day in seconds, so we can divide our seconds by this to get the # of days 47 + const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1; 48 + 49 + // count of units 50 + const count = Math.round(seconds / divisor); 51 + 52 + // Intl.RelativeTimeFormat do its magic 53 + callback({ 54 + seconds: seconds, 55 + count, 56 + units, 57 + text: formatter.format(count, units).replace('ago', '') 58 + }); 59 + 60 + // calculate time to next update, taking account rounding (e.g. it goes from 2 minutes to 1 minute at 90 seconds) 61 + // and also the changeover from units (59 seconds shouldn't show as 1 minute, so at 61 seconds we schedule the next 62 + // update for 1 second time) 63 + 64 + const divisorMs = divisor * 1000; 65 + 66 + let updateIn; 67 + 68 + if (unitIndex) { 69 + updateIn = divisorMs / 2 - (Math.abs(delta) % divisorMs); 70 + if (updateIn < 0) { 71 + updateIn += divisorMs; 72 + } 73 + } else { 74 + updateIn = divisorMs - (Math.abs(delta) % divisorMs); 75 + } 76 + 77 + const updateAt = now + updateIn; 78 + 79 + return updateAt; 80 + }
+60
packages/social/src/lib/components/post/relative-time/state.ts
··· 1 + import { getFormatter } from './formatter'; 2 + import { render } from './render'; 3 + import type { Callback, RenderState } from './render'; 4 + 5 + interface UpdateState extends RenderState { 6 + update: number; 7 + } 8 + 9 + // keep track of each instance 10 + const instances = new Map<Object, UpdateState>(); 11 + 12 + // we use a single timer for efficiency and to keep updates in sync 13 + let updateInterval: number | NodeJS.Timeout; 14 + 15 + // register or update instance 16 + export function register( 17 + instance: Object, 18 + date: Date | number, 19 + locale: string, 20 + live: boolean, 21 + callback: Callback 22 + ) { 23 + // get the formatter for the given locale, we do this here so we don't keep having to look it up on each tick 24 + const formatter = getFormatter(locale); 25 + 26 + // create state to render 27 + const state = { date, callback, formatter }; 28 + 29 + // initial render is immediate, so works for SSR 30 + const update = render(state, Date.now()); 31 + 32 + // if it's to update live, we keep a track and schedule the next update 33 + if (live) { 34 + instances.set(instance, { ...state, update }); 35 + } else { 36 + instances.delete(instance); 37 + } 38 + 39 + // start the clock ticking if there are any live instances 40 + if (instances.size) { 41 + updateInterval = 42 + updateInterval || 43 + setInterval(() => { 44 + const now = Date.now(); 45 + for (const state of instances.values()) { 46 + if (state.update <= now) { 47 + state.update = render(state, now); 48 + } 49 + } 50 + }, 1000); 51 + } 52 + } 53 + 54 + export function unregister(instance: Object) { 55 + instances.delete(instance); 56 + if (instances.size === 0) { 57 + clearInterval(updateInterval); 58 + updateInterval = 0; 59 + } 60 + }
+8
apps/docs/src/routes/(main)/components/social/bluesky-login/+page.svelte
··· 1 + <script lang="ts"> 2 + import BlueskyLoginDocs from './BlueskyLogin.md'; 3 + import { Prose } from '@fuxui/base'; 4 + </script> 5 + 6 + <Prose> 7 + <BlueskyLoginDocs /> 8 + </Prose>
+22
apps/docs/src/routes/(main)/components/social/bluesky-login/BlueskyLogin.md
··· 1 + <script lang="ts"> 2 + import Example from './Example.svelte'; 3 + </script> 4 + 5 + # Bluesky Login 6 + 7 + ## Example 8 + 9 + <Example /> 10 + 11 + ## Usage 12 + 13 + ```svelte 14 + <script lang="ts"> 15 + import { BlueskyLogin } from '@fuxui/social'; 16 + </script> 17 + 18 + <BlueskyLogin login={async (handle) => { 19 + // do login and return true if login 20 + // is successful and should hide the modal 21 + }} /> 22 + ```
+15
apps/docs/src/routes/(main)/components/social/bluesky-login/Example.svelte
··· 1 + <script lang="ts"> 2 + import { toast } from '@fuxui/base'; 3 + import { BlueskyLogin } from '@fuxui/social'; 4 + </script> 5 + 6 + 7 + <BlueskyLogin login={async (handle) => { 8 + if(!handle) { 9 + toast.error('Please enter a handle'); 10 + return false; 11 + } 12 + await new Promise((resolve) => setTimeout(resolve, 1000)); 13 + toast.success(`Login successful for ${handle}`); 14 + return true; 15 + }} />
+8
apps/docs/src/routes/(main)/components/social/card-swiper/+page.svelte
··· 1 + <script lang="ts"> 2 + import SwiperCardsDocs from './SwiperCards.md'; 3 + import { Prose } from '@fuxui/base'; 4 + </script> 5 + 6 + <Prose> 7 + <SwiperCardsDocs /> 8 + </Prose>
+68
apps/docs/src/routes/(main)/components/social/card-swiper/Example.svelte
··· 1 + <script lang="ts"> 2 + import { Button } from '@fuxui/base'; 3 + import { CardSwiper } from '@fuxui/social'; 4 + 5 + import profile1 from '$lib/assets/profiles/0.webp'; 6 + import profile2 from '$lib/assets/profiles/1.webp'; 7 + import profile3 from '$lib/assets/profiles/2.webp'; 8 + import profile4 from '$lib/assets/profiles/3.webp'; 9 + import profile5 from '$lib/assets/profiles/4.webp'; 10 + import profile6 from '$lib/assets/profiles/5.webp'; 11 + import profile7 from '$lib/assets/profiles/6.webp'; 12 + import profile8 from '$lib/assets/profiles/7.webp'; 13 + import profile9 from '$lib/assets/profiles/8.webp'; 14 + import profile10 from '$lib/assets/profiles/9.webp'; 15 + 16 + const profiles = [ 17 + profile1, 18 + profile2, 19 + profile3, 20 + profile4, 21 + profile5, 22 + profile6, 23 + profile7, 24 + profile8, 25 + profile9, 26 + profile10 27 + ]; 28 + 29 + let swipe: (direction: 'left' | 'right') => void; 30 + </script> 31 + 32 + <div class="not-prose relative h-[70vh] w-full"> 33 + <CardSwiper 34 + cardData={(i) => ({ 35 + title: `Card ${i}`, 36 + description: `Card ${i} description`, 37 + image: profiles[i % profiles.length] 38 + })} 39 + bind:swipe 40 + ></CardSwiper> 41 + 42 + <div class="absolute bottom-2 flex w-full justify-between px-2"> 43 + <Button onclick={() => swipe('left')} size="iconLg"> 44 + <svg 45 + xmlns="http://www.w3.org/2000/svg" 46 + fill="none" 47 + viewBox="0 0 24 24" 48 + stroke-width="2.5" 49 + stroke="currentColor" 50 + class="size-6" 51 + > 52 + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 53 + </svg> 54 + </Button> 55 + <Button onclick={() => swipe('right')} size="iconLg"> 56 + <svg 57 + xmlns="http://www.w3.org/2000/svg" 58 + viewBox="0 0 24 24" 59 + fill="currentColor" 60 + class="size-6" 61 + > 62 + <path 63 + d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" 64 + /> 65 + </svg> 66 + </Button> 67 + </div> 68 + </div>
+29
apps/docs/src/routes/(main)/components/social/card-swiper/SwiperCards.md
··· 1 + <script lang="ts"> 2 + import SwiperCardsExample from './Example.svelte'; 3 + </script> 4 + 5 + # Swiper Cards 6 + 7 + ## Example 8 + 9 + <SwiperCardsExample /> 10 + 11 + ## Usage 12 + 13 + ```svelte 14 + <script lang="ts"> 15 + import { CardSwiper } from '@fuxui/social'; 16 + 17 + const profiles = ['image1.webp', 'image2.webp', 'image3.webp']; 18 + </script> 19 + 20 + <div class="h-[70vh] w-full"> 21 + <CardSwiper 22 + cardData={(i) => ({ 23 + title: `Card ${i}`, 24 + description: `Card ${i} description`, 25 + image: profiles[i % profiles.length] 26 + })} 27 + ></CardSwiper> 28 + </div> 29 + ```
+8
apps/docs/src/routes/(main)/components/social/github-corner/+page.svelte
··· 1 + <script lang="ts"> 2 + import GithubCornerDocs from './GithubCorner.md'; 3 + import { Prose } from '@fuxui/base'; 4 + </script> 5 + 6 + <Prose> 7 + <GithubCornerDocs /> 8 + </Prose>
+9
apps/docs/src/routes/(main)/components/social/github-corner/Example.svelte
··· 1 + <script lang="ts"> 2 + import { Box } from '@fuxui/base'; 3 + import { GithubCorner } from '@fuxui/social'; 4 + </script> 5 + 6 + <Box class="not-prose relative min-h-24 w-full"> 7 + <GithubCorner class="absolute right-0 top-0" href="https://github.com/flo-bit/ui-kit" /> 8 + <div class="h-24"></div> 9 + </Box>
+19
apps/docs/src/routes/(main)/components/social/github-corner/GithubCorner.md
··· 1 + <script lang="ts"> 2 + import GithubCornerExample from './Example.svelte'; 3 + </script> 4 + 5 + # Github Corner 6 + 7 + ## Example 8 + 9 + <GithubCornerExample /> 10 + 11 + ## Usage 12 + 13 + ```svelte 14 + <script lang="ts"> 15 + import { GithubCorner } from '@fuxui/social'; 16 + </script> 17 + 18 + <GithubCorner href="https://github.com/flo-bit/ui-kit" /> 19 + ```
+8
apps/docs/src/routes/(main)/components/social/star-rating/+page.svelte
··· 1 + <script lang="ts"> 2 + import StarRatingDocs from './StarRating.md'; 3 + import { Prose } from '@fuxui/base'; 4 + </script> 5 + 6 + <Prose> 7 + <StarRatingDocs /> 8 + </Prose>
+7
apps/docs/src/routes/(main)/components/social/star-rating/Example.svelte
··· 1 + <script lang="ts"> 2 + import { StarRating } from '@fuxui/social'; 3 + </script> 4 + 5 + <div class="flex h-full w-full items-center justify-center"> 6 + <StarRating rating={4} changeable /> 7 + </div>
+19
apps/docs/src/routes/(main)/components/social/star-rating/StarRating.md
··· 1 + <script lang="ts"> 2 + import StarRatingExample from './Example.svelte'; 3 + </script> 4 + 5 + # Star Rating 6 + 7 + ## Example 8 + 9 + <StarRatingExample /> 10 + 11 + ## Usage 12 + 13 + ```svelte 14 + <script lang="ts"> 15 + import { StarRating } from '@fuxui/social'; 16 + </script> 17 + 18 + <StarRating rating={4} changeable /> 19 + ```
+8
apps/docs/src/routes/(main)/components/social/user-profile/+page.svelte
··· 1 + <script lang="ts"> 2 + import UserProfileDocs from './UserProfile.md'; 3 + import { Prose } from '@fuxui/base'; 4 + </script> 5 + 6 + <Prose> 7 + <UserProfileDocs /> 8 + </Prose>
+13
apps/docs/src/routes/(main)/components/social/user-profile/Example.svelte
··· 1 + <script lang="ts"> 2 + import { UserProfile } from '@fuxui/social'; 3 + </script> 4 + 5 + <UserProfile 6 + class="not-prose" 7 + profile={{ 8 + handle: '@flo-bit', 9 + displayName: 'Florian', 10 + avatar: 'https://github.com/flo-bit.png', 11 + banner: 'https://cdn.bsky.app/img/banner/plain/did:plc:257wekqxg4hyapkq6k47igmp/bafkreidekzsdfuwjphnfbyobzfniy4mijpfwshtagdqp2jibea7ojwoc2e@jpeg' 12 + }} 13 + />
+26
apps/docs/src/routes/(main)/components/social/user-profile/UserProfile.md
··· 1 + <script lang="ts"> 2 + import UserProfileExample from './Example.svelte'; 3 + </script> 4 + 5 + # User Profile 6 + 7 + ## Example 8 + 9 + <UserProfileExample /> 10 + 11 + ## Usage 12 + 13 + ```svelte 14 + <script lang="ts"> 15 + import { UserProfile } from '@fuxui/social'; 16 + </script> 17 + 18 + <UserProfile 19 + profile={{ 20 + handle: '@flo-bit', 21 + displayName: 'Florian', 22 + avatar: 'https://github.com/flo-bit.png', 23 + banner: 'https://cdn.bsky.app/img/banner/plain/did:plc:257wekqxg4hyapkq6k47igmp/bafkreidekzsdfuwjphnfbyobzfniy4mijpfwshtagdqp2jibea7ojwoc2e@jpeg' 24 + }} 25 + /> 26 + ```
+1 -1
apps/docs/src/routes/(main)/components/visual/excalidraw/Excalidraw.md
··· 16 16 17 17 ```svelte 18 18 <script lang="ts"> 19 - import { Excalidraw } from 'fuchs'; 19 + import { Excalidraw } from '@fuxui/visual'; 20 20 21 21 import svg from '$docs/assets/demo.svg?raw'; 22 22 </script>
+8
apps/docs/src/routes/(main)/components/visual/image-masonry/+page.svelte
··· 1 + <script lang="ts"> 2 + import ImageMasonryDocs from './ImageMasonry.md'; 3 + import { Prose } from '@fuxui/base'; 4 + </script> 5 + 6 + <Prose> 7 + <ImageMasonryDocs /> 8 + </Prose>
+69
apps/docs/src/routes/(main)/components/visual/image-masonry/Example.svelte
··· 1 + <script> 2 + import { ImageMasonry } from '@fuxui/visual'; 3 + 4 + import profile1 from '$lib/assets/profiles/0.webp'; 5 + import profile2 from '$lib/assets/profiles/1.webp'; 6 + import profile3 from '$lib/assets/profiles/2.webp'; 7 + import profile4 from '$lib/assets/profiles/3.webp'; 8 + import profile5 from '$lib/assets/profiles/4.webp'; 9 + import profile6 from '$lib/assets/profiles/5.webp'; 10 + import profile7 from '$lib/assets/profiles/6.webp'; 11 + import profile8 from '$lib/assets/profiles/7.webp'; 12 + import profile9 from '$lib/assets/profiles/8.webp'; 13 + import profile10 from '$lib/assets/profiles/9.webp'; 14 + import { toast } from '@fuxui/base'; 15 + 16 + const images = [ 17 + { 18 + src: profile1, 19 + name: 'Image 1', 20 + width: 200, 21 + height: 300, 22 + }, 23 + { 24 + src: profile2, 25 + name: 'Image 2', 26 + width: 200, 27 + height: 130 28 + }, 29 + { 30 + src: profile3, 31 + name: 'Image 3', 32 + width: 200, 33 + height: 200, 34 + }, 35 + { 36 + src: profile4, 37 + name: 'Image 4', 38 + width: 0.8 39 + }, 40 + { 41 + src: profile5, 42 + name: 'Image 5', 43 + height: 1.5 44 + }, 45 + { 46 + src: profile6, 47 + name: 'Image 6', 48 + }, 49 + { 50 + src: profile7, 51 + name: 'Image 7', 52 + height: 0.7 53 + }, 54 + { 55 + src: profile8, 56 + name: 'Image 8', 57 + height: 1.2 58 + } 59 + ].map((image) => ({ 60 + ...image, 61 + onclick: () => { 62 + toast.success('clicked'); 63 + } 64 + })); 65 + 66 + console.log(images); 67 + </script> 68 + 69 + <ImageMasonry {images} />
+46
apps/docs/src/routes/(main)/components/visual/image-masonry/ImageMasonry.md
··· 1 + <script lang="ts"> 2 + import Example from './Example.svelte'; 3 + </script> 4 + 5 + # Image Masonry 6 + 7 + ## Example 8 + 9 + <Example /> 10 + 11 + ## Usage 12 + 13 + ```svelte 14 + <script lang="ts"> 15 + import { ImageMasonry } from '@fuxui/visual'; 16 + 17 + const images = [ 18 + { 19 + src: 'https://picsum.photos/200/300', 20 + name: 'Image 1', 21 + width: 200, 22 + height: 300, 23 + // optional either onclick 24 + onclick: () => { 25 + console.log('clicked'); 26 + } 27 + }, 28 + { 29 + src: 'https://picsum.photos/200/300', 30 + name: 'Image 2', 31 + width: 200, 32 + height: 150, 33 + // or href 34 + href: '#' 35 + }, 36 + { 37 + src: 'https://picsum.photos/200/300', 38 + name: 'Image 3', 39 + width: 100, 40 + height: 100 41 + }, 42 + ]; 43 + </script> 44 + 45 + <ImageMasonry {images} /> 46 + ```
+1 -1
apps/docs/src/routes/(main)/components/visual/phone/Phone.md
··· 12 12 13 13 ```svelte 14 14 <script lang="ts"> 15 - import { Phone } from 'fuchs'; 15 + import { Phone } from '@fuxui/visual'; 16 16 </script> 17 17 18 18 <Phone>
+1 -1
apps/docs/src/routes/(main)/components/visual/quote/Quote.md
··· 12 12 13 13 ```svelte 14 14 <script> 15 - import { Quote } from 'fuchs'; 15 + import { Quote } from '@fuxui/visual'; 16 16 </script> 17 17 18 18 <Quote
+1 -1
apps/docs/src/routes/(main)/components/visual/undraw/Undraw.md
··· 22 22 23 23 ```svelte 24 24 <script lang="ts"> 25 - import { Undraw } from 'fuchs'; 25 + import { Undraw } from '@fuxui/visual'; 26 26 27 27 import svg from './your-illustration.svg?raw'; 28 28 </script>