grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

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

Merge branch 'main' into gallery-sort

+690 -226
+1 -1
deno.json
··· 2 2 "imports": { 3 3 "$lexicon/": "./__generated__/", 4 4 "@atproto/syntax": "npm:@atproto/syntax@^0.4.0", 5 - "@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.11", 5 + "@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.14", 6 6 "@gfx/canvas": "jsr:@gfx/canvas@^0.5.8", 7 7 "@std/path": "jsr:@std/path@^1.0.9", 8 8 "@tailwindcss/cli": "npm:@tailwindcss/cli@^4.1.4",
+223 -21
deno.lock
··· 1 1 { 2 2 "version": "4", 3 3 "specifiers": { 4 - "jsr:@bigmoves/atproto-oauth-client@0.1": "0.1.0", 5 - "jsr:@bigmoves/bff@0.3.0-beta.11": "0.3.0-beta.11", 4 + "jsr:@bigmoves/atproto-oauth-client@0.2": "0.2.0", 5 + "jsr:@bigmoves/bff@0.3.0-beta.14": "0.3.0-beta.14", 6 + "jsr:@deno/gfm@0.10": "0.10.0", 7 + "jsr:@denosaurs/emoji@0.3": "0.3.1", 6 8 "jsr:@denosaurs/plug@1": "1.0.5", 7 9 "jsr:@denosaurs/plug@1.0.5": "1.0.5", 8 10 "jsr:@gfx/canvas@~0.5.8": "0.5.8", 9 11 "jsr:@std/assert@0.214": "0.214.0", 10 12 "jsr:@std/assert@0.217": "0.217.0", 13 + "jsr:@std/assert@^1.0.12": "1.0.13", 11 14 "jsr:@std/assert@^1.0.13": "1.0.13", 15 + "jsr:@std/async@^1.0.12": "1.0.12", 12 16 "jsr:@std/cache@0.2": "0.2.0", 13 17 "jsr:@std/cli@^1.0.17": "1.0.17", 18 + "jsr:@std/data-structures@^1.0.6": "1.0.7", 14 19 "jsr:@std/encoding@0.214": "0.214.0", 15 20 "jsr:@std/encoding@0.217.0": "0.217.0", 16 21 "jsr:@std/encoding@^1.0.10": "1.0.10", 17 22 "jsr:@std/fmt@0.214": "0.214.0", 18 - "jsr:@std/fmt@^1.0.7": "1.0.7", 23 + "jsr:@std/fmt@^1.0.8": "1.0.8", 19 24 "jsr:@std/fs@0.214": "0.214.0", 20 25 "jsr:@std/fs@0.217.0": "0.217.0", 21 - "jsr:@std/html@^1.0.3": "1.0.3", 22 - "jsr:@std/http@^1.0.13": "1.0.15", 23 - "jsr:@std/internal@^1.0.6": "1.0.6", 26 + "jsr:@std/fs@^1.0.15": "1.0.17", 27 + "jsr:@std/fs@^1.0.16": "1.0.17", 28 + "jsr:@std/html@^1.0.4": "1.0.4", 29 + "jsr:@std/http@^1.0.13": "1.0.16", 30 + "jsr:@std/internal@^1.0.6": "1.0.7", 24 31 "jsr:@std/media-types@^1.1.0": "1.1.0", 25 32 "jsr:@std/net@^1.0.4": "1.0.4", 26 33 "jsr:@std/path@0.214": "0.214.0", ··· 29 36 "jsr:@std/path@^1.0.8": "1.0.9", 30 37 "jsr:@std/path@^1.0.9": "1.0.9", 31 38 "jsr:@std/streams@^1.0.9": "1.0.9", 39 + "jsr:@std/testing@^1.0.11": "1.0.11", 32 40 "npm:@atproto-labs/handle-resolver-node@~0.1.14": "0.1.14", 33 41 "npm:@atproto-labs/simple-store@~0.1.2": "0.1.2", 34 42 "npm:@atproto/api@~0.14.19": "0.14.22", ··· 42 50 "npm:@atproto/oauth-types@~0.2.4": "0.2.4", 43 51 "npm:@atproto/syntax@0.4": "0.4.0", 44 52 "npm:@atproto/xrpc-server@*": "0.7.15", 53 + "npm:@skyware/jetstream@~0.2.2": "0.2.2", 45 54 "npm:@tailwindcss/cli@*": "4.1.4", 55 + "npm:@tailwindcss/cli@^4.0.12": "4.1.4", 56 + "npm:@tailwindcss/cli@^4.1.3": "4.1.4", 46 57 "npm:@tailwindcss/cli@^4.1.4": "4.1.4", 47 58 "npm:@types/node@*": "22.12.0", 48 59 "npm:clsx@^2.1.1": "2.1.1", 49 60 "npm:date-fns@^4.1.0": "4.1.0", 61 + "npm:github-slugger@2": "2.0.0", 62 + "npm:he@^1.2.0": "1.2.0", 50 63 "npm:jose@5.9.6": "5.9.6", 64 + "npm:katex@0.16": "0.16.22", 65 + "npm:marked-alert@2": "2.1.2_marked@12.0.2", 66 + "npm:marked-footnote@^1.2.0": "1.2.4_marked@12.0.2", 67 + "npm:marked-gfm-heading-id@^3.1.0": "3.2.0_marked@12.0.2", 68 + "npm:marked@12": "12.0.2", 51 69 "npm:multiformats@*": "9.9.0", 52 70 "npm:multiformats@^13.3.2": "13.3.2", 53 71 "npm:popmotion@^11.0.5": "11.0.5", 54 72 "npm:preact-render-to-string@^6.5.13": "6.5.13_preact@10.26.5", 55 73 "npm:preact@^10.26.5": "10.26.5", 74 + "npm:prismjs@^1.29.0": "1.30.0", 75 + "npm:sanitize-html@^2.13.0": "2.15.0", 56 76 "npm:sharp@~0.34.1": "0.34.1", 57 77 "npm:tailwind-merge@^3.2.0": "3.2.0", 78 + "npm:tailwindcss@^4.0.12": "4.1.4", 79 + "npm:tailwindcss@^4.1.3": "4.1.4", 58 80 "npm:tailwindcss@^4.1.4": "4.1.4", 59 81 "npm:typed-htmx@~0.3.1": "0.3.1" 60 82 }, 61 83 "jsr": { 62 - "@bigmoves/atproto-oauth-client@0.1.0": { 63 - "integrity": "d5858f534a800a46af28b1c03b447b179d15bbf164c24767601ae78513501711", 84 + "@bigmoves/atproto-oauth-client@0.2.0": { 85 + "integrity": "5c3ca124dd52eff51dace83790779ebe48c4b41559b799e16c8750bd415f2124", 64 86 "dependencies": [ 65 87 "npm:@atproto-labs/handle-resolver-node", 66 88 "npm:@atproto-labs/simple-store", ··· 70 92 "npm:jose" 71 93 ] 72 94 }, 73 - "@bigmoves/bff@0.3.0-beta.11": { 74 - "integrity": "1bcdf36eaa440d2cafbf834b37852b4b3f49c97d9802b2307d077cb2f507db5f", 95 + "@bigmoves/bff@0.3.0-beta.14": { 96 + "integrity": "2b94d1f58c9b035cb2a50e3161953ab5c8c158caf902eccd89ae0beb2db60edc", 75 97 "dependencies": [ 76 98 "jsr:@bigmoves/atproto-oauth-client", 77 99 "jsr:@std/assert@^1.0.13", ··· 91 113 "npm:tailwind-merge" 92 114 ] 93 115 }, 116 + "@deno/gfm@0.10.0": { 117 + "integrity": "51708205e3559a4aeb6afb29d07c5bfafe7941f91bb360351ef6621de9a39527", 118 + "dependencies": [ 119 + "jsr:@denosaurs/emoji", 120 + "npm:github-slugger", 121 + "npm:he", 122 + "npm:katex", 123 + "npm:marked", 124 + "npm:marked-alert", 125 + "npm:marked-footnote", 126 + "npm:marked-gfm-heading-id", 127 + "npm:prismjs", 128 + "npm:sanitize-html" 129 + ] 130 + }, 131 + "@denosaurs/emoji@0.3.1": { 132 + "integrity": "b0aed5f55dec99e83da7c9637fe0a36d1d6252b7c99deaaa3fc5dea3fcf3da8b" 133 + }, 94 134 "@denosaurs/plug@1.0.5": { 95 135 "integrity": "04cd988da558adc226202d88c3a434d5fcc08146eaf4baf0cea0c2284b16d2bf", 96 136 "dependencies": [ ··· 122 162 "jsr:@std/internal" 123 163 ] 124 164 }, 165 + "@std/async@1.0.12": { 166 + "integrity": "d1bfcec459e8012846fe4e38dfc4241ab23240ecda3d8d6dfcf6d81a632e803d" 167 + }, 125 168 "@std/cache@0.2.0": { 126 169 "integrity": "63a2ccd5a9e7c03e430f7d34dfcfd0d0cfc90731a1eaf8208f4c66e418fc3035" 127 170 }, 128 171 "@std/cli@1.0.17": { 129 172 "integrity": "e15b9abe629e17be90cc6216327f03a29eae613365f1353837fa749aad29ce7b" 173 + }, 174 + "@std/data-structures@1.0.7": { 175 + "integrity": "16932d2c8d281f65eaaa2209af2473209881e33b1ced54cd1b015e7b4cdbb0d2" 130 176 }, 131 177 "@std/encoding@0.214.0": { 132 178 "integrity": "30a8713e1db22986c7e780555ffd2fefd1d4f9374d734bb41f5970f6c3352af5" ··· 140 186 "@std/fmt@0.214.0": { 141 187 "integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4" 142 188 }, 143 - "@std/fmt@1.0.7": { 144 - "integrity": "2a727c043d8df62cd0b819b3fb709b64dd622e42c3b1bb817ea7e6cc606360fb" 189 + "@std/fmt@1.0.8": { 190 + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" 145 191 }, 146 192 "@std/fs@0.214.0": { 147 193 "integrity": "bc880fea0be120cb1550b1ed7faf92fe071003d83f2456a1e129b39193d85bea", ··· 157 203 "jsr:@std/path@0.217" 158 204 ] 159 205 }, 160 - "@std/html@1.0.3": { 161 - "integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988" 206 + "@std/fs@1.0.17": { 207 + "integrity": "1c00c632677c1158988ef7a004cb16137f870aafdb8163b9dce86ec652f3952b", 208 + "dependencies": [ 209 + "jsr:@std/path@^1.0.9" 210 + ] 211 + }, 212 + "@std/html@1.0.4": { 213 + "integrity": "eff3497c08164e6ada49b7f81a28b5108087033823153d065e3f89467dd3d50e" 162 214 }, 163 - "@std/http@1.0.15": { 164 - "integrity": "435a4934b4e196e82a8233f724da525f7b7112f3566502f28815e94764c19159", 215 + "@std/http@1.0.16": { 216 + "integrity": "80c8d08c4bfcf615b89978dcefb84f7e880087cf3b6b901703936f3592a06933", 165 217 "dependencies": [ 166 218 "jsr:@std/cli", 167 219 "jsr:@std/encoding@^1.0.10", 168 - "jsr:@std/fmt@^1.0.7", 220 + "jsr:@std/fmt@^1.0.8", 169 221 "jsr:@std/html", 170 222 "jsr:@std/media-types", 171 223 "jsr:@std/net", ··· 173 225 "jsr:@std/streams" 174 226 ] 175 227 }, 176 - "@std/internal@1.0.6": { 177 - "integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4" 228 + "@std/internal@1.0.7": { 229 + "integrity": "39eeb5265190a7bc5d5591c9ff019490bd1f2c3907c044a11b0d545796158a0f" 178 230 }, 179 231 "@std/media-types@1.1.0": { 180 232 "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" ··· 199 251 }, 200 252 "@std/streams@1.0.9": { 201 253 "integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035" 254 + }, 255 + "@std/testing@1.0.11": { 256 + "integrity": "12b3db12d34f0f385a26248933bde766c0f8c5ad8b6ab34d4d38f528ab852f48", 257 + "dependencies": [ 258 + "jsr:@std/assert@^1.0.12", 259 + "jsr:@std/async", 260 + "jsr:@std/data-structures", 261 + "jsr:@std/fs@^1.0.16", 262 + "jsr:@std/internal", 263 + "jsr:@std/path@^1.0.8" 264 + ] 202 265 } 203 266 }, 204 267 "npm": { 268 + "@atcute/bluesky@1.0.15_@atcute+client@2.0.9": { 269 + "integrity": "sha512-+EFiybmKQ97aBAgtaD+cKRJER5AMn3cZMkEwEg/pDdWyzxYJ9m1UgemmLdTgI8VrxPufKqdXS2nl7uO7TY6BPA==", 270 + "dependencies": [ 271 + "@atcute/client" 272 + ] 273 + }, 274 + "@atcute/client@2.0.9": { 275 + "integrity": "sha512-QNDm9gMP6x9LY77ArwY+urQOBtQW74/onEAz42c40JxRm6Rl9K9cU4ROvNKJ+5cpVmEm1sthEWVRmDr5CSZENA==" 276 + }, 205 277 "@atproto-labs/did-resolver@0.1.11": { 206 278 "integrity": "sha512-qXNzIX2GPQnxT1gl35nv/8ErDdc4Fj/+RlJE7oyE7JGkFAPUyuY03TvKJ79SmWFsWE8wyTXEpLuphr9Da1Vhkw==", 207 279 "dependencies": [ ··· 334 406 "@atproto/lexicon", 335 407 "@atproto/syntax", 336 408 "chalk", 337 - "commander", 409 + "commander@9.5.0", 338 410 "prettier", 339 411 "ts-morph", 340 412 "yesno", ··· 611 683 "node-addon-api" 612 684 ] 613 685 }, 686 + "@skyware/jetstream@0.2.2": { 687 + "integrity": "sha512-d1MtWPTIFEciSzV8OClXZCJoz0DJ7aupt4EZSwpGAASYG0ZIPmZTt7RVJkoFzQyqRPHAMD7CvEwu0ut3MHX1og==", 688 + "dependencies": [ 689 + "@atcute/bluesky", 690 + "partysocket" 691 + ] 692 + }, 614 693 "@tailwindcss/cli@4.1.4": { 615 694 "integrity": "sha512-gP05Qihh+cZ2FqD5fa0WJXx3KEk2YWUYv/RBKAyiOg0V4vYVDr/xlLc0sacpnVEXM45BVUR9U2hsESufYs6YTA==", 616 695 "dependencies": [ ··· 856 935 "color-convert", 857 936 "color-string" 858 937 ] 938 + }, 939 + "commander@8.3.0": { 940 + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" 859 941 }, 860 942 "commander@9.5.0": { 861 943 "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==" ··· 884 966 "ms@2.0.0" 885 967 ] 886 968 }, 969 + "deepmerge@4.3.1": { 970 + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" 971 + }, 887 972 "depd@2.0.0": { 888 973 "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 889 974 }, ··· 896 981 "detect-libc@2.0.3": { 897 982 "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" 898 983 }, 984 + "dom-serializer@2.0.0": { 985 + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 986 + "dependencies": [ 987 + "domelementtype", 988 + "domhandler", 989 + "entities" 990 + ] 991 + }, 992 + "domelementtype@2.3.0": { 993 + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" 994 + }, 995 + "domhandler@5.0.3": { 996 + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 997 + "dependencies": [ 998 + "domelementtype" 999 + ] 1000 + }, 1001 + "domutils@3.2.2": { 1002 + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", 1003 + "dependencies": [ 1004 + "dom-serializer", 1005 + "domelementtype", 1006 + "domhandler" 1007 + ] 1008 + }, 899 1009 "dunder-proto@1.0.1": { 900 1010 "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 901 1011 "dependencies": [ ··· 919 1029 "graceful-fs", 920 1030 "tapable" 921 1031 ] 1032 + }, 1033 + "entities@4.5.0": { 1034 + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" 922 1035 }, 923 1036 "es-define-property@1.0.1": { 924 1037 "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" ··· 935 1048 "escape-html@1.0.3": { 936 1049 "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 937 1050 }, 1051 + "escape-string-regexp@4.0.0": { 1052 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" 1053 + }, 938 1054 "etag@1.8.1": { 939 1055 "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" 1056 + }, 1057 + "event-target-polyfill@0.0.4": { 1058 + "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==" 940 1059 }, 941 1060 "event-target-shim@5.0.1": { 942 1061 "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" ··· 1044 1163 "es-object-atoms" 1045 1164 ] 1046 1165 }, 1166 + "github-slugger@2.0.0": { 1167 + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" 1168 + }, 1047 1169 "gopd@1.2.0": { 1048 1170 "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" 1049 1171 }, ··· 1065 1187 "function-bind" 1066 1188 ] 1067 1189 }, 1190 + "he@1.2.0": { 1191 + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" 1192 + }, 1068 1193 "hey-listen@1.0.8": { 1069 1194 "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" 1070 1195 }, 1196 + "htmlparser2@8.0.2": { 1197 + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", 1198 + "dependencies": [ 1199 + "domelementtype", 1200 + "domhandler", 1201 + "domutils", 1202 + "entities" 1203 + ] 1204 + }, 1071 1205 "http-errors@2.0.0": { 1072 1206 "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 1073 1207 "dependencies": [ ··· 1111 1245 "is-number@7.0.0": { 1112 1246 "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 1113 1247 }, 1248 + "is-plain-object@5.0.0": { 1249 + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" 1250 + }, 1114 1251 "iso-datestring-validator@2.2.2": { 1115 1252 "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" 1116 1253 }, ··· 1119 1256 }, 1120 1257 "jose@5.9.6": { 1121 1258 "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==" 1259 + }, 1260 + "katex@0.16.22": { 1261 + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", 1262 + "dependencies": [ 1263 + "commander@8.3.0" 1264 + ] 1122 1265 }, 1123 1266 "lightningcss-darwin-arm64@1.29.2": { 1124 1267 "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==" ··· 1169 1312 "lru-cache@10.4.3": { 1170 1313 "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" 1171 1314 }, 1315 + "marked-alert@2.1.2_marked@12.0.2": { 1316 + "integrity": "sha512-EFNRZ08d8L/iEIPLTlQMDjvwIsj03gxWCczYTht6DCiHJIZhMk4NK5gtPY9UqAYb09eV5VGT+jD4lp396E0I+w==", 1317 + "dependencies": [ 1318 + "marked" 1319 + ] 1320 + }, 1321 + "marked-footnote@1.2.4_marked@12.0.2": { 1322 + "integrity": "sha512-DB2Kl+wFh6YwZd70qABMY6WUkG1UuyqoNTFoDfGyG79Pz24neYtLBkB+45a7o72V7gkfvbC3CGzIYFobxfMT1Q==", 1323 + "dependencies": [ 1324 + "marked" 1325 + ] 1326 + }, 1327 + "marked-gfm-heading-id@3.2.0_marked@12.0.2": { 1328 + "integrity": "sha512-Xfxpr5lXLDLY10XqzSCA9l2dDaiabQUgtYM9hw8yunyVsB/xYBRpiic6BOiY/EAJw1ik1eWr1ET1HKOAPZBhXg==", 1329 + "dependencies": [ 1330 + "github-slugger", 1331 + "marked" 1332 + ] 1333 + }, 1334 + "marked@12.0.2": { 1335 + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==" 1336 + }, 1172 1337 "math-intrinsics@1.1.0": { 1173 1338 "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" 1174 1339 }, ··· 1221 1386 "multiformats@9.9.0": { 1222 1387 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" 1223 1388 }, 1389 + "nanoid@3.3.11": { 1390 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" 1391 + }, 1224 1392 "negotiator@0.6.3": { 1225 1393 "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 1226 1394 }, ··· 1245 1413 "ee-first" 1246 1414 ] 1247 1415 }, 1416 + "parse-srcset@1.0.2": { 1417 + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" 1418 + }, 1248 1419 "parseurl@1.3.3": { 1249 1420 "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1250 1421 }, 1422 + "partysocket@1.1.3": { 1423 + "integrity": "sha512-87Jd/nqPoWnVfzHE6Z12WLWTJ+TAgxs0b7i2S163HfQSrVDUK5tW/FC64T5N8L5ss+gqF+EV0BwjZMWggMY3UA==", 1424 + "dependencies": [ 1425 + "event-target-polyfill" 1426 + ] 1427 + }, 1251 1428 "path-browserify@1.0.1": { 1252 1429 "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" 1253 1430 }, ··· 1298 1475 "tslib@2.4.0" 1299 1476 ] 1300 1477 }, 1478 + "postcss@8.5.3": { 1479 + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", 1480 + "dependencies": [ 1481 + "nanoid", 1482 + "picocolors", 1483 + "source-map-js" 1484 + ] 1485 + }, 1301 1486 "preact-render-to-string@6.5.13_preact@10.26.5": { 1302 1487 "integrity": "sha512-iGPd+hKPMFKsfpR2vL4kJ6ZPcFIoWZEcBf0Dpm3zOpdVvj77aY8RlLiQji5OMrngEyaxGogeakTb54uS2FvA6w==", 1303 1488 "dependencies": [ ··· 1309 1494 }, 1310 1495 "prettier@3.5.3": { 1311 1496 "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==" 1497 + }, 1498 + "prismjs@1.30.0": { 1499 + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==" 1312 1500 }, 1313 1501 "process-warning@3.0.0": { 1314 1502 "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" ··· 1378 1566 "safer-buffer@2.1.2": { 1379 1567 "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1380 1568 }, 1569 + "sanitize-html@2.15.0": { 1570 + "integrity": "sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==", 1571 + "dependencies": [ 1572 + "deepmerge", 1573 + "escape-string-regexp", 1574 + "htmlparser2", 1575 + "is-plain-object", 1576 + "parse-srcset", 1577 + "postcss" 1578 + ] 1579 + }, 1381 1580 "semver@7.7.1": { 1382 1581 "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==" 1383 1582 }, ··· 1486 1685 "dependencies": [ 1487 1686 "atomic-sleep" 1488 1687 ] 1688 + }, 1689 + "source-map-js@1.2.1": { 1690 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" 1489 1691 }, 1490 1692 "split2@4.2.0": { 1491 1693 "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" ··· 1608 1810 }, 1609 1811 "workspace": { 1610 1812 "dependencies": [ 1611 - "jsr:@bigmoves/bff@0.3.0-beta.11", 1813 + "jsr:@bigmoves/bff@0.3.0-beta.14", 1612 1814 "jsr:@gfx/canvas@~0.5.8", 1613 1815 "jsr:@std/path@^1.0.9", 1614 1816 "npm:@atproto/syntax@0.4",
+2 -10
input.css
··· 1 1 @import "tailwindcss"; 2 2 3 - .htmx-request.htmx-indicator { 4 - display: inline; 5 - } 6 - .htmx-indicator { 7 - display: none; 8 - } 9 - .htmx-request #submit-button { 10 - opacity: 0.5; 11 - pointer-events: none; 12 - } 3 + /* use to test light mode */ 4 + /* @custom-variant dark (&:where(.dark, .dark *)); */
+305 -156
main.tsx
··· 46 46 } from "@bigmoves/bff/components"; 47 47 import { createCanvas, Image } from "@gfx/canvas"; 48 48 import { join } from "@std/path"; 49 - import { formatDistanceStrict } from "date-fns"; 49 + import { 50 + differenceInDays, 51 + differenceInHours, 52 + differenceInMinutes, 53 + differenceInWeeks, 54 + } from "date-fns"; 50 55 import { wrap } from "popmotion"; 51 56 import { ComponentChildren, JSX, VNode } from "preact"; 52 57 ··· 54 59 const GOATCOUNTER_URL = Deno.env.get("GOATCOUNTER_URL"); 55 60 56 61 let cssContentHash: string = ""; 62 + const staticJsFiles = new Map<string, string>(); 57 63 58 64 bff({ 59 65 appName: "Grain Social", ··· 75 81 cssContentHash = Array.from(new Uint8Array(hashBuffer)) 76 82 .map((b) => b.toString(16).padStart(2, "0")) 77 83 .join(""); 84 + for (const entry of Deno.readDirSync(join(Deno.cwd(), "static"))) { 85 + if (entry.isFile && entry.name.endsWith(".js")) { 86 + const fileContent = await Deno.readFile( 87 + join(Deno.cwd(), "static", entry.name), 88 + ); 89 + const hashBuffer = await crypto.subtle.digest("SHA-256", fileContent); 90 + const hash = Array.from(new Uint8Array(hashBuffer)) 91 + .map((b) => b.toString(16).padStart(2, "0")) 92 + .join(""); 93 + staticJsFiles.set(entry.name, hash); 94 + } 95 + } 78 96 }, 79 97 onError: (err) => { 80 98 if (err instanceof UnauthorizedError) { ··· 102 120 <div 103 121 id="login" 104 122 class="flex justify-center items-center w-full h-full relative" 105 - style="background-image: url('https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@webp'); background-size: cover; background-position: center;" 123 + style="background-image: url('https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg'); background-size: cover; background-position: center;" 106 124 > 107 125 <Login hx-target="#login" error={error} errorClass="text-white" /> 108 126 <div class="absolute bottom-2 right-2 text-white text-sm"> ··· 134 152 if (!profile) return ctx.next(); 135 153 let follow: WithBffMeta<BskyFollow> | undefined; 136 154 if (ctx.currentUser) { 137 - follow = getFollow( 138 - profile.did, 139 - ctx.currentUser.did, 140 - ctx, 141 - ); 155 + follow = getFollow(profile.did, ctx.currentUser.did, ctx); 142 156 } 143 157 ctx.state.meta = [ 144 158 { ··· 214 228 createdAt: new Date().toISOString(), 215 229 }, 216 230 ); 217 - return ctx.html( 218 - <FollowButton followeeDid={did} followUri={followUri} />, 219 - ); 231 + return ctx.html(<FollowButton followeeDid={did} followUri={followUri} />); 220 232 }), 221 233 route("/follow/:did/:rkey", ["DELETE"], async (_req, params, ctx) => { 222 234 requireAuth(ctx); ··· 226 238 await ctx.deleteRecord( 227 239 `at://${ctx.currentUser.did}/app.bsky.graph.follow/${rkey}`, 228 240 ); 229 - return ctx.html( 230 - <FollowButton followeeDid={did} followUri={undefined} />, 231 - ); 241 + return ctx.html(<FollowButton followeeDid={did} followUri={undefined} />); 232 242 }), 233 243 route("/dialogs/gallery/new", (_req, _params, ctx) => { 234 244 requireAuth(ctx); ··· 308 318 />, 309 319 ); 310 320 }), 311 - route("/dialogs/image-alt", (req, _params, ctx) => { 312 - const url = new URL(req.url); 313 - const galleryUri = url.searchParams.get("galleryUri"); 314 - const imageCid = url.searchParams.get("imageCid"); 315 - if (!galleryUri || !imageCid) return ctx.next(); 316 - const atUri = new AtUri(galleryUri); 317 - const galleryDid = atUri.hostname; 318 - const galleryRkey = atUri.rkey; 319 - const gallery = getGallery(galleryDid, galleryRkey, ctx); 320 - const photo = gallery?.items?.filter(isPhotoView).find((photo) => { 321 - return photo.cid === imageCid; 322 - }); 323 - if (!photo || !gallery) return ctx.next(); 321 + route("/dialogs/photo/:rkey/alt", (_req, params, ctx) => { 322 + requireAuth(ctx); 323 + const photoRkey = params.rkey; 324 + const photoUri = 325 + `at://${ctx.currentUser.did}/social.grain.photo/${photoRkey}`; 326 + const photo = ctx.indexService.getRecord<WithBffMeta<Photo>>(photoUri); 327 + if (!photo) return ctx.next(); 324 328 return ctx.html( 325 - <PhotoAltDialog galleryUri={gallery.uri} photo={photo} />, 329 + <PhotoAltDialog photo={photoToView(ctx.currentUser.did, photo)} />, 326 330 ); 327 331 }), 328 332 route("/dialogs/photo-select/:galleryRkey", (_req, params, ctx) => { ··· 430 434 key={photo.cid} 431 435 photo={photoToView(photo.did, photo)} 432 436 gallery={gallery} 433 - isCreator={ctx.currentUser.did === gallery.creator.did} 434 - isLoggedIn={!!ctx.currentUser.did} 435 437 /> 436 438 </div> 437 439 <PhotoSelectButton ··· 508 510 }); 509 511 return new Response(null, { status: 200 }); 510 512 }), 513 + route("/actions/photo/:rkey", ["DELETE"], (_req, params, ctx) => { 514 + requireAuth(ctx); 515 + ctx.deleteRecord( 516 + `at://${ctx.currentUser.did}/social.grain.photo/${params.rkey}`, 517 + ); 518 + return new Response(null, { status: 200 }); 519 + }), 511 520 route("/actions/favorite", ["POST"], async (req, _params, ctx) => { 512 521 requireAuth(ctx); 513 522 const url = new URL(req.url); ··· 566 575 567 576 return ctx.redirect(`/profile/${ctx.currentUser.handle}`); 568 577 }), 569 - route("/actions/photo/:rkey", ["DELETE"], (_req, params, ctx) => { 570 - requireAuth(ctx); 571 - ctx.deleteRecord( 572 - `at://${ctx.currentUser.did}/social.grain.photo/${params.rkey}`, 573 - ); 574 - return new Response(null, { status: 200 }); 575 - }), 576 578 route("/actions/sort-end", ["POST"], async (req, _params, ctx) => { 577 579 const formData = await req.formData(); 578 580 const items = formData.getAll("item") as string[]; ··· 663 665 }; 664 666 665 667 function getFollow(followeeDid: string, followerDid: string, ctx: BffContext) { 666 - const { items: [follow] } = ctx.indexService.getRecords< 667 - WithBffMeta<BskyFollow> 668 - >( 668 + const { 669 + items: [follow], 670 + } = ctx.indexService.getRecords<WithBffMeta<BskyFollow>>( 669 671 "app.bsky.graph.follow", 670 672 { 671 673 where: [ ··· 1069 1071 href="https://unpkg.com/@fortawesome/fontawesome-free@6.7.2/css/all.min.css" 1070 1072 preload 1071 1073 /> 1072 - {scripts?.map((file) => <script key={file} src={`/static/${file}`} />)} 1074 + {scripts?.map((file) => ( 1075 + <script 1076 + key={file} 1077 + src={`/static/${file}?${staticJsFiles.get(file)}`} 1078 + /> 1079 + ))} 1073 1080 </head> 1074 1081 <body class="h-full w-full dark:bg-zinc-950 dark:text-white"> 1075 - <Layout id="layout" class="dark:border-zinc-800"> 1082 + <Layout id="layout" class="border-zinc-200 dark:border-zinc-800"> 1076 1083 <Layout.Nav 1077 1084 heading={ 1078 1085 <h1 class="font-['Jersey_20'] text-4xl text-zinc-900 dark:text-white"> ··· 1081 1088 </h1> 1082 1089 } 1083 1090 profile={profile} 1084 - class="dark:border-zinc-800" 1091 + class="border-zinc-200 dark:border-zinc-800" 1085 1092 /> 1086 1093 <Layout.Content>{props.children}</Layout.Content> 1087 1094 </Layout> ··· 1144 1151 ); 1145 1152 } 1146 1153 1154 + function ActorInfo({ profile }: Readonly<{ profile: Un$Typed<ProfileView> }>) { 1155 + return ( 1156 + <div class="flex items-center gap-2 min-w-0 flex-1"> 1157 + <img 1158 + src={profile.avatar} 1159 + alt={profile.handle} 1160 + class="rounded-full object-cover size-7 shrink-0" 1161 + /> 1162 + <a 1163 + href={profileLink(profile.handle)} 1164 + class="hover:underline text-zinc-600 dark:text-zinc-500 truncate max-w-[300px] sm:max-w-[400px]" 1165 + > 1166 + <span class="text-zinc-950 dark:text-zinc-50 font-semibold text-"> 1167 + {profile.displayName || profile.handle} 1168 + </span>{" "} 1169 + <span class="truncate">@{profile.handle}</span> 1170 + </a> 1171 + </div> 1172 + ); 1173 + } 1174 + 1147 1175 function Timeline({ items }: Readonly<{ items: TimelineItem[] }>) { 1148 1176 return ( 1149 1177 <div class="px-4 mb-4"> ··· 1159 1187 1160 1188 function TimelineItem({ item }: Readonly<{ item: TimelineItem }>) { 1161 1189 return ( 1162 - <li class="space-y-2"> 1163 - <div class="bg-zinc-100 dark:bg-zinc-900 w-fit p-2"> 1164 - <a 1165 - href={profileLink(item.actor.handle)} 1166 - class="font-semibold hover:underline" 1167 - > 1168 - @{item.actor.handle} 1169 - </a>{" "} 1170 - {item.itemType === "favorite" ? "favorited" : "created"}{" "} 1171 - <a 1172 - href={galleryLink( 1173 - item.gallery.creator.handle, 1174 - new AtUri(item.gallery.uri).rkey, 1175 - )} 1176 - class="font-semibold hover:underline" 1177 - > 1178 - {(item.gallery.record as Gallery).title} 1179 - </a> 1180 - <span class="ml-1"> 1181 - {formatDistanceStrict(item.createdAt, new Date(), { 1182 - addSuffix: true, 1183 - })} 1184 - </span> 1185 - </div> 1186 - <a 1187 - href={galleryLink( 1188 - item.gallery.creator.handle, 1189 - new AtUri(item.gallery.uri).rkey, 1190 - )} 1191 - class="w-fit flex" 1192 - > 1190 + <li> 1191 + <div class="w-fit flex flex-col gap-4 pb-4 border-b border-zinc-200 dark:border-zinc-800"> 1192 + <div class="flex items-center justify-between gap-2 w-full"> 1193 + <ActorInfo profile={item.actor} /> 1194 + <span class="shrink-0"> 1195 + {formatRelativeTime(new Date(item.createdAt))} 1196 + </span> 1197 + </div> 1193 1198 {item.gallery.items?.filter(isPhotoView).length 1194 1199 ? ( 1195 - <div class="flex w-full max-w-md mx-auto aspect-[3/2] overflow-hidden gap-2"> 1200 + <a 1201 + href={galleryLink( 1202 + item.gallery.creator.handle, 1203 + new AtUri(item.gallery.uri).rkey, 1204 + )} 1205 + class="flex w-full max-w-md mx-auto aspect-[3/2] overflow-hidden gap-2" 1206 + > 1196 1207 <div class="w-2/3 h-full"> 1197 1208 <img 1198 1209 src={item.gallery.items?.filter(isPhotoView)[0].thumb} ··· 1230 1241 )} 1231 1242 </div> 1232 1243 </div> 1233 - </div> 1244 + </a> 1234 1245 ) 1235 1246 : null} 1236 - </a> 1247 + <p> 1248 + {item.itemType === "favorite" ? "Favorited" : "Created"}{" "} 1249 + <a 1250 + href={galleryLink( 1251 + item.gallery.creator.handle, 1252 + new AtUri(item.gallery.uri).rkey, 1253 + )} 1254 + class="font-semibold hover:underline" 1255 + > 1256 + {(item.gallery.record as Gallery).title} 1257 + </a> 1258 + </p> 1259 + </div> 1237 1260 </li> 1238 1261 ); 1239 1262 } ··· 1259 1282 : { 1260 1283 children: ( 1261 1284 <> 1262 - <i class="fa-solid fa-plus mr-2" />Follow 1285 + <i class="fa-solid fa-plus mr-2" /> 1286 + Follow 1263 1287 </> 1264 1288 ), 1265 1289 "hx-post": `/follow/${followeeDid}`, ··· 1271 1295 ); 1272 1296 } 1273 1297 1298 + function formatRelativeTime(date: Date) { 1299 + const now = new Date(); 1300 + const weeks = differenceInWeeks(now, date); 1301 + if (weeks > 0) return `${weeks}w`; 1302 + 1303 + const days = differenceInDays(now, date); 1304 + if (days > 0) return `${days}d`; 1305 + 1306 + const hours = differenceInHours(now, date); 1307 + if (hours > 0) return `${hours}h`; 1308 + 1309 + const minutes = differenceInMinutes(now, date); 1310 + return `${Math.max(1, minutes)}m`; 1311 + } 1312 + 1274 1313 function ProfilePage({ 1275 1314 followUri, 1276 1315 loggedInUserDid, ··· 1287 1326 galleries?: GalleryView[]; 1288 1327 }>) { 1289 1328 const isCreator = loggedInUserDid === profile.did; 1329 + const displayName = profile.displayName || profile.handle; 1290 1330 return ( 1291 1331 <div class="px-4 mb-4" id="profile-page"> 1292 1332 <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4"> 1293 - <div class="flex flex-col"> 1333 + <div class="flex flex-col mb-4"> 1294 1334 <AvatarButton profile={profile} /> 1295 - <p class="text-2xl font-bold">{profile.displayName}</p> 1335 + <p class="text-2xl font-bold">{displayName}</p> 1296 1336 <p class="text-zinc-600 dark:text-zinc-500">@{profile.handle}</p> 1297 - <p class="my-2">{profile.description}</p> 1337 + {profile.description 1338 + ? <p class="mt-2">{profile.description}</p> 1339 + : null} 1298 1340 </div> 1299 1341 {!isCreator && loggedInUserDid 1300 1342 ? ( ··· 1384 1426 : null} 1385 1427 {selectedTab === "galleries" 1386 1428 ? ( 1387 - <div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4"> 1429 + <div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4"> 1388 1430 {galleries?.length 1389 1431 ? ( 1390 1432 galleries.map((gallery) => ( ··· 1398 1440 {gallery.items?.length 1399 1441 ? ( 1400 1442 <img 1401 - src={gallery.items?.filter(isPhotoView)?.[0]?.thumb} 1443 + src={gallery.items?.filter(isPhotoView)?.[0] 1444 + ?.fullsize} 1402 1445 alt={gallery.items?.filter(isPhotoView)?.[0]?.alt} 1403 1446 class="w-full h-full object-cover" 1404 1447 /> ··· 1421 1464 ); 1422 1465 } 1423 1466 1424 - function UploadPage( 1425 - { handle, photos, returnTo }: Readonly< 1426 - { handle: string; photos: PhotoView[]; returnTo?: string } 1427 - >, 1428 - ) { 1467 + function UploadPage({ 1468 + handle, 1469 + photos, 1470 + returnTo, 1471 + }: Readonly<{ handle: string; photos: PhotoView[]; returnTo?: string }>) { 1429 1472 return ( 1430 1473 <div class="flex flex-col px-4 pt-4 mb-4 space-y-4"> 1431 1474 <div class="flex"> 1432 1475 <div class="flex-1"> 1433 1476 {returnTo 1434 1477 ? ( 1435 - <a 1436 - href={returnTo} 1437 - class="hover:underline" 1438 - > 1478 + <a href={returnTo} class="hover:underline"> 1439 1479 <i class="fa-solid fa-arrow-left mr-2" /> 1440 1480 Back to gallery 1441 1481 </a> ··· 1447 1487 </a> 1448 1488 )} 1449 1489 </div> 1450 - <div>10/100 photos</div> 1451 1490 </div> 1452 - <Button variant="primary" class="mb-4" asChild> 1453 - <label class="w-fit"> 1491 + <Button variant="primary" class="mb-4 w-full sm:w-fit" asChild> 1492 + <label> 1454 1493 <i class="fa fa-plus"></i> Add photos 1455 1494 <input 1456 1495 class="hidden" ··· 1495 1534 }>) { 1496 1535 return ( 1497 1536 <Dialog> 1498 - <Dialog.Content class="dark:bg-zinc-950"> 1537 + <Dialog.Content class="dark:bg-zinc-950 relative"> 1538 + <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 1499 1539 <Dialog.Title>Edit my profile</Dialog.Title> 1500 1540 <div> 1501 1541 <AvatarForm src={profile.avatar} alt={profile.handle} /> ··· 1517 1557 name="displayName" 1518 1558 class="dark:bg-zinc-800 dark:text-white" 1519 1559 value={profile.displayName} 1560 + autoFocus 1520 1561 /> 1521 1562 </div> 1522 1563 <div class="mb-4 relative"> ··· 1598 1639 }>) { 1599 1640 const isCreator = currentUserDid === gallery.creator.did; 1600 1641 const isLoggedIn = !!currentUserDid; 1642 + const description = (gallery.record as Gallery).description; 1601 1643 return ( 1602 1644 <div class="px-4"> 1603 - <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4"> 1604 - <div class="flex flex-col space-y-1 mb-4"> 1645 + <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mt-4 mb-2"> 1646 + <div class="flex flex-col space-y-2 mb-4"> 1605 1647 <h1 class="font-bold text-2xl"> 1606 1648 {(gallery.record as Gallery).title} 1607 1649 </h1> 1608 - <div> 1609 - Gallery by{" "} 1610 - <a 1611 - href={profileLink(gallery.creator.handle)} 1612 - class="hover:underline" 1613 - > 1614 - <span class="font-semibold">{gallery.creator.displayName}</span> 1615 - {" "} 1616 - <span class="text-zinc-600 dark:text-zinc-500"> 1617 - @{gallery.creator.handle} 1618 - </span> 1619 - </a> 1620 - </div> 1621 - <p>{(gallery.record as Gallery).description}</p> 1650 + <ActorInfo profile={gallery.creator} /> 1651 + {description ? <p>{description}</p> : null} 1622 1652 </div> 1623 1653 {isLoggedIn && isCreator 1624 1654 ? ( 1625 1655 <div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row"> 1626 1656 <Button 1627 - hx-get={`/dialogs/photo-select/${new AtUri(gallery.uri).rkey}`} 1628 - hx-target="#layout" 1629 - hx-swap="afterbegin" 1630 - variant="primary" 1631 - class="self-start w-full sm:w-fit" 1632 - > 1633 - Add photos 1634 - </Button> 1635 - <Button 1636 1657 variant="primary" 1637 1658 class="self-start w-full sm:w-fit" 1638 1659 hx-get={`/dialogs/gallery/${new AtUri(gallery.uri).rkey}`} ··· 1650 1671 > 1651 1672 Edit 1652 1673 </Button> 1674 + <Button 1675 + hx-get={`/dialogs/photo-select/${new AtUri(gallery.uri).rkey}`} 1676 + hx-target="#layout" 1677 + hx-swap="afterbegin" 1678 + variant="primary" 1679 + class="self-start w-full sm:w-fit" 1680 + > 1681 + Add photos 1682 + </Button> 1683 + <ShareGalleryButton gallery={gallery} /> 1653 1684 </div> 1654 1685 ) 1655 1686 : null} 1656 1687 {!isCreator 1657 1688 ? ( 1658 - <FavoriteButton 1659 - currentUserDid={currentUserDid} 1660 - favs={favs} 1661 - galleryUri={gallery.uri} 1662 - /> 1689 + <div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row"> 1690 + <ShareGalleryButton gallery={gallery} /> 1691 + <FavoriteButton 1692 + currentUserDid={currentUserDid} 1693 + favs={favs} 1694 + galleryUri={gallery.uri} 1695 + /> 1696 + </div> 1663 1697 ) 1664 1698 : null} 1665 1699 </div> 1666 1700 <SortableGrid gallery={gallery} /> 1667 1701 { 1668 1702 /* <div 1703 + <div class="flex justify-end mb-2"> 1704 + <Button 1705 + id="justified-button" 1706 + variant="primary" 1707 + class="flex justify-center w-full sm:w-fit bg-zinc-100 dark:bg-zinc-800 border-zinc-100 dark:border-zinc-800 data-[selected=false]:bg-transparent data-[selected=false]:border-transparent text-zinc-950 dark:text-zinc-50" 1708 + _="on click call toggleLayout('justified') 1709 + set @data-selected to 'true' 1710 + set #masonry-button's @data-selected to 'false'" 1711 + > 1712 + <svg 1713 + width="24" 1714 + height="24" 1715 + viewBox="0 0 24 24" 1716 + xmlns="http://www.w3.org/2000/svg" 1717 + > 1718 + <rect x="2" y="2" width="8" height="6" fill="currentColor" rx="1" /> 1719 + <rect 1720 + x="12" 1721 + y="2" 1722 + width="10" 1723 + height="6" 1724 + fill="currentColor" 1725 + rx="1" 1726 + /> 1727 + <rect 1728 + x="2" 1729 + y="10" 1730 + width="6" 1731 + height="6" 1732 + fill="currentColor" 1733 + rx="1" 1734 + /> 1735 + <rect 1736 + x="10" 1737 + y="10" 1738 + width="12" 1739 + height="6" 1740 + fill="currentColor" 1741 + rx="1" 1742 + /> 1743 + <rect 1744 + x="2" 1745 + y="18" 1746 + width="20" 1747 + height="4" 1748 + fill="currentColor" 1749 + rx="1" 1750 + /> 1751 + </svg> 1752 + </Button> 1753 + <Button 1754 + id="masonry-button" 1755 + variant="primary" 1756 + data-selected="false" 1757 + class="flex justify-center w-full sm:w-fit bg-zinc-100 dark:bg-zinc-800 border-zinc-100 dark:border-zinc-800 data-[selected=false]:bg-transparent data-[selected=false]:border-transparent text-zinc-950 dark:text-zinc-50" 1758 + _="on click call toggleLayout('masonry') 1759 + set @data-selected to 'true' 1760 + set #justified-button's @data-selected to 'false'" 1761 + > 1762 + <svg 1763 + width="24" 1764 + height="24" 1765 + viewBox="0 0 24 24" 1766 + xmlns="http://www.w3.org/2000/svg" 1767 + > 1768 + <rect x="2" y="2" width="8" height="8" fill="currentColor" rx="1" /> 1769 + <rect 1770 + x="12" 1771 + y="2" 1772 + width="8" 1773 + height="4" 1774 + fill="currentColor" 1775 + rx="1" 1776 + /> 1777 + <rect 1778 + x="12" 1779 + y="8" 1780 + width="8" 1781 + height="6" 1782 + fill="currentColor" 1783 + rx="1" 1784 + /> 1785 + <rect 1786 + x="2" 1787 + y="12" 1788 + width="8" 1789 + height="8" 1790 + fill="currentColor" 1791 + rx="1" 1792 + /> 1793 + <rect 1794 + x="12" 1795 + y="16" 1796 + width="8" 1797 + height="4" 1798 + fill="currentColor" 1799 + rx="1" 1800 + /> 1801 + </svg> 1802 + </Button> 1803 + </div> 1804 + <div 1669 1805 id="masonry-container" 1670 1806 class="h-0 overflow-hidden relative mx-auto w-full" 1671 - _="on load or htmx:afterSettle call computeMasonry()" 1807 + _="on load or htmx:afterSettle call computeLayout()" 1672 1808 > 1673 1809 {gallery.items?.filter(isPhotoView)?.length 1674 1810 ? gallery?.items ··· 1678 1814 key={photo.cid} 1679 1815 photo={photo} 1680 1816 gallery={gallery} 1681 - isCreator={isCreator} 1682 - isLoggedIn={isLoggedIn} 1683 1817 /> 1684 1818 )) 1685 1819 : null} ··· 1692 1826 function PhotoButton({ 1693 1827 photo, 1694 1828 gallery, 1695 - isCreator, 1696 - isLoggedIn, 1697 1829 }: Readonly<{ 1698 1830 photo: PhotoView; 1699 1831 gallery: GalleryView; 1700 - isCreator: boolean; 1701 - isLoggedIn: boolean; 1702 1832 }>) { 1703 1833 return ( 1704 1834 <button ··· 1712 1842 data-width={photo.aspectRatio?.width} 1713 1843 data-height={photo.aspectRatio?.height} 1714 1844 > 1715 - {isLoggedIn && isCreator 1716 - ? <AltTextButton galleryUri={gallery.uri} cid={photo.cid} /> 1717 - : null} 1718 1845 <img 1719 1846 src={photo.fullsize} 1720 1847 alt={photo.alt} 1721 1848 class="w-full h-full object-cover" 1722 1849 /> 1723 - {!isCreator && photo.alt 1850 + {photo.alt 1724 1851 ? ( 1725 1852 <div class="absolute bg-zinc-950 dark:bg-zinc-900 bottom-1 right-1 sm:bottom-1 sm:right-1 text-xs text-white font-semibold py-[1px] px-[3px]"> 1726 1853 ALT ··· 1731 1858 ); 1732 1859 } 1733 1860 1734 - function SortableGrid({ 1735 - gallery, 1736 - }: Readonly<{ gallery: GalleryView }>) { 1861 + function SortableGrid({ gallery }: Readonly<{ gallery: GalleryView }>) { 1737 1862 return ( 1738 1863 <form 1739 1864 id="masonry-container" ··· 1763 1888 ); 1764 1889 } 1765 1890 1891 + function ShareGalleryButton({ gallery }: Readonly<{ gallery: GalleryView }>) { 1892 + return ( 1893 + <> 1894 + <input 1895 + type="hidden" 1896 + id="copy-text" 1897 + value={publicGalleryLink(gallery.creator.handle, gallery.uri)} 1898 + /> 1899 + <Button 1900 + variant="primary" 1901 + _={`on click 1902 + set copyText to #copy-text.value 1903 + writeText(copyText) on navigator.clipboard 1904 + alert('Copied to clipboard')`} 1905 + > 1906 + <i class="fa-solid fa-share-nodes mr-2" /> 1907 + Share 1908 + </Button> 1909 + </> 1910 + ); 1911 + } 1912 + 1766 1913 function FavoriteButton({ 1767 1914 currentUserDid, 1768 1915 favs = [], ··· 1795 1942 }: Readonly<{ gallery?: GalleryView | null }>) { 1796 1943 return ( 1797 1944 <Dialog id="gallery-dialog" class="z-30"> 1798 - <Dialog.Content class="dark:bg-zinc-950"> 1945 + <Dialog.Content class="dark:bg-zinc-950 relative"> 1946 + <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 1799 1947 <Dialog.Title> 1800 1948 {gallery ? "Edit gallery" : "Create a new gallery"} 1801 1949 </Dialog.Title> ··· 1892 2040 }>) { 1893 2041 return ( 1894 2042 <div class="relative aspect-square bg-zinc-200 dark:bg-zinc-900"> 2043 + {uri ? <AltTextButton photoUri={uri} /> : null} 1895 2044 {uri 1896 2045 ? ( 1897 2046 <button ··· 1914 2063 ); 1915 2064 } 1916 2065 1917 - function AltTextButton({ 1918 - galleryUri, 1919 - cid, 1920 - }: Readonly<{ galleryUri: string; cid: string }>) { 2066 + function AltTextButton({ photoUri }: Readonly<{ photoUri: string }>) { 1921 2067 return ( 1922 2068 <div 1923 - class="bg-zinc-950 dark:bg-zinc-900 py-[1px] px-[3px] absolute top-1 left-1 sm:top-1 sm:left-1 cursor-pointer flex items-center justify-center text-xs text-white font-semibold z-10" 1924 - hx-get={`/dialogs/image-alt?galleryUri=${galleryUri}&imageCid=${cid}`} 2069 + class="bg-zinc-950 dark:bg-zinc-950 py-[1px] px-[3px] absolute top-2 left-2 cursor-pointer flex items-center justify-center text-xs text-white font-semibold z-10" 2070 + hx-get={`/dialogs/photo/${new AtUri(photoUri).rkey}/alt`} 1925 2071 hx-trigger="click" 1926 2072 hx-target="#layout" 1927 2073 hx-swap="afterbegin" ··· 1945 2091 }>) { 1946 2092 return ( 1947 2093 <Dialog id="photo-dialog" class="bg-zinc-950 z-30"> 2094 + <Dialog.X /> 1948 2095 {nextImage 1949 2096 ? ( 1950 2097 <div ··· 1990 2137 1991 2138 function PhotoAltDialog({ 1992 2139 photo, 1993 - galleryUri, 1994 2140 }: Readonly<{ 1995 2141 photo: PhotoView; 1996 - galleryUri: string; 1997 2142 }>) { 1998 2143 return ( 1999 2144 <Dialog id="photo-alt-dialog" class="z-30"> 2000 - <Dialog.Content class="dark:bg-zinc-950"> 2145 + <Dialog.Content class="dark:bg-zinc-950 relative"> 2146 + <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 2001 2147 <Dialog.Title>Add alt text</Dialog.Title> 2002 2148 <div class="aspect-square relative"> 2003 2149 <img ··· 2010 2156 hx-put={`/actions/photo/${new AtUri(photo.uri).rkey}`} 2011 2157 _="on htmx:afterOnLoad trigger closeDialog" 2012 2158 > 2013 - <input type="hidden" name="galleryUri" value={galleryUri} /> 2014 - <input type="hidden" name="cid" value={photo.cid} /> 2015 2159 <div class="my-2"> 2016 2160 <label htmlFor="alt">Descriptive alt text</label> 2017 2161 <Textarea ··· 2047 2191 }>) { 2048 2192 return ( 2049 2193 <Dialog id="photo-select-dialog" class="z-30"> 2050 - <Dialog.Content class="w-full max-w-5xl dark:bg-zinc-950 sm:min-h-screen flex flex-col"> 2194 + <Dialog.Content class="w-full max-w-5xl dark:bg-zinc-950 sm:min-h-screen flex flex-col relative"> 2195 + <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 2051 2196 <Dialog.Title>Add photos</Dialog.Title> 2052 2197 {photos.length 2053 2198 ? ( ··· 2059 2204 : null} 2060 2205 {photos.length 2061 2206 ? ( 2062 - <div class="grid grid-cols-2 sm:grid-cols-3 gap-4 my-4 flex-1"> 2207 + <div class="grid grid-cols-3 sm:grid-cols-5 gap-4 my-4 flex-1"> 2063 2208 {photos.map((photo) => ( 2064 2209 <PhotoSelectButton 2065 2210 key={photo.cid} ··· 2120 2265 set @data-added to 'true' 2121 2266 end`} 2122 2267 > 2123 - <div class="hidden group-data-[added=true]:block absolute top-2 right-2"> 2268 + <div class="hidden group-data-[added=true]:block absolute top-2 right-2 z-30"> 2124 2269 <i class="fa-check fa-solid text-sky-500 z-10" /> 2125 2270 </div> 2126 2271 <img ··· 2181 2326 uri: photo.uri, 2182 2327 cid: photo.photo.ref.toString(), 2183 2328 thumb: 2184 - `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${photo.photo.ref.toString()}@webp`, 2329 + `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${photo.photo.ref.toString()}@jpeg`, 2185 2330 fullsize: 2186 - `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${photo.photo.ref.toString()}@webp`, 2331 + `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${photo.photo.ref.toString()}@jpeg`, 2187 2332 alt: photo.alt, 2188 2333 aspectRatio: photo.aspectRatio, 2189 2334 }; ··· 2445 2590 ), 2446 2591 ]; 2447 2592 } 2593 + 2594 + function publicGalleryLink(handle: string, galleryUri: string): string { 2595 + return `${PUBLIC_URL}/profile/${handle}/${new AtUri(galleryUri).rkey}`; 2596 + }
+85 -3
static/masonry.js
··· 1 1 // deno-lint-ignore-file 2 2 3 3 let masonryObserverInitialized = false; 4 + let layoutMode = "justified"; 5 + 6 + function computeLayout() { 7 + if (layoutMode === "masonry") { 8 + computeMasonry(); 9 + } else { 10 + computeJustified(); 11 + } 12 + } 13 + 14 + function toggleLayout(layout = "justified") { 15 + layoutMode = layout; 16 + computeLayout(); 17 + } 4 18 5 19 function computeMasonry() { 6 20 const container = document.getElementById("masonry-container"); 7 21 if (!container) return; 8 22 9 - const spacing = 12; 23 + const spacing = 8; 10 24 const containerWidth = container.offsetWidth; 11 25 12 26 if (containerWidth === 0) { ··· 52 66 container.style.height = `${Math.max(...columnHeights)}px`; 53 67 } 54 68 69 + function computeJustified() { 70 + const container = document.getElementById("masonry-container"); 71 + if (!container) return; 72 + 73 + const spacing = 8; 74 + const containerWidth = container.offsetWidth; 75 + 76 + if (containerWidth === 0) { 77 + requestAnimationFrame(computeJustified); 78 + return; 79 + } 80 + 81 + const tiles = Array.from(container.querySelectorAll(".masonry-tile")); 82 + let currentRow = []; 83 + let rowAspectRatioSum = 0; 84 + let yOffset = 0; 85 + 86 + // Clear all styles before layout 87 + tiles.forEach((tile) => { 88 + Object.assign(tile.style, { 89 + position: "absolute", 90 + left: "0px", 91 + top: "0px", 92 + width: "auto", 93 + height: "auto", 94 + }); 95 + }); 96 + 97 + for (let i = 0; i < tiles.length; i++) { 98 + const tile = tiles[i]; 99 + const imgW = parseFloat(tile.dataset.width); 100 + const imgH = parseFloat(tile.dataset.height); 101 + if (!imgW || !imgH) continue; 102 + 103 + const aspectRatio = imgW / imgH; 104 + currentRow.push({ tile, aspectRatio, imgW, imgH }); 105 + rowAspectRatioSum += aspectRatio; 106 + 107 + // Estimate if row is "full" enough 108 + const estimatedRowHeight = 109 + (containerWidth - (currentRow.length - 1) * spacing) / rowAspectRatioSum; 110 + 111 + // If height is reasonable or we're at the end, render the row 112 + if (estimatedRowHeight < 300 || i === tiles.length - 1) { 113 + let xOffset = 0; 114 + 115 + for (const item of currentRow) { 116 + const width = estimatedRowHeight * item.aspectRatio; 117 + Object.assign(item.tile.style, { 118 + position: "absolute", 119 + top: `${yOffset}px`, 120 + left: `${xOffset}px`, 121 + width: `${width}px`, 122 + height: `${estimatedRowHeight}px`, 123 + }); 124 + xOffset += width + spacing; 125 + } 126 + 127 + yOffset += estimatedRowHeight + spacing; 128 + currentRow = []; 129 + rowAspectRatioSum = 0; 130 + } 131 + } 132 + 133 + container.style.position = "relative"; 134 + container.style.height = `${yOffset}px`; 135 + } 136 + 55 137 function observeMasonry() { 56 138 if (masonryObserverInitialized) return; 57 139 masonryObserverInitialized = true; ··· 61 143 62 144 // Observe parent resize 63 145 if (typeof ResizeObserver !== "undefined") { 64 - const resizeObserver = new ResizeObserver(() => computeMasonry()); 146 + const resizeObserver = new ResizeObserver(() => computeLayout()); 65 147 if (container.parentElement) { 66 148 resizeObserver.observe(container.parentElement); 67 149 } ··· 69 151 70 152 // Observe inner content changes (tiles being added/removed) 71 153 const mutationObserver = new MutationObserver(() => { 72 - computeMasonry(); 154 + computeLayout(); 73 155 }); 74 156 75 157 mutationObserver.observe(container, {
+74 -35
static/styles.css
··· 8 8 --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", 9 9 "Courier New", monospace; 10 10 --color-sky-500: oklch(68.5% 0.169 237.323); 11 + --color-zinc-50: oklch(98.5% 0 0); 11 12 --color-zinc-100: oklch(96.7% 0.001 286.375); 12 13 --color-zinc-200: oklch(92% 0.004 286.32); 13 14 --color-zinc-500: oklch(55.2% 0.016 285.938); ··· 206 207 .inset-0 { 207 208 inset: calc(var(--spacing) * 0); 208 209 } 209 - .top-1 { 210 - top: calc(var(--spacing) * 1); 211 - } 212 210 .top-2 { 213 211 top: calc(var(--spacing) * 2); 214 212 } ··· 236 234 .left-0 { 237 235 left: calc(var(--spacing) * 0); 238 236 } 239 - .left-1 { 240 - left: calc(var(--spacing) * 1); 237 + .left-2 { 238 + left: calc(var(--spacing) * 2); 241 239 } 242 240 .z-10 { 243 241 z-index: 10; ··· 281 279 .mt-2 { 282 280 margin-top: calc(var(--spacing) * 2); 283 281 } 282 + .mt-4 { 283 + margin-top: calc(var(--spacing) * 4); 284 + } 284 285 .mr-1 { 285 286 margin-right: calc(var(--spacing) * 1); 286 287 } ··· 292 293 } 293 294 .mb-4 { 294 295 margin-bottom: calc(var(--spacing) * 4); 295 - } 296 - .ml-1 { 297 - margin-left: calc(var(--spacing) * 1); 298 296 } 299 297 .flex { 300 298 display: flex; ··· 315 313 width: calc(var(--spacing) * 4); 316 314 height: calc(var(--spacing) * 4); 317 315 } 316 + .size-7 { 317 + width: calc(var(--spacing) * 7); 318 + height: calc(var(--spacing) * 7); 319 + } 318 320 .size-16 { 319 321 width: calc(var(--spacing) * 16); 320 322 height: calc(var(--spacing) * 16); ··· 367 369 .max-w-5xl { 368 370 max-width: var(--container-5xl); 369 371 } 372 + .max-w-\[300px\] { 373 + max-width: 300px; 374 + } 370 375 .max-w-md { 371 376 max-width: var(--container-md); 372 377 } 373 378 .max-w-xl { 374 379 max-width: var(--container-xl); 375 380 } 381 + .min-w-0 { 382 + min-width: calc(var(--spacing) * 0); 383 + } 376 384 .flex-1 { 377 385 flex: 1; 386 + } 387 + .shrink-0 { 388 + flex-shrink: 0; 378 389 } 379 390 .cursor-pointer { 380 391 cursor: pointer; ··· 388 399 .grid-cols-2 { 389 400 grid-template-columns: repeat(2, minmax(0, 1fr)); 390 401 } 402 + .grid-cols-3 { 403 + grid-template-columns: repeat(3, minmax(0, 1fr)); 404 + } 391 405 .flex-col { 392 406 flex-direction: column; 393 407 } 394 408 .items-center { 395 409 align-items: center; 396 410 } 411 + .justify-between { 412 + justify-content: space-between; 413 + } 397 414 .justify-center { 398 415 justify-content: center; 399 416 } 417 + .justify-end { 418 + justify-content: flex-end; 419 + } 400 420 .gap-2 { 401 421 gap: calc(var(--spacing) * 2); 402 422 } 403 423 .gap-4 { 404 424 gap: calc(var(--spacing) * 4); 405 425 } 406 - .space-y-1 { 407 - :where(& > :not(:last-child)) { 408 - --tw-space-y-reverse: 0; 409 - margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); 410 - margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); 411 - } 412 - } 413 426 .space-y-2 { 414 427 :where(& > :not(:last-child)) { 415 428 --tw-space-y-reverse: 0; ··· 434 447 .self-start { 435 448 align-self: flex-start; 436 449 } 450 + .truncate { 451 + overflow: hidden; 452 + text-overflow: ellipsis; 453 + white-space: nowrap; 454 + } 437 455 .overflow-hidden { 438 456 overflow: hidden; 439 457 } ··· 443 461 .border { 444 462 border-style: var(--tw-border-style); 445 463 border-width: 1px; 464 + } 465 + .border-b { 466 + border-bottom-style: var(--tw-border-style); 467 + border-bottom-width: 1px; 468 + } 469 + .border-zinc-100 { 470 + border-color: var(--color-zinc-100); 446 471 } 447 472 .border-zinc-200 { 448 473 border-color: var(--color-zinc-200); ··· 471 496 .bg-zinc-950 { 472 497 background-color: var(--color-zinc-950); 473 498 } 499 + .fill-zinc-950 { 500 + fill: var(--color-zinc-950); 501 + } 474 502 .object-contain { 475 503 object-fit: contain; 476 504 } ··· 500 528 } 501 529 .pt-4 { 502 530 padding-top: calc(var(--spacing) * 4); 531 + } 532 + .pb-4 { 533 + padding-bottom: calc(var(--spacing) * 4); 503 534 } 504 535 .text-center { 505 536 text-align: center; ··· 556 587 .text-zinc-900 { 557 588 color: var(--color-zinc-900); 558 589 } 590 + .text-zinc-950 { 591 + color: var(--color-zinc-950); 592 + } 559 593 .lowercase { 560 594 text-transform: lowercase; 561 595 } ··· 591 625 box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); 592 626 } 593 627 } 628 + .data-\[selected\=false\]\:border-transparent { 629 + &[data-selected="false"] { 630 + border-color: transparent; 631 + } 632 + } 633 + .data-\[selected\=false\]\:bg-transparent { 634 + &[data-selected="false"] { 635 + background-color: transparent; 636 + } 637 + } 594 638 .data-\[state\=pending\]\:opacity-50 { 595 639 &[data-state="pending"] { 596 640 opacity: 50%; 597 - } 598 - } 599 - .sm\:top-1 { 600 - @media (width >= 40rem) { 601 - top: calc(var(--spacing) * 1); 602 641 } 603 642 } 604 643 .sm\:right-1 { ··· 611 650 bottom: calc(var(--spacing) * 1); 612 651 } 613 652 } 614 - .sm\:left-1 { 615 - @media (width >= 40rem) { 616 - left: calc(var(--spacing) * 1); 617 - } 618 - } 619 653 .sm\:h-screen { 620 654 @media (width >= 40rem) { 621 655 height: 100vh; ··· 629 663 .sm\:w-fit { 630 664 @media (width >= 40rem) { 631 665 width: fit-content; 666 + } 667 + } 668 + .sm\:max-w-\[400px\] { 669 + @media (width >= 40rem) { 670 + max-width: 400px; 632 671 } 633 672 } 634 673 .sm\:grid-cols-3 { ··· 681 720 background-color: var(--color-zinc-950); 682 721 } 683 722 } 723 + .dark\:fill-zinc-50 { 724 + @media (prefers-color-scheme: dark) { 725 + fill: var(--color-zinc-50); 726 + } 727 + } 684 728 .dark\:text-white { 685 729 @media (prefers-color-scheme: dark) { 686 730 color: var(--color-white); 731 + } 732 + } 733 + .dark\:text-zinc-50 { 734 + @media (prefers-color-scheme: dark) { 735 + color: var(--color-zinc-50); 687 736 } 688 737 } 689 738 .dark\:text-zinc-500 { ··· 691 740 color: var(--color-zinc-500); 692 741 } 693 742 } 694 - } 695 - .htmx-request.htmx-indicator { 696 - display: inline; 697 - } 698 - .htmx-indicator { 699 - display: none; 700 - } 701 - .htmx-request #submit-button { 702 - opacity: 0.5; 703 - pointer-events: none; 704 743 } 705 744 @property --tw-space-y-reverse { 706 745 syntax: "*";