+4
__generated__/lexicons.ts
+4
__generated__/lexicons.ts
+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.15",
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.15": "0.3.0-beta.15",
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.15": {
96
+
"integrity": "934d0fab8cc73804099ccb5362fa89f5ef3cd6269a6613029131770c97cdfcb9",
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.15",
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 *)); */
+423
-157
main.tsx
+423
-157
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
53
58
const PUBLIC_URL = Deno.env.get("BFF_PUBLIC_URL") ?? "http://localhost:8080";
54
59
const GOATCOUNTER_URL = Deno.env.get("GOATCOUNTER_URL");
55
60
56
-
let cssContentHash: string = "";
61
+
const staticFilesHash = new Map<string, string>();
57
62
58
63
bff({
59
64
appName: "Grain Social",
···
68
73
lexicons,
69
74
rootElement: Root,
70
75
onListen: async () => {
71
-
const cssFileContent = await Deno.readFile(
72
-
join(Deno.cwd(), "static", "styles.css"),
73
-
);
74
-
const hashBuffer = await crypto.subtle.digest("SHA-256", cssFileContent);
75
-
cssContentHash = Array.from(new Uint8Array(hashBuffer))
76
-
.map((b) => b.toString(16).padStart(2, "0"))
77
-
.join("");
76
+
for (const entry of Deno.readDirSync(join(Deno.cwd(), "static"))) {
77
+
if (
78
+
entry.isFile &&
79
+
(entry.name.endsWith(".js") || entry.name.endsWith(".css"))
80
+
) {
81
+
const fileContent = await Deno.readFile(
82
+
join(Deno.cwd(), "static", entry.name),
83
+
);
84
+
const hashBuffer = await crypto.subtle.digest("SHA-256", fileContent);
85
+
const hash = Array.from(new Uint8Array(hashBuffer))
86
+
.map((b) => b.toString(16).padStart(2, "0"))
87
+
.join("");
88
+
staticFilesHash.set(entry.name, hash);
89
+
}
90
+
}
78
91
},
79
92
onError: (err) => {
80
93
if (err instanceof UnauthorizedError) {
···
102
115
<div
103
116
id="login"
104
117
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;"
118
+
style="background-image: url('https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg'); background-size: cover; background-position: center;"
106
119
>
107
120
<Login hx-target="#login" error={error} errorClass="text-white" />
108
121
<div class="absolute bottom-2 right-2 text-white text-sm">
···
134
147
if (!profile) return ctx.next();
135
148
let follow: WithBffMeta<BskyFollow> | undefined;
136
149
if (ctx.currentUser) {
137
-
follow = getFollow(
138
-
profile.did,
139
-
ctx.currentUser.did,
140
-
ctx,
141
-
);
150
+
follow = getFollow(profile.did, ctx.currentUser.did, ctx);
142
151
}
143
152
ctx.state.meta = [
144
153
{
···
182
191
...getPageMeta(galleryLink(handle, rkey)),
183
192
...getGalleryMeta(gallery),
184
193
];
185
-
ctx.state.scripts = ["photo_dialog.js", "masonry.js"];
194
+
ctx.state.scripts = ["photo_dialog.js", "masonry.js", "sortable.js"];
186
195
return ctx.render(
187
196
<GalleryPage favs={favs} gallery={gallery} currentUserDid={did} />,
188
197
);
···
214
223
createdAt: new Date().toISOString(),
215
224
},
216
225
);
217
-
return ctx.html(
218
-
<FollowButton followeeDid={did} followUri={followUri} />,
219
-
);
226
+
return ctx.html(<FollowButton followeeDid={did} followUri={followUri} />);
220
227
}),
221
228
route("/follow/:did/:rkey", ["DELETE"], async (_req, params, ctx) => {
222
229
requireAuth(ctx);
···
226
233
await ctx.deleteRecord(
227
234
`at://${ctx.currentUser.did}/app.bsky.graph.follow/${rkey}`,
228
235
);
229
-
return ctx.html(
230
-
<FollowButton followeeDid={did} followUri={undefined} />,
231
-
);
236
+
return ctx.html(<FollowButton followeeDid={did} followUri={undefined} />);
232
237
}),
233
238
route("/dialogs/gallery/new", (_req, _params, ctx) => {
234
239
requireAuth(ctx);
···
241
246
const gallery = getGallery(handle, rkey, ctx);
242
247
return ctx.html(<GalleryCreateEditDialog gallery={gallery} />);
243
248
}),
249
+
route("/dialogs/gallery/:rkey/sort", (_req, params, ctx) => {
250
+
requireAuth(ctx);
251
+
const handle = ctx.currentUser.handle;
252
+
const rkey = params.rkey;
253
+
const gallery = getGallery(handle, rkey, ctx);
254
+
if (!gallery) return ctx.next();
255
+
return ctx.html(<GallerySortDialog gallery={gallery} />);
256
+
}),
244
257
route("/onboard", (_req, _params, ctx) => {
245
258
requireAuth(ctx);
246
259
return ctx.render(
···
308
321
/>,
309
322
);
310
323
}),
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
+
route("/dialogs/photo/:rkey/alt", (_req, params, ctx) => {
325
+
requireAuth(ctx);
326
+
const photoRkey = params.rkey;
327
+
const photoUri =
328
+
`at://${ctx.currentUser.did}/social.grain.photo/${photoRkey}`;
329
+
const photo = ctx.indexService.getRecord<WithBffMeta<Photo>>(photoUri);
330
+
if (!photo) return ctx.next();
324
331
return ctx.html(
325
-
<PhotoAltDialog galleryUri={gallery.uri} photo={photo} />,
332
+
<PhotoAltDialog photo={photoToView(ctx.currentUser.did, photo)} />,
326
333
);
327
334
}),
328
335
route("/dialogs/photo-select/:galleryRkey", (_req, params, ctx) => {
···
430
437
key={photo.cid}
431
438
photo={photoToView(photo.did, photo)}
432
439
gallery={gallery}
433
-
isCreator={ctx.currentUser.did === gallery.creator.did}
434
-
isLoggedIn={!!ctx.currentUser.did}
435
440
/>
436
441
</div>
437
442
<PhotoSelectButton
···
508
513
});
509
514
return new Response(null, { status: 200 });
510
515
}),
516
+
route("/actions/photo/:rkey", ["DELETE"], (_req, params, ctx) => {
517
+
requireAuth(ctx);
518
+
ctx.deleteRecord(
519
+
`at://${ctx.currentUser.did}/social.grain.photo/${params.rkey}`,
520
+
);
521
+
return new Response(null, { status: 200 });
522
+
}),
511
523
route("/actions/favorite", ["POST"], async (req, _params, ctx) => {
512
524
requireAuth(ctx);
513
525
const url = new URL(req.url);
···
566
578
567
579
return ctx.redirect(`/profile/${ctx.currentUser.handle}`);
568
580
}),
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
-
}),
581
+
route(
582
+
"/actions/gallery/:rkey/sort",
583
+
["POST"],
584
+
async (req, params, ctx) => {
585
+
requireAuth(ctx);
586
+
const galleryRkey = params.rkey;
587
+
const galleryUri =
588
+
`at://${ctx.currentUser.did}/social.grain.gallery/${galleryRkey}`;
589
+
const {
590
+
items,
591
+
} = ctx.indexService.getRecords<WithBffMeta<GalleryItem>>(
592
+
"social.grain.gallery.item",
593
+
{
594
+
where: [
595
+
{
596
+
field: "gallery",
597
+
equals: galleryUri,
598
+
},
599
+
],
600
+
},
601
+
);
602
+
const itemsMap = new Map<string, WithBffMeta<GalleryItem>>();
603
+
for (const item of items) {
604
+
itemsMap.set(item.item, item);
605
+
}
606
+
const formData = await req.formData();
607
+
const sortedItems = formData.getAll("item") as string[];
608
+
const updates = [];
609
+
let position = 0;
610
+
for (const sortedItemUri of sortedItems) {
611
+
const item = itemsMap.get(sortedItemUri);
612
+
if (!item) continue;
613
+
updates.push({
614
+
collection: "social.grain.gallery.item",
615
+
rkey: new AtUri(item.uri).rkey,
616
+
data: {
617
+
gallery: item.gallery,
618
+
item: item.item,
619
+
createdAt: item.createdAt,
620
+
position,
621
+
},
622
+
});
623
+
position++;
624
+
}
625
+
await ctx.updateRecords<WithBffMeta<GalleryItem>>(updates);
626
+
return ctx.redirect(
627
+
`/profile/${ctx.currentUser.handle}/${galleryRkey}`,
628
+
);
629
+
},
630
+
),
576
631
...photoUploadRoutes(),
577
632
...avatarUploadRoutes(),
578
633
],
···
657
712
};
658
713
659
714
function getFollow(followeeDid: string, followerDid: string, ctx: BffContext) {
660
-
const { items: [follow] } = ctx.indexService.getRecords<
661
-
WithBffMeta<BskyFollow>
662
-
>(
715
+
const {
716
+
items: [follow],
717
+
} = ctx.indexService.getRecords<WithBffMeta<BskyFollow>>(
663
718
"app.bsky.graph.follow",
664
719
{
665
720
where: [
···
691
746
const { items: galleryItems } = ctx.indexService.getRecords<
692
747
WithBffMeta<GalleryItem>
693
748
>("social.grain.gallery.item", {
694
-
orderBy: { field: "createdAt", direction: "asc" },
749
+
orderBy: { field: "position", direction: "asc" },
695
750
where: [{ field: "gallery", in: galleryUris }],
696
751
});
697
752
···
1045
1100
: null}
1046
1101
<script src="https://unpkg.com/htmx.org@1.9.10" />
1047
1102
<script src="https://unpkg.com/hyperscript.org@0.9.14" />
1103
+
<script src="https://unpkg.com/sortablejs@1.15.6" />
1048
1104
<style dangerouslySetInnerHTML={{ __html: CSS }} />
1049
-
<link rel="stylesheet" href={`/static/styles.css?${cssContentHash}`} />
1105
+
<link
1106
+
rel="stylesheet"
1107
+
href={`/static/styles.css?${staticFilesHash.get("styles.css")}`}
1108
+
/>
1050
1109
<link rel="preconnect" href="https://fonts.googleapis.com" />
1051
1110
<link
1052
1111
rel="preconnect"
···
1062
1121
href="https://unpkg.com/@fortawesome/fontawesome-free@6.7.2/css/all.min.css"
1063
1122
preload
1064
1123
/>
1065
-
{scripts?.map((file) => <script key={file} src={`/static/${file}`} />)}
1124
+
{scripts?.map((file) => (
1125
+
<script
1126
+
key={file}
1127
+
src={`/static/${file}?${staticFilesHash.get(file)}`}
1128
+
/>
1129
+
))}
1066
1130
</head>
1067
1131
<body class="h-full w-full dark:bg-zinc-950 dark:text-white">
1068
-
<Layout id="layout" class="dark:border-zinc-800">
1132
+
<Layout id="layout" class="border-zinc-200 dark:border-zinc-800">
1069
1133
<Layout.Nav
1070
1134
heading={
1071
1135
<h1 class="font-['Jersey_20'] text-4xl text-zinc-900 dark:text-white">
···
1074
1138
</h1>
1075
1139
}
1076
1140
profile={profile}
1077
-
class="dark:border-zinc-800"
1141
+
class="border-zinc-200 dark:border-zinc-800"
1078
1142
/>
1079
1143
<Layout.Content>{props.children}</Layout.Content>
1080
1144
</Layout>
···
1137
1201
);
1138
1202
}
1139
1203
1204
+
function ActorInfo({ profile }: Readonly<{ profile: Un$Typed<ProfileView> }>) {
1205
+
return (
1206
+
<div class="flex items-center gap-2 min-w-0 flex-1">
1207
+
<img
1208
+
src={profile.avatar}
1209
+
alt={profile.handle}
1210
+
class="rounded-full object-cover size-7 shrink-0"
1211
+
/>
1212
+
<a
1213
+
href={profileLink(profile.handle)}
1214
+
class="hover:underline text-zinc-600 dark:text-zinc-500 truncate max-w-[300px] sm:max-w-[400px]"
1215
+
>
1216
+
<span class="text-zinc-950 dark:text-zinc-50 font-semibold text-">
1217
+
{profile.displayName || profile.handle}
1218
+
</span>{" "}
1219
+
<span class="truncate">@{profile.handle}</span>
1220
+
</a>
1221
+
</div>
1222
+
);
1223
+
}
1224
+
1140
1225
function Timeline({ items }: Readonly<{ items: TimelineItem[] }>) {
1141
1226
return (
1142
1227
<div class="px-4 mb-4">
···
1152
1237
1153
1238
function TimelineItem({ item }: Readonly<{ item: TimelineItem }>) {
1154
1239
return (
1155
-
<li class="space-y-2">
1156
-
<div class="bg-zinc-100 dark:bg-zinc-900 w-fit p-2">
1157
-
<a
1158
-
href={profileLink(item.actor.handle)}
1159
-
class="font-semibold hover:underline"
1160
-
>
1161
-
@{item.actor.handle}
1162
-
</a>{" "}
1163
-
{item.itemType === "favorite" ? "favorited" : "created"}{" "}
1164
-
<a
1165
-
href={galleryLink(
1166
-
item.gallery.creator.handle,
1167
-
new AtUri(item.gallery.uri).rkey,
1168
-
)}
1169
-
class="font-semibold hover:underline"
1170
-
>
1171
-
{(item.gallery.record as Gallery).title}
1172
-
</a>
1173
-
<span class="ml-1">
1174
-
{formatDistanceStrict(item.createdAt, new Date(), {
1175
-
addSuffix: true,
1176
-
})}
1177
-
</span>
1178
-
</div>
1179
-
<a
1180
-
href={galleryLink(
1181
-
item.gallery.creator.handle,
1182
-
new AtUri(item.gallery.uri).rkey,
1183
-
)}
1184
-
class="w-fit flex"
1185
-
>
1240
+
<li>
1241
+
<div class="w-fit flex flex-col gap-4 pb-4 border-b border-zinc-200 dark:border-zinc-800">
1242
+
<div class="flex items-center justify-between gap-2 w-full">
1243
+
<ActorInfo profile={item.actor} />
1244
+
<span class="shrink-0">
1245
+
{formatRelativeTime(new Date(item.createdAt))}
1246
+
</span>
1247
+
</div>
1186
1248
{item.gallery.items?.filter(isPhotoView).length
1187
1249
? (
1188
-
<div class="flex w-full max-w-md mx-auto aspect-[3/2] overflow-hidden gap-2">
1250
+
<a
1251
+
href={galleryLink(
1252
+
item.gallery.creator.handle,
1253
+
new AtUri(item.gallery.uri).rkey,
1254
+
)}
1255
+
class="flex w-full max-w-md mx-auto aspect-[3/2] overflow-hidden gap-2"
1256
+
>
1189
1257
<div class="w-2/3 h-full">
1190
1258
<img
1191
1259
src={item.gallery.items?.filter(isPhotoView)[0].thumb}
···
1223
1291
)}
1224
1292
</div>
1225
1293
</div>
1226
-
</div>
1294
+
</a>
1227
1295
)
1228
1296
: null}
1229
-
</a>
1297
+
<p>
1298
+
{item.itemType === "favorite" ? "Favorited" : "Created"}{" "}
1299
+
<a
1300
+
href={galleryLink(
1301
+
item.gallery.creator.handle,
1302
+
new AtUri(item.gallery.uri).rkey,
1303
+
)}
1304
+
class="font-semibold hover:underline"
1305
+
>
1306
+
{(item.gallery.record as Gallery).title}
1307
+
</a>
1308
+
</p>
1309
+
</div>
1230
1310
</li>
1231
1311
);
1232
1312
}
···
1252
1332
: {
1253
1333
children: (
1254
1334
<>
1255
-
<i class="fa-solid fa-plus mr-2" />Follow
1335
+
<i class="fa-solid fa-plus mr-2" />
1336
+
Follow
1256
1337
</>
1257
1338
),
1258
1339
"hx-post": `/follow/${followeeDid}`,
···
1262
1343
hx-swap="outerHTML"
1263
1344
/>
1264
1345
);
1346
+
}
1347
+
1348
+
function formatRelativeTime(date: Date) {
1349
+
const now = new Date();
1350
+
const weeks = differenceInWeeks(now, date);
1351
+
if (weeks > 0) return `${weeks}w`;
1352
+
1353
+
const days = differenceInDays(now, date);
1354
+
if (days > 0) return `${days}d`;
1355
+
1356
+
const hours = differenceInHours(now, date);
1357
+
if (hours > 0) return `${hours}h`;
1358
+
1359
+
const minutes = differenceInMinutes(now, date);
1360
+
return `${Math.max(1, minutes)}m`;
1265
1361
}
1266
1362
1267
1363
function ProfilePage({
···
1280
1376
galleries?: GalleryView[];
1281
1377
}>) {
1282
1378
const isCreator = loggedInUserDid === profile.did;
1379
+
const displayName = profile.displayName || profile.handle;
1283
1380
return (
1284
1381
<div class="px-4 mb-4" id="profile-page">
1285
1382
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4">
1286
-
<div class="flex flex-col">
1383
+
<div class="flex flex-col mb-4">
1287
1384
<AvatarButton profile={profile} />
1288
-
<p class="text-2xl font-bold">{profile.displayName}</p>
1385
+
<p class="text-2xl font-bold">{displayName}</p>
1289
1386
<p class="text-zinc-600 dark:text-zinc-500">@{profile.handle}</p>
1290
-
<p class="my-2">{profile.description}</p>
1387
+
{profile.description
1388
+
? <p class="mt-2">{profile.description}</p>
1389
+
: null}
1291
1390
</div>
1292
1391
{!isCreator && loggedInUserDid
1293
1392
? (
···
1377
1476
: null}
1378
1477
{selectedTab === "galleries"
1379
1478
? (
1380
-
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
1479
+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4">
1381
1480
{galleries?.length
1382
1481
? (
1383
1482
galleries.map((gallery) => (
···
1391
1490
{gallery.items?.length
1392
1491
? (
1393
1492
<img
1394
-
src={gallery.items?.filter(isPhotoView)?.[0]?.thumb}
1493
+
src={gallery.items?.filter(isPhotoView)?.[0]
1494
+
?.fullsize}
1395
1495
alt={gallery.items?.filter(isPhotoView)?.[0]?.alt}
1396
1496
class="w-full h-full object-cover"
1397
1497
/>
···
1414
1514
);
1415
1515
}
1416
1516
1417
-
function UploadPage(
1418
-
{ handle, photos, returnTo }: Readonly<
1419
-
{ handle: string; photos: PhotoView[]; returnTo?: string }
1420
-
>,
1421
-
) {
1517
+
function UploadPage({
1518
+
handle,
1519
+
photos,
1520
+
returnTo,
1521
+
}: Readonly<{ handle: string; photos: PhotoView[]; returnTo?: string }>) {
1422
1522
return (
1423
1523
<div class="flex flex-col px-4 pt-4 mb-4 space-y-4">
1424
1524
<div class="flex">
1425
1525
<div class="flex-1">
1426
1526
{returnTo
1427
1527
? (
1428
-
<a
1429
-
href={returnTo}
1430
-
class="hover:underline"
1431
-
>
1528
+
<a href={returnTo} class="hover:underline">
1432
1529
<i class="fa-solid fa-arrow-left mr-2" />
1433
1530
Back to gallery
1434
1531
</a>
···
1440
1537
</a>
1441
1538
)}
1442
1539
</div>
1443
-
<div>10/100 photos</div>
1444
1540
</div>
1445
-
<Button variant="primary" class="mb-4" asChild>
1446
-
<label class="w-fit">
1541
+
<Button variant="primary" class="mb-4 w-full sm:w-fit" asChild>
1542
+
<label>
1447
1543
<i class="fa fa-plus"></i> Add photos
1448
1544
<input
1449
1545
class="hidden"
···
1488
1584
}>) {
1489
1585
return (
1490
1586
<Dialog>
1491
-
<Dialog.Content class="dark:bg-zinc-950">
1587
+
<Dialog.Content class="dark:bg-zinc-950 relative">
1588
+
<Dialog.X class="fill-zinc-950 dark:fill-zinc-50" />
1492
1589
<Dialog.Title>Edit my profile</Dialog.Title>
1493
1590
<div>
1494
1591
<AvatarForm src={profile.avatar} alt={profile.handle} />
···
1510
1607
name="displayName"
1511
1608
class="dark:bg-zinc-800 dark:text-white"
1512
1609
value={profile.displayName}
1610
+
autoFocus
1513
1611
/>
1514
1612
</div>
1515
1613
<div class="mb-4 relative">
···
1591
1689
}>) {
1592
1690
const isCreator = currentUserDid === gallery.creator.did;
1593
1691
const isLoggedIn = !!currentUserDid;
1692
+
const description = (gallery.record as Gallery).description;
1594
1693
return (
1595
1694
<div class="px-4">
1596
-
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4">
1597
-
<div class="flex flex-col space-y-1 mb-4">
1695
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mt-4 mb-2">
1696
+
<div class="flex flex-col space-y-2 mb-4">
1598
1697
<h1 class="font-bold text-2xl">
1599
1698
{(gallery.record as Gallery).title}
1600
1699
</h1>
1601
-
<div>
1602
-
Gallery by{" "}
1603
-
<a
1604
-
href={profileLink(gallery.creator.handle)}
1605
-
class="hover:underline"
1606
-
>
1607
-
<span class="font-semibold">{gallery.creator.displayName}</span>
1608
-
{" "}
1609
-
<span class="text-zinc-600 dark:text-zinc-500">
1610
-
@{gallery.creator.handle}
1611
-
</span>
1612
-
</a>
1613
-
</div>
1614
-
<p>{(gallery.record as Gallery).description}</p>
1700
+
<ActorInfo profile={gallery.creator} />
1701
+
{description ? <p>{description}</p> : null}
1615
1702
</div>
1616
1703
{isLoggedIn && isCreator
1617
1704
? (
1618
1705
<div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row">
1619
1706
<Button
1707
+
variant="primary"
1708
+
class="self-start w-full sm:w-fit"
1709
+
hx-get={`/dialogs/gallery/${new AtUri(gallery.uri).rkey}`}
1710
+
hx-target="#layout"
1711
+
hx-swap="afterbegin"
1712
+
>
1713
+
Edit
1714
+
</Button>
1715
+
<Button
1620
1716
hx-get={`/dialogs/photo-select/${new AtUri(gallery.uri).rkey}`}
1621
1717
hx-target="#layout"
1622
1718
hx-swap="afterbegin"
···
1628
1724
<Button
1629
1725
variant="primary"
1630
1726
class="self-start w-full sm:w-fit"
1631
-
hx-get={`/dialogs/gallery/${new AtUri(gallery.uri).rkey}`}
1727
+
hx-get={`/dialogs/gallery/${new AtUri(gallery.uri).rkey}/sort`}
1632
1728
hx-target="#layout"
1633
1729
hx-swap="afterbegin"
1634
1730
>
1635
-
Edit
1731
+
Sort order
1636
1732
</Button>
1733
+
<ShareGalleryButton gallery={gallery} />
1637
1734
</div>
1638
1735
)
1639
1736
: null}
1640
1737
{!isCreator
1641
1738
? (
1642
-
<FavoriteButton
1643
-
currentUserDid={currentUserDid}
1644
-
favs={favs}
1645
-
galleryUri={gallery.uri}
1646
-
/>
1739
+
<div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row">
1740
+
<ShareGalleryButton gallery={gallery} />
1741
+
<FavoriteButton
1742
+
currentUserDid={currentUserDid}
1743
+
favs={favs}
1744
+
galleryUri={gallery.uri}
1745
+
/>
1746
+
</div>
1647
1747
)
1648
1748
: null}
1649
1749
</div>
1750
+
<div class="flex justify-end mb-2">
1751
+
<Button
1752
+
id="justified-button"
1753
+
variant="primary"
1754
+
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"
1755
+
_="on click call toggleLayout('justified')
1756
+
set @data-selected to 'true'
1757
+
set #masonry-button's @data-selected to 'false'"
1758
+
>
1759
+
<svg
1760
+
width="24"
1761
+
height="24"
1762
+
viewBox="0 0 24 24"
1763
+
xmlns="http://www.w3.org/2000/svg"
1764
+
>
1765
+
<rect x="2" y="2" width="8" height="6" fill="currentColor" rx="1" />
1766
+
<rect
1767
+
x="12"
1768
+
y="2"
1769
+
width="10"
1770
+
height="6"
1771
+
fill="currentColor"
1772
+
rx="1"
1773
+
/>
1774
+
<rect
1775
+
x="2"
1776
+
y="10"
1777
+
width="6"
1778
+
height="6"
1779
+
fill="currentColor"
1780
+
rx="1"
1781
+
/>
1782
+
<rect
1783
+
x="10"
1784
+
y="10"
1785
+
width="12"
1786
+
height="6"
1787
+
fill="currentColor"
1788
+
rx="1"
1789
+
/>
1790
+
<rect
1791
+
x="2"
1792
+
y="18"
1793
+
width="20"
1794
+
height="4"
1795
+
fill="currentColor"
1796
+
rx="1"
1797
+
/>
1798
+
</svg>
1799
+
</Button>
1800
+
<Button
1801
+
id="masonry-button"
1802
+
variant="primary"
1803
+
data-selected="false"
1804
+
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"
1805
+
_="on click call toggleLayout('masonry')
1806
+
set @data-selected to 'true'
1807
+
set #justified-button's @data-selected to 'false'"
1808
+
>
1809
+
<svg
1810
+
width="24"
1811
+
height="24"
1812
+
viewBox="0 0 24 24"
1813
+
xmlns="http://www.w3.org/2000/svg"
1814
+
>
1815
+
<rect x="2" y="2" width="8" height="8" fill="currentColor" rx="1" />
1816
+
<rect
1817
+
x="12"
1818
+
y="2"
1819
+
width="8"
1820
+
height="4"
1821
+
fill="currentColor"
1822
+
rx="1"
1823
+
/>
1824
+
<rect
1825
+
x="12"
1826
+
y="8"
1827
+
width="8"
1828
+
height="6"
1829
+
fill="currentColor"
1830
+
rx="1"
1831
+
/>
1832
+
<rect
1833
+
x="2"
1834
+
y="12"
1835
+
width="8"
1836
+
height="8"
1837
+
fill="currentColor"
1838
+
rx="1"
1839
+
/>
1840
+
<rect
1841
+
x="12"
1842
+
y="16"
1843
+
width="8"
1844
+
height="4"
1845
+
fill="currentColor"
1846
+
rx="1"
1847
+
/>
1848
+
</svg>
1849
+
</Button>
1850
+
</div>
1650
1851
<div
1651
1852
id="masonry-container"
1652
1853
class="h-0 overflow-hidden relative mx-auto w-full"
1653
-
_="on load or htmx:afterSettle call computeMasonry()"
1854
+
_="on load or htmx:afterSettle call computeLayout()"
1654
1855
>
1655
1856
{gallery.items?.filter(isPhotoView)?.length
1656
1857
? gallery?.items
···
1660
1861
key={photo.cid}
1661
1862
photo={photo}
1662
1863
gallery={gallery}
1663
-
isCreator={isCreator}
1664
-
isLoggedIn={isLoggedIn}
1665
1864
/>
1666
1865
))
1667
1866
: null}
···
1673
1872
function PhotoButton({
1674
1873
photo,
1675
1874
gallery,
1676
-
isCreator,
1677
-
isLoggedIn,
1678
1875
}: Readonly<{
1679
1876
photo: PhotoView;
1680
1877
gallery: GalleryView;
1681
-
isCreator: boolean;
1682
-
isLoggedIn: boolean;
1683
1878
}>) {
1684
1879
return (
1685
1880
<button
···
1693
1888
data-width={photo.aspectRatio?.width}
1694
1889
data-height={photo.aspectRatio?.height}
1695
1890
>
1696
-
{isLoggedIn && isCreator
1697
-
? <AltTextButton galleryUri={gallery.uri} cid={photo.cid} />
1698
-
: null}
1699
1891
<img
1700
1892
src={photo.fullsize}
1701
1893
alt={photo.alt}
1702
1894
class="w-full h-full object-cover"
1703
1895
/>
1704
-
{!isCreator && photo.alt
1896
+
{photo.alt
1705
1897
? (
1706
1898
<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]">
1707
1899
ALT
···
1712
1904
);
1713
1905
}
1714
1906
1907
+
function GallerySortDialog({ gallery }: Readonly<{ gallery: GalleryView }>) {
1908
+
return (
1909
+
<Dialog>
1910
+
<Dialog.Content class="dark:bg-zinc-950 relative">
1911
+
<Dialog.X class="fill-zinc-950 dark:fill-zinc-50" />
1912
+
<Dialog.Title>Sort gallery</Dialog.Title>
1913
+
<p class="my-2 text-center">Drag photos to rearrange</p>
1914
+
<form
1915
+
hx-post={`/actions/gallery/${new AtUri(gallery.uri).rkey}/sort`}
1916
+
hx-trigger="submit"
1917
+
hx-swap="none"
1918
+
>
1919
+
<div class="sortable grid grid-cols-3 sm:grid-cols-5 gap-2 mt-2">
1920
+
{gallery?.items?.filter(isPhotoView).map((item) => (
1921
+
<div
1922
+
key={item.cid}
1923
+
class="relative aspect-square cursor-grab"
1924
+
>
1925
+
<input type="hidden" name="item" value={item.uri} />
1926
+
<img
1927
+
src={item.fullsize}
1928
+
alt={item.alt}
1929
+
class="w-full h-full absolute object-cover"
1930
+
/>
1931
+
</div>
1932
+
))}
1933
+
</div>
1934
+
<div class="flex flex-col gap-2 mt-2">
1935
+
<Button
1936
+
variant="primary"
1937
+
type="submit"
1938
+
class="w-full"
1939
+
>
1940
+
Save
1941
+
</Button>
1942
+
<Button
1943
+
variant="secondary"
1944
+
type="button"
1945
+
class="w-full"
1946
+
_={Dialog._closeOnClick}
1947
+
>
1948
+
Cancel
1949
+
</Button>
1950
+
</div>
1951
+
</form>
1952
+
</Dialog.Content>
1953
+
</Dialog>
1954
+
);
1955
+
}
1956
+
1957
+
function ShareGalleryButton({ gallery }: Readonly<{ gallery: GalleryView }>) {
1958
+
return (
1959
+
<>
1960
+
<input
1961
+
type="hidden"
1962
+
id="copy-text"
1963
+
value={publicGalleryLink(gallery.creator.handle, gallery.uri)}
1964
+
/>
1965
+
<Button
1966
+
variant="primary"
1967
+
_={`on click
1968
+
set copyText to #copy-text.value
1969
+
writeText(copyText) on navigator.clipboard
1970
+
alert('Copied to clipboard')`}
1971
+
>
1972
+
<i class="fa-solid fa-share-nodes mr-2" />
1973
+
Share
1974
+
</Button>
1975
+
</>
1976
+
);
1977
+
}
1978
+
1715
1979
function FavoriteButton({
1716
1980
currentUserDid,
1717
1981
favs = [],
···
1744
2008
}: Readonly<{ gallery?: GalleryView | null }>) {
1745
2009
return (
1746
2010
<Dialog id="gallery-dialog" class="z-30">
1747
-
<Dialog.Content class="dark:bg-zinc-950">
2011
+
<Dialog.Content class="dark:bg-zinc-950 relative">
2012
+
<Dialog.X class="fill-zinc-950 dark:fill-zinc-50" />
1748
2013
<Dialog.Title>
1749
2014
{gallery ? "Edit gallery" : "Create a new gallery"}
1750
2015
</Dialog.Title>
···
1841
2106
}>) {
1842
2107
return (
1843
2108
<div class="relative aspect-square bg-zinc-200 dark:bg-zinc-900">
2109
+
{uri ? <AltTextButton photoUri={uri} /> : null}
1844
2110
{uri
1845
2111
? (
1846
2112
<button
···
1863
2129
);
1864
2130
}
1865
2131
1866
-
function AltTextButton({
1867
-
galleryUri,
1868
-
cid,
1869
-
}: Readonly<{ galleryUri: string; cid: string }>) {
2132
+
function AltTextButton({ photoUri }: Readonly<{ photoUri: string }>) {
1870
2133
return (
1871
2134
<div
1872
-
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"
1873
-
hx-get={`/dialogs/image-alt?galleryUri=${galleryUri}&imageCid=${cid}`}
2135
+
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"
2136
+
hx-get={`/dialogs/photo/${new AtUri(photoUri).rkey}/alt`}
1874
2137
hx-trigger="click"
1875
2138
hx-target="#layout"
1876
2139
hx-swap="afterbegin"
···
1894
2157
}>) {
1895
2158
return (
1896
2159
<Dialog id="photo-dialog" class="bg-zinc-950 z-30">
2160
+
<Dialog.X />
1897
2161
{nextImage
1898
2162
? (
1899
2163
<div
···
1939
2203
1940
2204
function PhotoAltDialog({
1941
2205
photo,
1942
-
galleryUri,
1943
2206
}: Readonly<{
1944
2207
photo: PhotoView;
1945
-
galleryUri: string;
1946
2208
}>) {
1947
2209
return (
1948
2210
<Dialog id="photo-alt-dialog" class="z-30">
1949
-
<Dialog.Content class="dark:bg-zinc-950">
2211
+
<Dialog.Content class="dark:bg-zinc-950 relative">
2212
+
<Dialog.X class="fill-zinc-950 dark:fill-zinc-50" />
1950
2213
<Dialog.Title>Add alt text</Dialog.Title>
1951
2214
<div class="aspect-square relative">
1952
2215
<img
···
1959
2222
hx-put={`/actions/photo/${new AtUri(photo.uri).rkey}`}
1960
2223
_="on htmx:afterOnLoad trigger closeDialog"
1961
2224
>
1962
-
<input type="hidden" name="galleryUri" value={galleryUri} />
1963
-
<input type="hidden" name="cid" value={photo.cid} />
1964
2225
<div class="my-2">
1965
2226
<label htmlFor="alt">Descriptive alt text</label>
1966
2227
<Textarea
···
1996
2257
}>) {
1997
2258
return (
1998
2259
<Dialog id="photo-select-dialog" class="z-30">
1999
-
<Dialog.Content class="w-full max-w-5xl dark:bg-zinc-950 sm:min-h-screen flex flex-col">
2260
+
<Dialog.Content class="w-full max-w-5xl dark:bg-zinc-950 sm:min-h-screen flex flex-col relative">
2261
+
<Dialog.X class="fill-zinc-950 dark:fill-zinc-50" />
2000
2262
<Dialog.Title>Add photos</Dialog.Title>
2001
2263
{photos.length
2002
2264
? (
···
2008
2270
: null}
2009
2271
{photos.length
2010
2272
? (
2011
-
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4 my-4 flex-1">
2273
+
<div class="grid grid-cols-3 sm:grid-cols-5 gap-4 my-4 flex-1">
2012
2274
{photos.map((photo) => (
2013
2275
<PhotoSelectButton
2014
2276
key={photo.cid}
···
2069
2331
set @data-added to 'true'
2070
2332
end`}
2071
2333
>
2072
-
<div class="hidden group-data-[added=true]:block absolute top-2 right-2">
2334
+
<div class="hidden group-data-[added=true]:block absolute top-2 right-2 z-30">
2073
2335
<i class="fa-check fa-solid text-sky-500 z-10" />
2074
2336
</div>
2075
2337
<img
···
2130
2392
uri: photo.uri,
2131
2393
cid: photo.photo.ref.toString(),
2132
2394
thumb:
2133
-
`https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${photo.photo.ref.toString()}@webp`,
2395
+
`https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${photo.photo.ref.toString()}@jpeg`,
2134
2396
fullsize:
2135
-
`https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${photo.photo.ref.toString()}@webp`,
2397
+
`https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${photo.photo.ref.toString()}@jpeg`,
2136
2398
alt: photo.alt,
2137
2399
aspectRatio: photo.aspectRatio,
2138
2400
};
···
2394
2656
),
2395
2657
];
2396
2658
}
2659
+
2660
+
function publicGalleryLink(handle: string, galleryUri: string): string {
2661
+
return `${PUBLIC_URL}/profile/${handle}/${new AtUri(galleryUri).rkey}`;
2662
+
}
+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, {
+8
static/sortable.js
+8
static/sortable.js
+149
-35
static/styles.css
+149
-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
}
···
293
294
.mb-4 {
294
295
margin-bottom: calc(var(--spacing) * 4);
295
296
}
296
-
.ml-1 {
297
-
margin-left: calc(var(--spacing) * 1);
298
-
}
299
297
.flex {
300
298
display: flex;
301
299
}
···
314
312
.size-4 {
315
313
width: calc(var(--spacing) * 4);
316
314
height: calc(var(--spacing) * 4);
315
+
}
316
+
.size-7 {
317
+
width: calc(var(--spacing) * 7);
318
+
height: calc(var(--spacing) * 7);
317
319
}
318
320
.size-16 {
319
321
width: 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;
378
386
}
387
+
.shrink-0 {
388
+
flex-shrink: 0;
389
+
}
390
+
.cursor-grab {
391
+
cursor: grab;
392
+
}
379
393
.cursor-pointer {
380
394
cursor: pointer;
381
395
}
···
388
402
.grid-cols-2 {
389
403
grid-template-columns: repeat(2, minmax(0, 1fr));
390
404
}
405
+
.grid-cols-3 {
406
+
grid-template-columns: repeat(3, minmax(0, 1fr));
407
+
}
391
408
.flex-col {
392
409
flex-direction: column;
393
410
}
394
411
.items-center {
395
412
align-items: center;
396
413
}
414
+
.justify-between {
415
+
justify-content: space-between;
416
+
}
397
417
.justify-center {
398
418
justify-content: center;
419
+
}
420
+
.justify-end {
421
+
justify-content: flex-end;
399
422
}
400
423
.gap-2 {
401
424
gap: calc(var(--spacing) * 2);
402
425
}
403
426
.gap-4 {
404
427
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
428
}
413
429
.space-y-2 {
414
430
:where(& > :not(:last-child)) {
···
434
450
.self-start {
435
451
align-self: flex-start;
436
452
}
453
+
.truncate {
454
+
overflow: hidden;
455
+
text-overflow: ellipsis;
456
+
white-space: nowrap;
457
+
}
437
458
.overflow-hidden {
438
459
overflow: hidden;
439
460
}
···
443
464
.border {
444
465
border-style: var(--tw-border-style);
445
466
border-width: 1px;
467
+
}
468
+
.border-b {
469
+
border-bottom-style: var(--tw-border-style);
470
+
border-bottom-width: 1px;
471
+
}
472
+
.border-zinc-100 {
473
+
border-color: var(--color-zinc-100);
446
474
}
447
475
.border-zinc-200 {
448
476
border-color: var(--color-zinc-200);
···
459
487
background-color: color-mix(in oklab, var(--color-black) 80%, transparent);
460
488
}
461
489
}
490
+
.bg-sky-500 {
491
+
background-color: var(--color-sky-500);
492
+
}
462
493
.bg-zinc-100 {
463
494
background-color: var(--color-zinc-100);
464
495
}
···
470
501
}
471
502
.bg-zinc-950 {
472
503
background-color: var(--color-zinc-950);
504
+
}
505
+
.fill-zinc-950 {
506
+
fill: var(--color-zinc-950);
473
507
}
474
508
.object-contain {
475
509
object-fit: contain;
···
500
534
}
501
535
.pt-4 {
502
536
padding-top: calc(var(--spacing) * 4);
537
+
}
538
+
.pb-4 {
539
+
padding-bottom: calc(var(--spacing) * 4);
503
540
}
504
541
.text-center {
505
542
text-align: center;
···
556
593
.text-zinc-900 {
557
594
color: var(--color-zinc-900);
558
595
}
596
+
.text-zinc-950 {
597
+
color: var(--color-zinc-950);
598
+
}
559
599
.lowercase {
560
600
text-transform: lowercase;
561
601
}
562
602
.ring-sky-500 {
563
603
--tw-ring-color: var(--color-sky-500);
604
+
}
605
+
.filter {
606
+
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
564
607
}
565
608
.group-data-\[added\=true\]\:block {
566
609
&:is(:where(.group)[data-added="true"] *) {
···
585
628
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
586
629
}
587
630
}
631
+
.data-\[selected\=false\]\:border-transparent {
632
+
&[data-selected="false"] {
633
+
border-color: transparent;
634
+
}
635
+
}
636
+
.data-\[selected\=false\]\:bg-transparent {
637
+
&[data-selected="false"] {
638
+
background-color: transparent;
639
+
}
640
+
}
588
641
.data-\[state\=pending\]\:opacity-50 {
589
642
&[data-state="pending"] {
590
643
opacity: 50%;
591
-
}
592
-
}
593
-
.sm\:top-1 {
594
-
@media (width >= 40rem) {
595
-
top: calc(var(--spacing) * 1);
596
644
}
597
645
}
598
646
.sm\:right-1 {
···
605
653
bottom: calc(var(--spacing) * 1);
606
654
}
607
655
}
608
-
.sm\:left-1 {
609
-
@media (width >= 40rem) {
610
-
left: calc(var(--spacing) * 1);
611
-
}
612
-
}
613
656
.sm\:h-screen {
614
657
@media (width >= 40rem) {
615
658
height: 100vh;
···
623
666
.sm\:w-fit {
624
667
@media (width >= 40rem) {
625
668
width: fit-content;
669
+
}
670
+
}
671
+
.sm\:max-w-\[400px\] {
672
+
@media (width >= 40rem) {
673
+
max-width: 400px;
626
674
}
627
675
}
628
676
.sm\:grid-cols-3 {
···
675
723
background-color: var(--color-zinc-950);
676
724
}
677
725
}
726
+
.dark\:fill-zinc-50 {
727
+
@media (prefers-color-scheme: dark) {
728
+
fill: var(--color-zinc-50);
729
+
}
730
+
}
678
731
.dark\:text-white {
679
732
@media (prefers-color-scheme: dark) {
680
733
color: var(--color-white);
681
734
}
682
735
}
736
+
.dark\:text-zinc-50 {
737
+
@media (prefers-color-scheme: dark) {
738
+
color: var(--color-zinc-50);
739
+
}
740
+
}
683
741
.dark\:text-zinc-500 {
684
742
@media (prefers-color-scheme: dark) {
685
743
color: var(--color-zinc-500);
686
744
}
687
745
}
688
746
}
689
-
.htmx-request.htmx-indicator {
690
-
display: inline;
691
-
}
692
-
.htmx-indicator {
693
-
display: none;
694
-
}
695
-
.htmx-request #submit-button {
696
-
opacity: 0.5;
697
-
pointer-events: none;
698
-
}
699
747
@property --tw-space-y-reverse {
700
748
syntax: "*";
701
749
inherits: false;
···
715
763
syntax: "*";
716
764
inherits: false;
717
765
}
766
+
@property --tw-blur {
767
+
syntax: "*";
768
+
inherits: false;
769
+
}
770
+
@property --tw-brightness {
771
+
syntax: "*";
772
+
inherits: false;
773
+
}
774
+
@property --tw-contrast {
775
+
syntax: "*";
776
+
inherits: false;
777
+
}
778
+
@property --tw-grayscale {
779
+
syntax: "*";
780
+
inherits: false;
781
+
}
782
+
@property --tw-hue-rotate {
783
+
syntax: "*";
784
+
inherits: false;
785
+
}
786
+
@property --tw-invert {
787
+
syntax: "*";
788
+
inherits: false;
789
+
}
790
+
@property --tw-opacity {
791
+
syntax: "*";
792
+
inherits: false;
793
+
}
794
+
@property --tw-saturate {
795
+
syntax: "*";
796
+
inherits: false;
797
+
}
798
+
@property --tw-sepia {
799
+
syntax: "*";
800
+
inherits: false;
801
+
}
802
+
@property --tw-drop-shadow {
803
+
syntax: "*";
804
+
inherits: false;
805
+
}
806
+
@property --tw-drop-shadow-color {
807
+
syntax: "*";
808
+
inherits: false;
809
+
}
810
+
@property --tw-drop-shadow-alpha {
811
+
syntax: "<percentage>";
812
+
inherits: false;
813
+
initial-value: 100%;
814
+
}
815
+
@property --tw-drop-shadow-size {
816
+
syntax: "*";
817
+
inherits: false;
818
+
}
718
819
@property --tw-shadow {
719
820
syntax: "*";
720
821
inherits: false;
···
787
888
--tw-space-x-reverse: 0;
788
889
--tw-border-style: solid;
789
890
--tw-font-weight: initial;
891
+
--tw-blur: initial;
892
+
--tw-brightness: initial;
893
+
--tw-contrast: initial;
894
+
--tw-grayscale: initial;
895
+
--tw-hue-rotate: initial;
896
+
--tw-invert: initial;
897
+
--tw-opacity: initial;
898
+
--tw-saturate: initial;
899
+
--tw-sepia: initial;
900
+
--tw-drop-shadow: initial;
901
+
--tw-drop-shadow-color: initial;
902
+
--tw-drop-shadow-alpha: 100%;
903
+
--tw-drop-shadow-size: initial;
790
904
--tw-shadow: 0 0 #0000;
791
905
--tw-shadow-color: initial;
792
906
--tw-shadow-alpha: 100%;