grain.social is a photo sharing platform built on atproto.

Merge branch 'main' into gallery-sort

+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: "*";