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

Merge branch 'main' into gallery-sort

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