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