+1
-1
deno.json
+1
-1
deno.json
+223
-21
deno.lock
+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
+2
-10
input.css
+305
-156
main.tsx
+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
+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
+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: "*";