+4
-3
index.html
+4
-3
index.html
···
11
<meta property="description" content="Browse the public data on atproto" />
12
<link rel="manifest" href="/manifest.json" />
13
<title>PDSls</title>
14
-
<link rel="preconnect" href="https://rsms.me/" />
15
-
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
16
<link rel="preconnect" href="https://fonts.bunny.net" />
17
<link href="https://fonts.bunny.net/css?family=roboto-mono:400" rel="stylesheet" />
18
<link href="https://fonts.cdnfonts.com/css/pecita" rel="stylesheet" />
···
26
<script src="/src/index.tsx" type="module"></script>
27
</head>
28
29
-
<body id="root" class="dark:bg-dark-500 min-h-screen bg-neutral-100">
30
<noscript>You need to enable JavaScript to run this app.</noscript>
31
</body>
32
</html>
···
11
<meta property="description" content="Browse the public data on atproto" />
12
<link rel="manifest" href="/manifest.json" />
13
<title>PDSls</title>
14
<link rel="preconnect" href="https://fonts.bunny.net" />
15
<link href="https://fonts.bunny.net/css?family=roboto-mono:400" rel="stylesheet" />
16
<link href="https://fonts.cdnfonts.com/css/pecita" rel="stylesheet" />
···
24
<script src="/src/index.tsx" type="module"></script>
25
</head>
26
27
+
<body
28
+
id="root"
29
+
class="dark:bg-dark-500 min-h-screen bg-neutral-100 text-neutral-900 dark:text-neutral-200"
30
+
>
31
<noscript>You need to enable JavaScript to run this app.</noscript>
32
</body>
33
</html>
+21
-20
package.json
+21
-20
package.json
···
9
"serve": "vite preview"
10
},
11
"devDependencies": {
12
-
"@iconify-json/lucide": "^1.2.75",
13
-
"@iconify/tailwind4": "^1.1.0",
14
-
"@tailwindcss/vite": "^4.1.17",
15
-
"prettier": "^3.6.2",
16
"prettier-plugin-organize-imports": "^4.3.0",
17
-
"prettier-plugin-tailwindcss": "^0.7.1",
18
-
"tailwindcss": "^4.1.17",
19
"typescript": "^5.9.3",
20
-
"vite": "^7.2.4",
21
"vite-plugin-solid": "^2.11.10"
22
},
23
"dependencies": {
24
"@atcute/atproto": "^3.1.9",
25
-
"@atcute/bluesky": "^3.2.10",
26
-
"@atcute/client": "^4.0.5",
27
-
"@atcute/crypto": "^2.2.6",
28
"@atcute/did-plc": "^0.2.0",
29
"@atcute/identity": "^1.1.3",
30
-
"@atcute/identity-resolver": "^1.1.4",
31
-
"@atcute/leaflet": "^1.0.12",
32
-
"@atcute/lexicon-doc": "^2.0.1",
33
-
"@atcute/lexicon-resolver": "^0.1.4",
34
-
"@atcute/lexicons": "^1.2.4",
35
-
"@atcute/oauth-browser-client": "^2.0.1",
36
"@atcute/repo": "^0.1.0",
37
-
"@atcute/tangled": "^1.0.12",
38
"@atcute/tid": "^1.0.3",
39
"@codemirror/commands": "^6.10.0",
40
"@codemirror/lang-json": "^6.0.2",
41
"@codemirror/lint": "^6.9.2",
42
"@codemirror/state": "^6.5.2",
43
-
"@codemirror/view": "^6.38.8",
44
-
"@fsegurai/codemirror-theme-basic-dark": "^6.2.2",
45
-
"@fsegurai/codemirror-theme-basic-light": "^6.2.2",
46
"@mary/exif-rm": "jsr:^0.2.2",
47
"@skyware/firehose": "^0.5.2",
48
"@solidjs/meta": "^0.29.4",
···
9
"serve": "vite preview"
10
},
11
"devDependencies": {
12
+
"@iconify-json/lucide": "^1.2.81",
13
+
"@iconify/tailwind4": "^1.2.0",
14
+
"@tailwindcss/vite": "^4.1.18",
15
+
"prettier": "^3.7.4",
16
"prettier-plugin-organize-imports": "^4.3.0",
17
+
"prettier-plugin-tailwindcss": "^0.7.2",
18
+
"tailwindcss": "^4.1.18",
19
"typescript": "^5.9.3",
20
+
"vite": "^7.2.7",
21
"vite-plugin-solid": "^2.11.10"
22
},
23
"dependencies": {
24
"@atcute/atproto": "^3.1.9",
25
+
"@atcute/bluesky": "^3.2.14",
26
+
"@atcute/client": "^4.1.1",
27
+
"@atcute/crypto": "^2.3.0",
28
"@atcute/did-plc": "^0.2.0",
29
"@atcute/identity": "^1.1.3",
30
+
"@atcute/identity-resolver": "^1.2.0",
31
+
"@atcute/leaflet": "^1.0.14",
32
+
"@atcute/lexicon-doc": "^2.0.5",
33
+
"@atcute/lexicon-resolver": "^0.1.5",
34
+
"@atcute/lexicons": "^1.2.5",
35
+
"@atcute/multibase": "^1.1.6",
36
+
"@atcute/oauth-browser-client": "^2.0.3",
37
"@atcute/repo": "^0.1.0",
38
+
"@atcute/tangled": "^1.0.13",
39
"@atcute/tid": "^1.0.3",
40
"@codemirror/commands": "^6.10.0",
41
"@codemirror/lang-json": "^6.0.2",
42
"@codemirror/lint": "^6.9.2",
43
"@codemirror/state": "^6.5.2",
44
+
"@codemirror/view": "^6.39.4",
45
+
"@fsegurai/codemirror-theme-basic-dark": "^6.2.3",
46
+
"@fsegurai/codemirror-theme-basic-light": "^6.2.3",
47
"@mary/exif-rm": "jsr:^0.2.2",
48
"@skyware/firehose": "^0.5.2",
49
"@solidjs/meta": "^0.29.4",
+317
-817
pnpm-lock.yaml
+317
-817
pnpm-lock.yaml
···
12
specifier: ^3.1.9
13
version: 3.1.9
14
'@atcute/bluesky':
15
-
specifier: ^3.2.10
16
-
version: 3.2.10
17
'@atcute/client':
18
-
specifier: ^4.0.5
19
-
version: 4.0.5
20
'@atcute/crypto':
21
-
specifier: ^2.2.6
22
-
version: 2.2.6
23
'@atcute/did-plc':
24
specifier: ^0.2.0
25
version: 0.2.0
···
27
specifier: ^1.1.3
28
version: 1.1.3
29
'@atcute/identity-resolver':
30
-
specifier: ^1.1.4
31
-
version: 1.1.4(@atcute/identity@1.1.3)
32
'@atcute/leaflet':
33
-
specifier: ^1.0.12
34
-
version: 1.0.12
35
'@atcute/lexicon-doc':
36
-
specifier: ^2.0.1
37
-
version: 2.0.1
38
'@atcute/lexicon-resolver':
39
-
specifier: ^0.1.4
40
-
version: 0.1.4(@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)
41
'@atcute/lexicons':
42
-
specifier: ^1.2.4
43
-
version: 1.2.4
44
'@atcute/oauth-browser-client':
45
-
specifier: ^2.0.1
46
-
version: 2.0.1
47
'@atcute/repo':
48
specifier: ^0.1.0
49
version: 0.1.0
50
'@atcute/tangled':
51
-
specifier: ^1.0.12
52
-
version: 1.0.12
53
'@atcute/tid':
54
specifier: ^1.0.3
55
version: 1.0.3
···
66
specifier: ^6.5.2
67
version: 6.5.2
68
'@codemirror/view':
69
-
specifier: ^6.38.8
70
-
version: 6.38.8
71
'@fsegurai/codemirror-theme-basic-dark':
72
-
specifier: ^6.2.2
73
-
version: 6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)
74
'@fsegurai/codemirror-theme-basic-light':
75
-
specifier: ^6.2.2
76
-
version: 6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)
77
'@mary/exif-rm':
78
specifier: jsr:^0.2.2
79
version: '@jsr/mary__exif-rm@0.2.2'
···
94
version: 1.9.10
95
devDependencies:
96
'@iconify-json/lucide':
97
-
specifier: ^1.2.75
98
-
version: 1.2.75
99
'@iconify/tailwind4':
100
-
specifier: ^1.1.0
101
-
version: 1.1.0(tailwindcss@4.1.17)
102
'@tailwindcss/vite':
103
-
specifier: ^4.1.17
104
-
version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))
105
prettier:
106
-
specifier: ^3.6.2
107
-
version: 3.6.2
108
prettier-plugin-organize-imports:
109
specifier: ^4.3.0
110
-
version: 4.3.0(prettier@3.6.2)(typescript@5.9.3)
111
prettier-plugin-tailwindcss:
112
-
specifier: ^0.7.1
113
-
version: 0.7.1(prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3))(prettier@3.6.2)
114
tailwindcss:
115
-
specifier: ^4.1.17
116
-
version: 4.1.17
117
typescript:
118
specifier: ^5.9.3
119
version: 5.9.3
120
vite:
121
-
specifier: ^7.2.4
122
-
version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
123
vite-plugin-solid:
124
specifier: ^2.11.10
125
-
version: 2.11.10(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))
126
127
packages:
128
129
'@antfu/install-pkg@1.1.0':
130
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
131
132
-
'@antfu/utils@8.1.1':
133
-
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
134
-
135
'@atcute/atproto@3.1.9':
136
resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==}
137
138
-
'@atcute/bluesky@3.2.10':
139
-
resolution: {integrity: sha512-qwQWTzRf3umnh2u41gdU+xWYkbzGlKDupc3zeOB+YjmuP1N9wEaUhwS8H7vgrqr0xC9SGNDjeUVcjC4m5BPLBg==}
140
141
'@atcute/car@3.1.3':
142
resolution: {integrity: sha512-WJ13bAEt7TjDMVi09ubjLtvhdljbWInGm9Kfy7Y6NhrmiyC/aZYaA/zHX/bHI6xv1c/h3SQduWqxOr4ae49eqA==}
···
150
'@atcute/cid@2.2.6':
151
resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==}
152
153
-
'@atcute/client@4.0.5':
154
-
resolution: {integrity: sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA==}
155
156
-
'@atcute/crypto@2.2.6':
157
-
resolution: {integrity: sha512-vkuexF+kmrKE1/Uqzub99Qi4QpnxA2jbu60E6PTgL4XypELQ6rb59MB/J1VbY2gs0kd3ET7+L3+NWpKD5nXyfA==}
158
159
'@atcute/did-plc@0.2.0':
160
resolution: {integrity: sha512-1sGek8GRM/Ph7nLVRREm8FqM7g4shGckItvdVwJcRbUa8Rh0zOsXQa0QyYWAC0k40BhkqO9FwKXhJEaXCmF5oQ==}
161
162
-
'@atcute/identity-resolver@1.1.4':
163
-
resolution: {integrity: sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA==}
164
peerDependencies:
165
'@atcute/identity': ^1.0.0
166
167
'@atcute/identity@1.1.3':
168
resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==}
169
170
-
'@atcute/leaflet@1.0.12':
171
-
resolution: {integrity: sha512-T5laBTl8vwzy0eZXBy07IQSjsLqhbZmRJsffnNQ6XMSc+lnCZ/NHfuKy8TNJbDU6dc26Z7o5l0ELfWz5QESo+w==}
172
173
-
'@atcute/lexicon-doc@2.0.1':
174
-
resolution: {integrity: sha512-yWgcBYkvifczVODZSgdVkIljzIfdh50t+QXjkDL/FSu2RP43NGBEZ5xfZqJcT68/UoyE+doSg0dhvOEIlVGU/A==}
175
176
-
'@atcute/lexicon-resolver@0.1.4':
177
-
resolution: {integrity: sha512-8MAN3wrlP+PvyAbzHgzavaGeNNHq/r0LDEV4ABqDozHIZ2pcLR3O3J40UdiGW6ldeC/YciSkkmpl6f/zP3sXzw==}
178
peerDependencies:
179
'@atcute/identity': ^1.1.0
180
'@atcute/identity-resolver': ^1.1.3
181
182
-
'@atcute/lexicons@1.2.4':
183
-
resolution: {integrity: sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ==}
184
185
'@atcute/mst@0.1.0':
186
resolution: {integrity: sha512-h+iDToKEnBpigk2DOHjSqY63vJtjYKUIztqu1CZ0P+I54wV2SrgoqAXAT1xrW6A1Iup8cjTv+U2H5WVG4KxPLw==}
···
188
'@atcute/multibase@1.1.6':
189
resolution: {integrity: sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg==}
190
191
-
'@atcute/oauth-browser-client@2.0.1':
192
-
resolution: {integrity: sha512-lG021GkeORG06zfFf4bH85egObjBEKHNgAWHvbtY/E2dX4wxo88hf370pJDx8acdnuUJLJ2VKPikJtZwo4Heeg==}
193
194
'@atcute/repo@0.1.0':
195
resolution: {integrity: sha512-INiYAuma8dydBu7cqd2WVpcXh3mzhIepYBUqFWAK5MqMulPRLTRCc/9GW3G9pxYrOdlvLCVamG2Jf8XK0nuFEw==}
196
197
-
'@atcute/tangled@1.0.12':
198
-
resolution: {integrity: sha512-JKA5sOhd8SLhDFhY+PKHqLLytQBBKSiwcaEzfYUJBeyfvqXFPNNAwvRbe3VST4IQ3izoOu3O0R9/b1mjL45UzA==}
199
200
'@atcute/tid@1.0.3':
201
resolution: {integrity: sha512-wfMJx1IMdnu0CZgWl0uR4JO2s6PGT1YPhpytD4ZHzEYKKQVuqV6Eb/7vieaVo1eYNMp2FrY67FZObeR7utRl2w==}
202
203
-
'@atcute/uint8array@1.0.5':
204
-
resolution: {integrity: sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q==}
205
206
-
'@atcute/util-fetch@1.0.3':
207
-
resolution: {integrity: sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ==}
208
209
'@atcute/varint@1.0.3':
210
resolution: {integrity: sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog==}
···
315
'@codemirror/state@6.5.2':
316
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
317
318
-
'@codemirror/view@6.38.8':
319
-
resolution: {integrity: sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==}
320
321
'@esbuild/aix-ppc64@0.23.1':
322
resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
···
618
cpu: [x64]
619
os: [win32]
620
621
-
'@fsegurai/codemirror-theme-basic-dark@6.2.2':
622
-
resolution: {integrity: sha512-cVK4VheF7ZkuV0sfy20lmH2S7Q2xIfKoqN2HdU5rpGH8mZM2LVG9Tl+oHT0XNPpsWFqNAAKLzjYFw0IPX95Biw==}
623
peerDependencies:
624
'@codemirror/language': ^6.0.0
625
'@codemirror/state': ^6.0.0
626
'@codemirror/view': ^6.0.0
627
'@lezer/highlight': ^1.0.0
628
629
-
'@fsegurai/codemirror-theme-basic-light@6.2.2':
630
-
resolution: {integrity: sha512-zFtJ6VwwEeZ/HAXMYdcJz6+DdW1aQkngFwbD3diku79cctpTglCWH49KRFO8Mifjzwylsynm7dLyOUnGhIu0NQ==}
631
peerDependencies:
632
'@codemirror/language': ^6.0.0
633
'@codemirror/state': ^6.0.0
634
'@codemirror/view': ^6.0.0
635
'@lezer/highlight': ^1.0.0
636
637
-
'@iconify-json/lucide@1.2.75':
638
-
resolution: {integrity: sha512-sWBN0t/rTo1FxWG/46xKgkIcDerHpsjyNgMH48nvtC4/kUG88sFQXI+7mxX3SD8eSUaQQ2kS9C7ZKWm2DKgBlw==}
639
640
-
'@iconify/tailwind4@1.1.0':
641
-
resolution: {integrity: sha512-HqgAYtYk4eFtLvdYfhQrBRT9ohToh+VJJVhHtJ7B4Qhw+J+mRPvGC9Wr6Cgtb36YbIWqBxWuBaAUw9TE/8m2/w==}
642
peerDependencies:
643
tailwindcss: '>= 4.0.0'
644
645
-
'@iconify/tools@4.1.4':
646
-
resolution: {integrity: sha512-s6BcNUcCxQ3S6cvhlsoWzOuBt8qKXdVyXB9rT57uSJ/ARHD7dVM43+5ERBWn3tmkMWXeJ/s9DPVc3dUasayzeA==}
647
648
'@iconify/types@2.0.0':
649
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
650
651
-
'@iconify/utils@2.3.0':
652
-
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
653
654
'@jridgewell/gen-mapping@0.3.13':
655
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
···
670
'@jsr/mary__exif-rm@0.2.2':
671
resolution: {integrity: sha512-+ZpLaC+1CyqWhH608Sqd6/yTG0pOlokn2tCXha7s1SMQ+GLKo4Nn/PskTeeP9Pt+6gNYSu6ednoSlRvXb2ZGxg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__exif-rm/0.2.2.tgz}
672
673
-
'@lezer/common@1.3.0':
674
-
resolution: {integrity: sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==}
675
676
'@lezer/highlight@1.2.3':
677
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
···
679
'@lezer/json@1.0.3':
680
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
681
682
-
'@lezer/lr@1.4.3':
683
-
resolution: {integrity: sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==}
684
685
'@marijn/find-cluster-break@1.0.2':
686
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
···
814
'@standard-schema/spec@1.0.0':
815
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
816
817
-
'@tailwindcss/node@4.1.17':
818
-
resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==}
819
820
-
'@tailwindcss/oxide-android-arm64@4.1.17':
821
-
resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==}
822
engines: {node: '>= 10'}
823
cpu: [arm64]
824
os: [android]
825
826
-
'@tailwindcss/oxide-darwin-arm64@4.1.17':
827
-
resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==}
828
engines: {node: '>= 10'}
829
cpu: [arm64]
830
os: [darwin]
831
832
-
'@tailwindcss/oxide-darwin-x64@4.1.17':
833
-
resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==}
834
engines: {node: '>= 10'}
835
cpu: [x64]
836
os: [darwin]
837
838
-
'@tailwindcss/oxide-freebsd-x64@4.1.17':
839
-
resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==}
840
engines: {node: '>= 10'}
841
cpu: [x64]
842
os: [freebsd]
843
844
-
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17':
845
-
resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==}
846
engines: {node: '>= 10'}
847
cpu: [arm]
848
os: [linux]
849
850
-
'@tailwindcss/oxide-linux-arm64-gnu@4.1.17':
851
-
resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==}
852
engines: {node: '>= 10'}
853
cpu: [arm64]
854
os: [linux]
855
856
-
'@tailwindcss/oxide-linux-arm64-musl@4.1.17':
857
-
resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==}
858
engines: {node: '>= 10'}
859
cpu: [arm64]
860
os: [linux]
861
862
-
'@tailwindcss/oxide-linux-x64-gnu@4.1.17':
863
-
resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==}
864
engines: {node: '>= 10'}
865
cpu: [x64]
866
os: [linux]
867
868
-
'@tailwindcss/oxide-linux-x64-musl@4.1.17':
869
-
resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==}
870
engines: {node: '>= 10'}
871
cpu: [x64]
872
os: [linux]
873
874
-
'@tailwindcss/oxide-wasm32-wasi@4.1.17':
875
-
resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==}
876
engines: {node: '>=14.0.0'}
877
cpu: [wasm32]
878
bundledDependencies:
···
883
- '@emnapi/wasi-threads'
884
- tslib
885
886
-
'@tailwindcss/oxide-win32-arm64-msvc@4.1.17':
887
-
resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==}
888
engines: {node: '>= 10'}
889
cpu: [arm64]
890
os: [win32]
891
892
-
'@tailwindcss/oxide-win32-x64-msvc@4.1.17':
893
-
resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==}
894
engines: {node: '>= 10'}
895
cpu: [x64]
896
os: [win32]
897
898
-
'@tailwindcss/oxide@4.1.17':
899
-
resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==}
900
engines: {node: '>= 10'}
901
902
-
'@tailwindcss/vite@4.1.17':
903
-
resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==}
904
peerDependencies:
905
vite: ^5.2.0 || ^6 || ^7
906
907
-
'@trysound/sax@0.2.0':
908
-
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
909
-
engines: {node: '>=10.13.0'}
910
-
911
'@types/babel__core@7.20.5':
912
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
913
···
926
'@types/node@24.10.1':
927
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
928
929
-
'@types/tar@6.1.13':
930
-
resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==}
931
-
932
-
'@types/yauzl@2.10.3':
933
-
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
934
-
935
acorn@8.15.0:
936
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
937
engines: {node: '>=0.4.0'}
938
hasBin: true
939
940
-
asynckit@0.4.0:
941
-
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
942
-
943
-
axios@1.13.2:
944
-
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
945
-
946
babel-plugin-jsx-dom-expressions@0.40.3:
947
resolution: {integrity: sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==}
948
peerDependencies:
···
957
solid-js:
958
optional: true
959
960
-
baseline-browser-mapping@2.8.31:
961
-
resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==}
962
hasBin: true
963
964
boolbase@1.0.0:
965
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
966
967
-
browserslist@4.28.0:
968
-
resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==}
969
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
970
hasBin: true
971
972
-
buffer-crc32@0.2.13:
973
-
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
974
-
975
-
call-bind-apply-helpers@1.0.2:
976
-
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
977
-
engines: {node: '>= 0.4'}
978
-
979
-
caniuse-lite@1.0.30001757:
980
-
resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==}
981
-
982
-
cheerio-select@2.1.0:
983
-
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
984
-
985
-
cheerio@1.0.0:
986
-
resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==}
987
-
engines: {node: '>=18.17'}
988
-
989
-
chownr@2.0.0:
990
-
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
991
-
engines: {node: '>=10'}
992
993
codemirror@6.0.2:
994
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
995
996
-
combined-stream@1.0.8:
997
-
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
998
-
engines: {node: '>= 0.8'}
999
-
1000
-
commander@7.2.0:
1001
-
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
1002
-
engines: {node: '>= 10'}
1003
1004
confbox@0.1.8:
1005
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
1006
1007
-
confbox@0.2.2:
1008
-
resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
1009
-
1010
convert-source-map@2.0.0:
1011
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
1012
···
1020
resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
1021
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
1022
1023
-
css-tree@2.3.1:
1024
-
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
1025
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
1026
1027
css-what@6.2.2:
···
1044
supports-color:
1045
optional: true
1046
1047
-
delayed-stream@1.0.0:
1048
-
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
1049
-
engines: {node: '>=0.4.0'}
1050
-
1051
detect-libc@2.1.2:
1052
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
1053
engines: {node: '>=8'}
···
1065
domutils@3.2.2:
1066
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
1067
1068
-
dunder-proto@1.0.1:
1069
-
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
1070
-
engines: {node: '>= 0.4'}
1071
-
1072
-
electron-to-chromium@1.5.259:
1073
-
resolution: {integrity: sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==}
1074
-
1075
-
encoding-sniffer@0.2.1:
1076
-
resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==}
1077
-
1078
-
end-of-stream@1.4.5:
1079
-
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
1080
1081
-
enhanced-resolve@5.18.3:
1082
-
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
1083
engines: {node: '>=10.13.0'}
1084
1085
entities@4.5.0:
···
1089
entities@6.0.1:
1090
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
1091
engines: {node: '>=0.12'}
1092
-
1093
-
es-define-property@1.0.1:
1094
-
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
1095
-
engines: {node: '>= 0.4'}
1096
-
1097
-
es-errors@1.3.0:
1098
-
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
1099
-
engines: {node: '>= 0.4'}
1100
-
1101
-
es-object-atoms@1.1.1:
1102
-
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
1103
-
engines: {node: '>= 0.4'}
1104
-
1105
-
es-set-tostringtag@2.1.0:
1106
-
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
1107
-
engines: {node: '>= 0.4'}
1108
1109
esbuild@0.23.1:
1110
resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==}
···
1123
esm-env@1.2.2:
1124
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
1125
1126
-
exsolve@1.0.8:
1127
-
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
1128
-
1129
-
extract-zip@2.0.1:
1130
-
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
1131
-
engines: {node: '>= 10.17.0'}
1132
-
hasBin: true
1133
-
1134
-
fd-slicer@1.1.0:
1135
-
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
1136
-
1137
fdir@6.5.0:
1138
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
1139
engines: {node: '>=12.0.0'}
···
1143
picomatch:
1144
optional: true
1145
1146
-
follow-redirects@1.15.11:
1147
-
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
1148
-
engines: {node: '>=4.0'}
1149
-
peerDependencies:
1150
-
debug: '*'
1151
-
peerDependenciesMeta:
1152
-
debug:
1153
-
optional: true
1154
-
1155
-
form-data@4.0.5:
1156
-
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
1157
-
engines: {node: '>= 6'}
1158
-
1159
-
fs-minipass@2.1.0:
1160
-
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
1161
-
engines: {node: '>= 8'}
1162
1163
fsevents@2.3.3:
1164
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
1165
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
1166
os: [darwin]
1167
1168
-
function-bind@1.1.2:
1169
-
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
1170
-
1171
gensync@1.0.0-beta.2:
1172
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
1173
engines: {node: '>=6.9.0'}
1174
1175
-
get-intrinsic@1.3.0:
1176
-
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
1177
-
engines: {node: '>= 0.4'}
1178
-
1179
-
get-proto@1.0.1:
1180
-
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
1181
-
engines: {node: '>= 0.4'}
1182
-
1183
-
get-stream@5.2.0:
1184
-
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
1185
-
engines: {node: '>=8'}
1186
-
1187
get-tsconfig@4.13.0:
1188
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
1189
1190
-
globals@15.15.0:
1191
-
resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
1192
-
engines: {node: '>=18'}
1193
-
1194
-
gopd@1.2.0:
1195
-
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
1196
-
engines: {node: '>= 0.4'}
1197
-
1198
graceful-fs@4.2.11:
1199
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
1200
1201
-
has-symbols@1.1.0:
1202
-
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
1203
-
engines: {node: '>= 0.4'}
1204
-
1205
-
has-tostringtag@1.0.2:
1206
-
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
1207
-
engines: {node: '>= 0.4'}
1208
-
1209
-
hasown@2.0.2:
1210
-
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
1211
-
engines: {node: '>= 0.4'}
1212
-
1213
html-entities@2.3.3:
1214
resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==}
1215
1216
-
htmlparser2@9.1.0:
1217
-
resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==}
1218
-
1219
-
iconv-lite@0.6.3:
1220
-
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
1221
-
engines: {node: '>=0.10.0'}
1222
-
1223
is-what@4.1.16:
1224
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
1225
engines: {node: '>=12.13'}
···
1240
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
1241
engines: {node: '>=6'}
1242
hasBin: true
1243
-
1244
-
kolorist@1.8.0:
1245
-
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
1246
1247
lightningcss-android-arm64@1.30.2:
1248
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
···
1314
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
1315
engines: {node: '>= 12.0.0'}
1316
1317
-
local-pkg@0.5.1:
1318
-
resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==}
1319
-
engines: {node: '>=14'}
1320
-
1321
-
local-pkg@1.1.2:
1322
-
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
1323
-
engines: {node: '>=14'}
1324
-
1325
lru-cache@5.1.1:
1326
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
1327
1328
magic-string@0.30.21:
1329
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
1330
1331
-
math-intrinsics@1.1.0:
1332
-
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
1333
-
engines: {node: '>= 0.4'}
1334
-
1335
mdn-data@2.0.28:
1336
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
1337
1338
-
mdn-data@2.0.30:
1339
-
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
1340
1341
merge-anything@5.1.7:
1342
resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==}
1343
engines: {node: '>=12.13'}
1344
1345
-
mime-db@1.52.0:
1346
-
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
1347
-
engines: {node: '>= 0.6'}
1348
-
1349
-
mime-types@2.1.35:
1350
-
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
1351
-
engines: {node: '>= 0.6'}
1352
-
1353
-
minipass@3.3.6:
1354
-
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
1355
-
engines: {node: '>=8'}
1356
-
1357
-
minipass@4.2.8:
1358
-
resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==}
1359
-
engines: {node: '>=8'}
1360
-
1361
-
minipass@5.0.0:
1362
-
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
1363
-
engines: {node: '>=8'}
1364
-
1365
-
minizlib@2.1.2:
1366
-
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
1367
-
engines: {node: '>= 8'}
1368
-
1369
-
mkdirp@1.0.4:
1370
-
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
1371
-
engines: {node: '>=10'}
1372
-
hasBin: true
1373
-
1374
mlly@1.8.0:
1375
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
1376
1377
ms@2.1.3:
1378
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
···
1397
nth-check@2.1.1:
1398
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
1399
1400
-
once@1.4.0:
1401
-
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
1402
-
1403
-
package-manager-detector@1.5.0:
1404
-
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
1405
-
1406
-
parse5-htmlparser2-tree-adapter@7.1.0:
1407
-
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
1408
-
1409
-
parse5-parser-stream@7.1.2:
1410
-
resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==}
1411
1412
parse5@7.3.0:
1413
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
1414
1415
-
pathe@1.1.2:
1416
-
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
1417
-
1418
pathe@2.0.3:
1419
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
1420
-
1421
-
pend@1.2.0:
1422
-
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
1423
1424
picocolors@1.1.1:
1425
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
···
1431
pkg-types@1.3.1:
1432
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
1433
1434
-
pkg-types@2.3.0:
1435
-
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
1436
-
1437
postcss@8.5.6:
1438
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
1439
engines: {node: ^10 || ^12 || >=14}
···
1448
vue-tsc:
1449
optional: true
1450
1451
-
prettier-plugin-tailwindcss@0.7.1:
1452
-
resolution: {integrity: sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==}
1453
engines: {node: '>=20.19'}
1454
peerDependencies:
1455
'@ianvs/prettier-plugin-sort-imports': '*'
···
1503
prettier-plugin-svelte:
1504
optional: true
1505
1506
-
prettier@3.6.2:
1507
-
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
1508
engines: {node: '>=14'}
1509
hasBin: true
1510
1511
-
proxy-from-env@1.1.0:
1512
-
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
1513
-
1514
-
pump@3.0.3:
1515
-
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
1516
-
1517
-
quansync@0.2.11:
1518
-
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
1519
-
1520
resolve-pkg-maps@1.0.0:
1521
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
1522
···
1525
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1526
hasBin: true
1527
1528
-
safer-buffer@2.1.2:
1529
-
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
1530
1531
semver@6.3.1:
1532
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
···
1557
style-mod@4.1.3:
1558
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
1559
1560
-
svgo@3.3.2:
1561
-
resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
1562
-
engines: {node: '>=14.0.0'}
1563
hasBin: true
1564
1565
-
tailwindcss@4.1.17:
1566
-
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
1567
1568
tapable@2.3.0:
1569
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
1570
engines: {node: '>=6'}
1571
1572
-
tar@6.2.1:
1573
-
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
1574
-
engines: {node: '>=10'}
1575
-
1576
tinyexec@1.0.2:
1577
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
1578
engines: {node: '>=18'}
···
1597
undici-types@7.16.0:
1598
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
1599
1600
-
undici@6.22.0:
1601
-
resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==}
1602
-
engines: {node: '>=18.17'}
1603
-
1604
-
update-browserslist-db@1.1.4:
1605
-
resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==}
1606
hasBin: true
1607
peerDependencies:
1608
browserslist: '>= 4.21.0'
···
1617
'@testing-library/jest-dom':
1618
optional: true
1619
1620
-
vite@7.2.4:
1621
-
resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==}
1622
engines: {node: ^20.19.0 || >=22.12.0}
1623
hasBin: true
1624
peerDependencies:
···
1668
w3c-keyname@2.2.8:
1669
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
1670
1671
-
whatwg-encoding@3.1.1:
1672
-
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
1673
-
engines: {node: '>=18'}
1674
-
1675
-
whatwg-mimetype@4.0.0:
1676
-
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
1677
-
engines: {node: '>=18'}
1678
-
1679
-
wrappy@1.0.2:
1680
-
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
1681
-
1682
yallist@3.1.1:
1683
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
1684
1685
-
yallist@4.0.0:
1686
-
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
1687
-
1688
-
yauzl@2.10.0:
1689
-
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
1690
-
1691
yocto-queue@1.2.2:
1692
resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
1693
engines: {node: '>=12.20'}
···
1696
1697
'@antfu/install-pkg@1.1.0':
1698
dependencies:
1699
-
package-manager-detector: 1.5.0
1700
tinyexec: 1.0.2
1701
1702
-
'@antfu/utils@8.1.1': {}
1703
-
1704
'@atcute/atproto@3.1.9':
1705
dependencies:
1706
-
'@atcute/lexicons': 1.2.4
1707
1708
-
'@atcute/bluesky@3.2.10':
1709
dependencies:
1710
'@atcute/atproto': 3.1.9
1711
-
'@atcute/lexicons': 1.2.4
1712
1713
'@atcute/car@3.1.3':
1714
dependencies:
1715
'@atcute/cbor': 2.2.8
1716
'@atcute/cid': 2.2.6
1717
-
'@atcute/uint8array': 1.0.5
1718
'@atcute/varint': 1.0.3
1719
yocto-queue: 1.2.2
1720
···
1722
dependencies:
1723
'@atcute/cbor': 2.2.8
1724
'@atcute/cid': 2.2.6
1725
-
'@atcute/uint8array': 1.0.5
1726
'@atcute/varint': 1.0.3
1727
1728
'@atcute/cbor@2.2.8':
1729
dependencies:
1730
'@atcute/cid': 2.2.6
1731
'@atcute/multibase': 1.1.6
1732
-
'@atcute/uint8array': 1.0.5
1733
1734
'@atcute/cid@2.2.6':
1735
dependencies:
1736
'@atcute/multibase': 1.1.6
1737
-
'@atcute/uint8array': 1.0.5
1738
1739
-
'@atcute/client@4.0.5':
1740
dependencies:
1741
'@atcute/identity': 1.1.3
1742
-
'@atcute/lexicons': 1.2.4
1743
1744
-
'@atcute/crypto@2.2.6':
1745
dependencies:
1746
'@atcute/multibase': 1.1.6
1747
-
'@atcute/uint8array': 1.0.5
1748
'@noble/secp256k1': 3.0.0
1749
1750
'@atcute/did-plc@0.2.0':
1751
dependencies:
1752
'@atcute/cbor': 2.2.8
1753
'@atcute/cid': 2.2.6
1754
-
'@atcute/crypto': 2.2.6
1755
'@atcute/identity': 1.1.3
1756
-
'@atcute/lexicons': 1.2.4
1757
'@atcute/multibase': 1.1.6
1758
-
'@atcute/uint8array': 1.0.5
1759
'@badrap/valita': 0.4.6
1760
1761
-
'@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3)':
1762
dependencies:
1763
'@atcute/identity': 1.1.3
1764
-
'@atcute/lexicons': 1.2.4
1765
-
'@atcute/util-fetch': 1.0.3
1766
'@badrap/valita': 0.4.6
1767
1768
'@atcute/identity@1.1.3':
1769
dependencies:
1770
-
'@atcute/lexicons': 1.2.4
1771
'@badrap/valita': 0.4.6
1772
1773
-
'@atcute/leaflet@1.0.12':
1774
dependencies:
1775
'@atcute/atproto': 3.1.9
1776
-
'@atcute/lexicons': 1.2.4
1777
1778
-
'@atcute/lexicon-doc@2.0.1':
1779
dependencies:
1780
'@atcute/identity': 1.1.3
1781
-
'@atcute/lexicons': 1.2.4
1782
'@badrap/valita': 0.4.6
1783
1784
-
'@atcute/lexicon-resolver@0.1.4(@atcute/identity-resolver@1.1.4(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)':
1785
dependencies:
1786
-
'@atcute/crypto': 2.2.6
1787
'@atcute/identity': 1.1.3
1788
-
'@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3)
1789
-
'@atcute/lexicon-doc': 2.0.1
1790
-
'@atcute/lexicons': 1.2.4
1791
'@atcute/repo': 0.1.0
1792
-
'@atcute/util-fetch': 1.0.3
1793
'@badrap/valita': 0.4.6
1794
1795
-
'@atcute/lexicons@1.2.4':
1796
dependencies:
1797
'@standard-schema/spec': 1.0.0
1798
esm-env: 1.2.2
···
1801
dependencies:
1802
'@atcute/cbor': 2.2.8
1803
'@atcute/cid': 2.2.6
1804
-
'@atcute/uint8array': 1.0.5
1805
1806
'@atcute/multibase@1.1.6':
1807
dependencies:
1808
-
'@atcute/uint8array': 1.0.5
1809
1810
-
'@atcute/oauth-browser-client@2.0.1':
1811
dependencies:
1812
-
'@atcute/client': 4.0.5
1813
-
'@atcute/identity': 1.1.3
1814
-
'@atcute/identity-resolver': 1.1.4(@atcute/identity@1.1.3)
1815
-
'@atcute/lexicons': 1.2.4
1816
'@atcute/multibase': 1.1.6
1817
-
'@atcute/uint8array': 1.0.5
1818
nanoid: 5.1.6
1819
1820
'@atcute/repo@0.1.0':
1821
dependencies:
1822
'@atcute/car': 5.0.0
1823
'@atcute/cbor': 2.2.8
1824
'@atcute/cid': 2.2.6
1825
-
'@atcute/crypto': 2.2.6
1826
-
'@atcute/lexicons': 1.2.4
1827
'@atcute/mst': 0.1.0
1828
-
'@atcute/uint8array': 1.0.5
1829
1830
-
'@atcute/tangled@1.0.12':
1831
dependencies:
1832
'@atcute/atproto': 3.1.9
1833
-
'@atcute/lexicons': 1.2.4
1834
1835
'@atcute/tid@1.0.3': {}
1836
1837
-
'@atcute/uint8array@1.0.5': {}
1838
1839
-
'@atcute/util-fetch@1.0.3':
1840
dependencies:
1841
'@badrap/valita': 0.4.6
1842
···
1882
dependencies:
1883
'@babel/compat-data': 7.28.5
1884
'@babel/helper-validator-option': 7.27.1
1885
-
browserslist: 4.28.0
1886
lru-cache: 5.1.1
1887
semver: 6.3.1
1888
···
1959
dependencies:
1960
'@codemirror/language': 6.11.3
1961
'@codemirror/state': 6.5.2
1962
-
'@codemirror/view': 6.38.8
1963
-
'@lezer/common': 1.3.0
1964
1965
'@codemirror/commands@6.10.0':
1966
dependencies:
1967
'@codemirror/language': 6.11.3
1968
'@codemirror/state': 6.5.2
1969
-
'@codemirror/view': 6.38.8
1970
-
'@lezer/common': 1.3.0
1971
1972
'@codemirror/lang-json@6.0.2':
1973
dependencies:
···
1977
'@codemirror/language@6.11.3':
1978
dependencies:
1979
'@codemirror/state': 6.5.2
1980
-
'@codemirror/view': 6.38.8
1981
-
'@lezer/common': 1.3.0
1982
'@lezer/highlight': 1.2.3
1983
-
'@lezer/lr': 1.4.3
1984
style-mod: 4.1.3
1985
1986
'@codemirror/lint@6.9.2':
1987
dependencies:
1988
'@codemirror/state': 6.5.2
1989
-
'@codemirror/view': 6.38.8
1990
crelt: 1.0.6
1991
1992
'@codemirror/search@6.5.11':
1993
dependencies:
1994
'@codemirror/state': 6.5.2
1995
-
'@codemirror/view': 6.38.8
1996
crelt: 1.0.6
1997
1998
'@codemirror/state@6.5.2':
1999
dependencies:
2000
'@marijn/find-cluster-break': 1.0.2
2001
2002
-
'@codemirror/view@6.38.8':
2003
dependencies:
2004
'@codemirror/state': 6.5.2
2005
crelt: 1.0.6
2006
style-mod: 4.1.3
2007
w3c-keyname: 2.2.8
2008
2009
'@esbuild/aix-ppc64@0.23.1':
2010
optional: true
···
2156
'@esbuild/win32-x64@0.25.12':
2157
optional: true
2158
2159
-
'@fsegurai/codemirror-theme-basic-dark@6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)':
2160
dependencies:
2161
'@codemirror/language': 6.11.3
2162
'@codemirror/state': 6.5.2
2163
-
'@codemirror/view': 6.38.8
2164
'@lezer/highlight': 1.2.3
2165
2166
-
'@fsegurai/codemirror-theme-basic-light@6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)':
2167
dependencies:
2168
'@codemirror/language': 6.11.3
2169
'@codemirror/state': 6.5.2
2170
-
'@codemirror/view': 6.38.8
2171
'@lezer/highlight': 1.2.3
2172
2173
-
'@iconify-json/lucide@1.2.75':
2174
dependencies:
2175
'@iconify/types': 2.0.0
2176
2177
-
'@iconify/tailwind4@1.1.0(tailwindcss@4.1.17)':
2178
dependencies:
2179
-
'@iconify/tools': 4.1.4
2180
'@iconify/types': 2.0.0
2181
-
'@iconify/utils': 2.3.0
2182
-
tailwindcss: 4.1.17
2183
-
transitivePeerDependencies:
2184
-
- debug
2185
-
- supports-color
2186
2187
-
'@iconify/tools@4.1.4':
2188
dependencies:
2189
'@iconify/types': 2.0.0
2190
-
'@iconify/utils': 2.3.0
2191
-
'@types/tar': 6.1.13
2192
-
axios: 1.13.2
2193
-
cheerio: 1.0.0
2194
-
domhandler: 5.0.3
2195
-
extract-zip: 2.0.1
2196
-
local-pkg: 0.5.1
2197
-
pathe: 1.1.2
2198
-
svgo: 3.3.2
2199
-
tar: 6.2.1
2200
-
transitivePeerDependencies:
2201
-
- debug
2202
-
- supports-color
2203
2204
'@iconify/types@2.0.0': {}
2205
2206
-
'@iconify/utils@2.3.0':
2207
dependencies:
2208
'@antfu/install-pkg': 1.1.0
2209
-
'@antfu/utils': 8.1.1
2210
'@iconify/types': 2.0.0
2211
-
debug: 4.4.3
2212
-
globals: 15.15.0
2213
-
kolorist: 1.8.0
2214
-
local-pkg: 1.1.2
2215
mlly: 1.8.0
2216
-
transitivePeerDependencies:
2217
-
- supports-color
2218
2219
'@jridgewell/gen-mapping@0.3.13':
2220
dependencies:
···
2237
2238
'@jsr/mary__exif-rm@0.2.2': {}
2239
2240
-
'@lezer/common@1.3.0': {}
2241
2242
'@lezer/highlight@1.2.3':
2243
dependencies:
2244
-
'@lezer/common': 1.3.0
2245
2246
'@lezer/json@1.0.3':
2247
dependencies:
2248
-
'@lezer/common': 1.3.0
2249
'@lezer/highlight': 1.2.3
2250
-
'@lezer/lr': 1.4.3
2251
2252
-
'@lezer/lr@1.4.3':
2253
dependencies:
2254
-
'@lezer/common': 1.3.0
2255
2256
'@marijn/find-cluster-break@1.0.2': {}
2257
···
2339
2340
'@standard-schema/spec@1.0.0': {}
2341
2342
-
'@tailwindcss/node@4.1.17':
2343
dependencies:
2344
'@jridgewell/remapping': 2.3.5
2345
-
enhanced-resolve: 5.18.3
2346
jiti: 2.6.1
2347
lightningcss: 1.30.2
2348
magic-string: 0.30.21
2349
source-map-js: 1.2.1
2350
-
tailwindcss: 4.1.17
2351
2352
-
'@tailwindcss/oxide-android-arm64@4.1.17':
2353
optional: true
2354
2355
-
'@tailwindcss/oxide-darwin-arm64@4.1.17':
2356
optional: true
2357
2358
-
'@tailwindcss/oxide-darwin-x64@4.1.17':
2359
optional: true
2360
2361
-
'@tailwindcss/oxide-freebsd-x64@4.1.17':
2362
optional: true
2363
2364
-
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17':
2365
optional: true
2366
2367
-
'@tailwindcss/oxide-linux-arm64-gnu@4.1.17':
2368
optional: true
2369
2370
-
'@tailwindcss/oxide-linux-arm64-musl@4.1.17':
2371
optional: true
2372
2373
-
'@tailwindcss/oxide-linux-x64-gnu@4.1.17':
2374
optional: true
2375
2376
-
'@tailwindcss/oxide-linux-x64-musl@4.1.17':
2377
optional: true
2378
2379
-
'@tailwindcss/oxide-wasm32-wasi@4.1.17':
2380
optional: true
2381
2382
-
'@tailwindcss/oxide-win32-arm64-msvc@4.1.17':
2383
optional: true
2384
2385
-
'@tailwindcss/oxide-win32-x64-msvc@4.1.17':
2386
optional: true
2387
2388
-
'@tailwindcss/oxide@4.1.17':
2389
optionalDependencies:
2390
-
'@tailwindcss/oxide-android-arm64': 4.1.17
2391
-
'@tailwindcss/oxide-darwin-arm64': 4.1.17
2392
-
'@tailwindcss/oxide-darwin-x64': 4.1.17
2393
-
'@tailwindcss/oxide-freebsd-x64': 4.1.17
2394
-
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17
2395
-
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.17
2396
-
'@tailwindcss/oxide-linux-arm64-musl': 4.1.17
2397
-
'@tailwindcss/oxide-linux-x64-gnu': 4.1.17
2398
-
'@tailwindcss/oxide-linux-x64-musl': 4.1.17
2399
-
'@tailwindcss/oxide-wasm32-wasi': 4.1.17
2400
-
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.17
2401
-
'@tailwindcss/oxide-win32-x64-msvc': 4.1.17
2402
2403
-
'@tailwindcss/vite@4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))':
2404
dependencies:
2405
-
'@tailwindcss/node': 4.1.17
2406
-
'@tailwindcss/oxide': 4.1.17
2407
-
tailwindcss: 4.1.17
2408
-
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
2409
-
2410
-
'@trysound/sax@0.2.0': {}
2411
2412
'@types/babel__core@7.20.5':
2413
dependencies:
···
2435
'@types/node@24.10.1':
2436
dependencies:
2437
undici-types: 7.16.0
2438
-
2439
-
'@types/tar@6.1.13':
2440
-
dependencies:
2441
-
'@types/node': 24.10.1
2442
-
minipass: 4.2.8
2443
-
2444
-
'@types/yauzl@2.10.3':
2445
-
dependencies:
2446
-
'@types/node': 24.10.1
2447
optional: true
2448
2449
acorn@8.15.0: {}
2450
-
2451
-
asynckit@0.4.0: {}
2452
-
2453
-
axios@1.13.2:
2454
-
dependencies:
2455
-
follow-redirects: 1.15.11
2456
-
form-data: 4.0.5
2457
-
proxy-from-env: 1.1.0
2458
-
transitivePeerDependencies:
2459
-
- debug
2460
2461
babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.5):
2462
dependencies:
···
2474
optionalDependencies:
2475
solid-js: 1.9.10
2476
2477
-
baseline-browser-mapping@2.8.31: {}
2478
2479
boolbase@1.0.0: {}
2480
2481
-
browserslist@4.28.0:
2482
dependencies:
2483
-
baseline-browser-mapping: 2.8.31
2484
-
caniuse-lite: 1.0.30001757
2485
-
electron-to-chromium: 1.5.259
2486
node-releases: 2.0.27
2487
-
update-browserslist-db: 1.1.4(browserslist@4.28.0)
2488
2489
-
buffer-crc32@0.2.13: {}
2490
-
2491
-
call-bind-apply-helpers@1.0.2:
2492
-
dependencies:
2493
-
es-errors: 1.3.0
2494
-
function-bind: 1.1.2
2495
-
2496
-
caniuse-lite@1.0.30001757: {}
2497
-
2498
-
cheerio-select@2.1.0:
2499
-
dependencies:
2500
-
boolbase: 1.0.0
2501
-
css-select: 5.2.2
2502
-
css-what: 6.2.2
2503
-
domelementtype: 2.3.0
2504
-
domhandler: 5.0.3
2505
-
domutils: 3.2.2
2506
-
2507
-
cheerio@1.0.0:
2508
-
dependencies:
2509
-
cheerio-select: 2.1.0
2510
-
dom-serializer: 2.0.0
2511
-
domhandler: 5.0.3
2512
-
domutils: 3.2.2
2513
-
encoding-sniffer: 0.2.1
2514
-
htmlparser2: 9.1.0
2515
-
parse5: 7.3.0
2516
-
parse5-htmlparser2-tree-adapter: 7.1.0
2517
-
parse5-parser-stream: 7.1.2
2518
-
undici: 6.22.0
2519
-
whatwg-mimetype: 4.0.0
2520
-
2521
-
chownr@2.0.0: {}
2522
2523
codemirror@6.0.2:
2524
dependencies:
···
2528
'@codemirror/lint': 6.9.2
2529
'@codemirror/search': 6.5.11
2530
'@codemirror/state': 6.5.2
2531
-
'@codemirror/view': 6.38.8
2532
2533
-
combined-stream@1.0.8:
2534
-
dependencies:
2535
-
delayed-stream: 1.0.0
2536
-
2537
-
commander@7.2.0: {}
2538
2539
confbox@0.1.8: {}
2540
-
2541
-
confbox@0.2.2: {}
2542
2543
convert-source-map@2.0.0: {}
2544
···
2557
mdn-data: 2.0.28
2558
source-map-js: 1.2.1
2559
2560
-
css-tree@2.3.1:
2561
dependencies:
2562
-
mdn-data: 2.0.30
2563
source-map-js: 1.2.1
2564
2565
css-what@6.2.2: {}
···
2573
debug@4.4.3:
2574
dependencies:
2575
ms: 2.1.3
2576
-
2577
-
delayed-stream@1.0.0: {}
2578
2579
detect-libc@2.1.2: {}
2580
···
2596
domelementtype: 2.3.0
2597
domhandler: 5.0.3
2598
2599
-
dunder-proto@1.0.1:
2600
-
dependencies:
2601
-
call-bind-apply-helpers: 1.0.2
2602
-
es-errors: 1.3.0
2603
-
gopd: 1.2.0
2604
2605
-
electron-to-chromium@1.5.259: {}
2606
-
2607
-
encoding-sniffer@0.2.1:
2608
-
dependencies:
2609
-
iconv-lite: 0.6.3
2610
-
whatwg-encoding: 3.1.1
2611
-
2612
-
end-of-stream@1.4.5:
2613
-
dependencies:
2614
-
once: 1.4.0
2615
-
2616
-
enhanced-resolve@5.18.3:
2617
dependencies:
2618
graceful-fs: 4.2.11
2619
tapable: 2.3.0
···
2621
entities@4.5.0: {}
2622
2623
entities@6.0.1: {}
2624
-
2625
-
es-define-property@1.0.1: {}
2626
-
2627
-
es-errors@1.3.0: {}
2628
-
2629
-
es-object-atoms@1.1.1:
2630
-
dependencies:
2631
-
es-errors: 1.3.0
2632
-
2633
-
es-set-tostringtag@2.1.0:
2634
-
dependencies:
2635
-
es-errors: 1.3.0
2636
-
get-intrinsic: 1.3.0
2637
-
has-tostringtag: 1.0.2
2638
-
hasown: 2.0.2
2639
2640
esbuild@0.23.1:
2641
optionalDependencies:
···
2698
2699
esm-env@1.2.2: {}
2700
2701
-
exsolve@1.0.8: {}
2702
-
2703
-
extract-zip@2.0.1:
2704
-
dependencies:
2705
-
debug: 4.4.3
2706
-
get-stream: 5.2.0
2707
-
yauzl: 2.10.0
2708
-
optionalDependencies:
2709
-
'@types/yauzl': 2.10.3
2710
-
transitivePeerDependencies:
2711
-
- supports-color
2712
-
2713
-
fd-slicer@1.1.0:
2714
-
dependencies:
2715
-
pend: 1.2.0
2716
-
2717
fdir@6.5.0(picomatch@4.0.3):
2718
optionalDependencies:
2719
picomatch: 4.0.3
2720
2721
-
follow-redirects@1.15.11: {}
2722
-
2723
-
form-data@4.0.5:
2724
-
dependencies:
2725
-
asynckit: 0.4.0
2726
-
combined-stream: 1.0.8
2727
-
es-set-tostringtag: 2.1.0
2728
-
hasown: 2.0.2
2729
-
mime-types: 2.1.35
2730
-
2731
-
fs-minipass@2.1.0:
2732
-
dependencies:
2733
-
minipass: 3.3.6
2734
2735
fsevents@2.3.3:
2736
optional: true
2737
2738
-
function-bind@1.1.2: {}
2739
-
2740
gensync@1.0.0-beta.2: {}
2741
2742
-
get-intrinsic@1.3.0:
2743
-
dependencies:
2744
-
call-bind-apply-helpers: 1.0.2
2745
-
es-define-property: 1.0.1
2746
-
es-errors: 1.3.0
2747
-
es-object-atoms: 1.1.1
2748
-
function-bind: 1.1.2
2749
-
get-proto: 1.0.1
2750
-
gopd: 1.2.0
2751
-
has-symbols: 1.1.0
2752
-
hasown: 2.0.2
2753
-
math-intrinsics: 1.1.0
2754
-
2755
-
get-proto@1.0.1:
2756
-
dependencies:
2757
-
dunder-proto: 1.0.1
2758
-
es-object-atoms: 1.1.1
2759
-
2760
-
get-stream@5.2.0:
2761
-
dependencies:
2762
-
pump: 3.0.3
2763
-
2764
get-tsconfig@4.13.0:
2765
dependencies:
2766
resolve-pkg-maps: 1.0.0
2767
optional: true
2768
2769
-
globals@15.15.0: {}
2770
-
2771
-
gopd@1.2.0: {}
2772
-
2773
graceful-fs@4.2.11: {}
2774
2775
-
has-symbols@1.1.0: {}
2776
-
2777
-
has-tostringtag@1.0.2:
2778
-
dependencies:
2779
-
has-symbols: 1.1.0
2780
-
2781
-
hasown@2.0.2:
2782
-
dependencies:
2783
-
function-bind: 1.1.2
2784
-
2785
html-entities@2.3.3: {}
2786
2787
-
htmlparser2@9.1.0:
2788
-
dependencies:
2789
-
domelementtype: 2.3.0
2790
-
domhandler: 5.0.3
2791
-
domutils: 3.2.2
2792
-
entities: 4.5.0
2793
-
2794
-
iconv-lite@0.6.3:
2795
-
dependencies:
2796
-
safer-buffer: 2.1.2
2797
-
2798
is-what@4.1.16: {}
2799
2800
jiti@2.6.1: {}
···
2804
jsesc@3.1.0: {}
2805
2806
json5@2.2.3: {}
2807
-
2808
-
kolorist@1.8.0: {}
2809
2810
lightningcss-android-arm64@1.30.2:
2811
optional: true
···
2856
lightningcss-win32-arm64-msvc: 1.30.2
2857
lightningcss-win32-x64-msvc: 1.30.2
2858
2859
-
local-pkg@0.5.1:
2860
-
dependencies:
2861
-
mlly: 1.8.0
2862
-
pkg-types: 1.3.1
2863
-
2864
-
local-pkg@1.1.2:
2865
-
dependencies:
2866
-
mlly: 1.8.0
2867
-
pkg-types: 2.3.0
2868
-
quansync: 0.2.11
2869
-
2870
lru-cache@5.1.1:
2871
dependencies:
2872
yallist: 3.1.1
···
2875
dependencies:
2876
'@jridgewell/sourcemap-codec': 1.5.5
2877
2878
-
math-intrinsics@1.1.0: {}
2879
-
2880
mdn-data@2.0.28: {}
2881
2882
-
mdn-data@2.0.30: {}
2883
2884
merge-anything@5.1.7:
2885
dependencies:
2886
is-what: 4.1.16
2887
-
2888
-
mime-db@1.52.0: {}
2889
-
2890
-
mime-types@2.1.35:
2891
-
dependencies:
2892
-
mime-db: 1.52.0
2893
-
2894
-
minipass@3.3.6:
2895
-
dependencies:
2896
-
yallist: 4.0.0
2897
-
2898
-
minipass@4.2.8: {}
2899
-
2900
-
minipass@5.0.0: {}
2901
-
2902
-
minizlib@2.1.2:
2903
-
dependencies:
2904
-
minipass: 3.3.6
2905
-
yallist: 4.0.0
2906
-
2907
-
mkdirp@1.0.4: {}
2908
2909
mlly@1.8.0:
2910
dependencies:
···
2913
pkg-types: 1.3.1
2914
ufo: 1.6.1
2915
2916
ms@2.1.3: {}
2917
2918
nanoevents@9.1.0: {}
···
2927
dependencies:
2928
boolbase: 1.0.0
2929
2930
-
once@1.4.0:
2931
-
dependencies:
2932
-
wrappy: 1.0.2
2933
-
2934
-
package-manager-detector@1.5.0: {}
2935
-
2936
-
parse5-htmlparser2-tree-adapter@7.1.0:
2937
-
dependencies:
2938
-
domhandler: 5.0.3
2939
-
parse5: 7.3.0
2940
-
2941
-
parse5-parser-stream@7.1.2:
2942
-
dependencies:
2943
-
parse5: 7.3.0
2944
2945
parse5@7.3.0:
2946
dependencies:
2947
entities: 6.0.1
2948
2949
-
pathe@1.1.2: {}
2950
-
2951
pathe@2.0.3: {}
2952
-
2953
-
pend@1.2.0: {}
2954
2955
picocolors@1.1.1: {}
2956
···
2960
dependencies:
2961
confbox: 0.1.8
2962
mlly: 1.8.0
2963
-
pathe: 2.0.3
2964
-
2965
-
pkg-types@2.3.0:
2966
-
dependencies:
2967
-
confbox: 0.2.2
2968
-
exsolve: 1.0.8
2969
pathe: 2.0.3
2970
2971
postcss@8.5.6:
···
2974
picocolors: 1.1.1
2975
source-map-js: 1.2.1
2976
2977
-
prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3):
2978
dependencies:
2979
-
prettier: 3.6.2
2980
typescript: 5.9.3
2981
2982
-
prettier-plugin-tailwindcss@0.7.1(prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3))(prettier@3.6.2):
2983
dependencies:
2984
-
prettier: 3.6.2
2985
optionalDependencies:
2986
-
prettier-plugin-organize-imports: 4.3.0(prettier@3.6.2)(typescript@5.9.3)
2987
-
2988
-
prettier@3.6.2: {}
2989
-
2990
-
proxy-from-env@1.1.0: {}
2991
-
2992
-
pump@3.0.3:
2993
-
dependencies:
2994
-
end-of-stream: 1.4.5
2995
-
once: 1.4.0
2996
2997
-
quansync@0.2.11: {}
2998
2999
resolve-pkg-maps@1.0.0:
3000
optional: true
···
3027
'@rollup/rollup-win32-x64-msvc': 4.53.3
3028
fsevents: 2.3.3
3029
3030
-
safer-buffer@2.1.2: {}
3031
3032
semver@6.3.1: {}
3033
···
3056
3057
style-mod@4.1.3: {}
3058
3059
-
svgo@3.3.2:
3060
dependencies:
3061
-
'@trysound/sax': 0.2.0
3062
-
commander: 7.2.0
3063
css-select: 5.2.2
3064
-
css-tree: 2.3.1
3065
css-what: 6.2.2
3066
csso: 5.0.5
3067
picocolors: 1.1.1
3068
3069
-
tailwindcss@4.1.17: {}
3070
3071
tapable@2.3.0: {}
3072
3073
-
tar@6.2.1:
3074
-
dependencies:
3075
-
chownr: 2.0.0
3076
-
fs-minipass: 2.1.0
3077
-
minipass: 5.0.0
3078
-
minizlib: 2.1.2
3079
-
mkdirp: 1.0.4
3080
-
yallist: 4.0.0
3081
-
3082
tinyexec@1.0.2: {}
3083
3084
tinyglobby@0.2.15:
···
3098
3099
ufo@1.6.1: {}
3100
3101
-
undici-types@7.16.0: {}
3102
3103
-
undici@6.22.0: {}
3104
-
3105
-
update-browserslist-db@1.1.4(browserslist@4.28.0):
3106
dependencies:
3107
-
browserslist: 4.28.0
3108
escalade: 3.2.0
3109
picocolors: 1.1.1
3110
3111
-
vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)):
3112
dependencies:
3113
'@babel/core': 7.28.5
3114
'@types/babel__core': 7.20.5
···
3116
merge-anything: 5.1.7
3117
solid-js: 1.9.10
3118
solid-refresh: 0.6.3(solid-js@1.9.10)
3119
-
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
3120
-
vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))
3121
transitivePeerDependencies:
3122
- supports-color
3123
3124
-
vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2):
3125
dependencies:
3126
esbuild: 0.25.12
3127
fdir: 6.5.0(picomatch@4.0.3)
···
3136
lightningcss: 1.30.2
3137
tsx: 4.19.2
3138
3139
-
vitefu@1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)):
3140
optionalDependencies:
3141
-
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
3142
3143
w3c-keyname@2.2.8: {}
3144
3145
-
whatwg-encoding@3.1.1:
3146
-
dependencies:
3147
-
iconv-lite: 0.6.3
3148
-
3149
-
whatwg-mimetype@4.0.0: {}
3150
-
3151
-
wrappy@1.0.2: {}
3152
-
3153
yallist@3.1.1: {}
3154
-
3155
-
yallist@4.0.0: {}
3156
-
3157
-
yauzl@2.10.0:
3158
-
dependencies:
3159
-
buffer-crc32: 0.2.13
3160
-
fd-slicer: 1.1.0
3161
3162
yocto-queue@1.2.2: {}
···
12
specifier: ^3.1.9
13
version: 3.1.9
14
'@atcute/bluesky':
15
+
specifier: ^3.2.14
16
+
version: 3.2.14
17
'@atcute/client':
18
+
specifier: ^4.1.1
19
+
version: 4.1.1
20
'@atcute/crypto':
21
+
specifier: ^2.3.0
22
+
version: 2.3.0
23
'@atcute/did-plc':
24
specifier: ^0.2.0
25
version: 0.2.0
···
27
specifier: ^1.1.3
28
version: 1.1.3
29
'@atcute/identity-resolver':
30
+
specifier: ^1.2.0
31
+
version: 1.2.0(@atcute/identity@1.1.3)
32
'@atcute/leaflet':
33
+
specifier: ^1.0.14
34
+
version: 1.0.14
35
'@atcute/lexicon-doc':
36
+
specifier: ^2.0.5
37
+
version: 2.0.5
38
'@atcute/lexicon-resolver':
39
+
specifier: ^0.1.5
40
+
version: 0.1.5(@atcute/identity-resolver@1.2.0(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)
41
'@atcute/lexicons':
42
+
specifier: ^1.2.5
43
+
version: 1.2.5
44
+
'@atcute/multibase':
45
+
specifier: ^1.1.6
46
+
version: 1.1.6
47
'@atcute/oauth-browser-client':
48
+
specifier: ^2.0.3
49
+
version: 2.0.3(@atcute/identity@1.1.3)
50
'@atcute/repo':
51
specifier: ^0.1.0
52
version: 0.1.0
53
'@atcute/tangled':
54
+
specifier: ^1.0.13
55
+
version: 1.0.13
56
'@atcute/tid':
57
specifier: ^1.0.3
58
version: 1.0.3
···
69
specifier: ^6.5.2
70
version: 6.5.2
71
'@codemirror/view':
72
+
specifier: ^6.39.4
73
+
version: 6.39.4
74
'@fsegurai/codemirror-theme-basic-dark':
75
+
specifier: ^6.2.3
76
+
version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3)
77
'@fsegurai/codemirror-theme-basic-light':
78
+
specifier: ^6.2.3
79
+
version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3)
80
'@mary/exif-rm':
81
specifier: jsr:^0.2.2
82
version: '@jsr/mary__exif-rm@0.2.2'
···
97
version: 1.9.10
98
devDependencies:
99
'@iconify-json/lucide':
100
+
specifier: ^1.2.81
101
+
version: 1.2.81
102
'@iconify/tailwind4':
103
+
specifier: ^1.2.0
104
+
version: 1.2.0(tailwindcss@4.1.18)
105
'@tailwindcss/vite':
106
+
specifier: ^4.1.18
107
+
version: 4.1.18(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))
108
prettier:
109
+
specifier: ^3.7.4
110
+
version: 3.7.4
111
prettier-plugin-organize-imports:
112
specifier: ^4.3.0
113
+
version: 4.3.0(prettier@3.7.4)(typescript@5.9.3)
114
prettier-plugin-tailwindcss:
115
+
specifier: ^0.7.2
116
+
version: 0.7.2(prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3))(prettier@3.7.4)
117
tailwindcss:
118
+
specifier: ^4.1.18
119
+
version: 4.1.18
120
typescript:
121
specifier: ^5.9.3
122
version: 5.9.3
123
vite:
124
+
specifier: ^7.2.7
125
+
version: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
126
vite-plugin-solid:
127
specifier: ^2.11.10
128
+
version: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))
129
130
packages:
131
132
'@antfu/install-pkg@1.1.0':
133
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
134
135
'@atcute/atproto@3.1.9':
136
resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==}
137
138
+
'@atcute/bluesky@3.2.14':
139
+
resolution: {integrity: sha512-XlVuF55AYIyplmKvlGLlj+cUvk9ggxNRPczkTPIY991xJ4qDxDHpBJ39ekAV4dWcuBoRo2o9JynzpafPu2ljDA==}
140
141
'@atcute/car@3.1.3':
142
resolution: {integrity: sha512-WJ13bAEt7TjDMVi09ubjLtvhdljbWInGm9Kfy7Y6NhrmiyC/aZYaA/zHX/bHI6xv1c/h3SQduWqxOr4ae49eqA==}
···
150
'@atcute/cid@2.2.6':
151
resolution: {integrity: sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ==}
152
153
+
'@atcute/client@4.1.1':
154
+
resolution: {integrity: sha512-FROCbTTCeL5u4tO/n72jDEKyKqjdlXMB56Ehve3W/gnnLGCYWvN42sS7tvL1Mgu6sbO3yZwsXKDrmM2No4XpjA==}
155
156
+
'@atcute/crypto@2.3.0':
157
+
resolution: {integrity: sha512-w5pkJKCjbNMQu+F4JRHbR3ROQyhi1wbn+GSC6WDQamcYHkZmEZk1/eoI354bIQOOfkEM6aFLv718iskrkon4GQ==}
158
159
'@atcute/did-plc@0.2.0':
160
resolution: {integrity: sha512-1sGek8GRM/Ph7nLVRREm8FqM7g4shGckItvdVwJcRbUa8Rh0zOsXQa0QyYWAC0k40BhkqO9FwKXhJEaXCmF5oQ==}
161
162
+
'@atcute/identity-resolver@1.2.0':
163
+
resolution: {integrity: sha512-5UbSJfdV3JIkF8ksXz7g4nKBWasf2wROvzM66cfvTIWydWFO6/oS1KZd+zo9Eokje5Scf5+jsY9ZfgVARLepXg==}
164
peerDependencies:
165
'@atcute/identity': ^1.0.0
166
167
'@atcute/identity@1.1.3':
168
resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==}
169
170
+
'@atcute/leaflet@1.0.14':
171
+
resolution: {integrity: sha512-TWbtB7b73GChBaYwfd7aWFyGVObZ/DqrRtwkpWGm1GO8zZmQ9eJyKDUnXim7NOAs2hmKQ1u2wk2AM4AYzkF5Gg==}
172
173
+
'@atcute/lexicon-doc@2.0.5':
174
+
resolution: {integrity: sha512-fNCp94ehGjWFZMIqP6pWD1F9MOJogNCyqsaMVZluPSIclZ+lDL528iXB56aW4u0eSiD6Y9WJB1OI/lElG39cSA==}
175
176
+
'@atcute/lexicon-resolver@0.1.5':
177
+
resolution: {integrity: sha512-0bx1/zdMQPuxvRcHW6ykAxRxktC2rEZLoAVSFoLSWDAA92Tf09F9QPK5wgXSF4MNODm1dvzMEdWSMIvlg8sr3A==}
178
peerDependencies:
179
'@atcute/identity': ^1.1.0
180
'@atcute/identity-resolver': ^1.1.3
181
182
+
'@atcute/lexicons@1.2.5':
183
+
resolution: {integrity: sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q==}
184
185
'@atcute/mst@0.1.0':
186
resolution: {integrity: sha512-h+iDToKEnBpigk2DOHjSqY63vJtjYKUIztqu1CZ0P+I54wV2SrgoqAXAT1xrW6A1Iup8cjTv+U2H5WVG4KxPLw==}
···
188
'@atcute/multibase@1.1.6':
189
resolution: {integrity: sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg==}
190
191
+
'@atcute/oauth-browser-client@2.0.3':
192
+
resolution: {integrity: sha512-rzUjwhjE4LRRKdQnCFQag/zXRZMEAB1hhBoLfnoQuHwWbmDUCL7fzwC3jRhDPp3om8XaYNDj8a/iqRip0wRqoQ==}
193
194
'@atcute/repo@0.1.0':
195
resolution: {integrity: sha512-INiYAuma8dydBu7cqd2WVpcXh3mzhIepYBUqFWAK5MqMulPRLTRCc/9GW3G9pxYrOdlvLCVamG2Jf8XK0nuFEw==}
196
197
+
'@atcute/tangled@1.0.13':
198
+
resolution: {integrity: sha512-K95jmjDXl/f1FFzOJkk07ibNbFsPmn64sdrMACxQmUibO9WcfSjzjZLPXuH6WHFnCNtIBG3x1FQ7ndQgLoZAmw==}
199
200
'@atcute/tid@1.0.3':
201
resolution: {integrity: sha512-wfMJx1IMdnu0CZgWl0uR4JO2s6PGT1YPhpytD4ZHzEYKKQVuqV6Eb/7vieaVo1eYNMp2FrY67FZObeR7utRl2w==}
202
203
+
'@atcute/uint8array@1.0.6':
204
+
resolution: {integrity: sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==}
205
206
+
'@atcute/util-fetch@1.0.4':
207
+
resolution: {integrity: sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg==}
208
209
'@atcute/varint@1.0.3':
210
resolution: {integrity: sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog==}
···
315
'@codemirror/state@6.5.2':
316
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
317
318
+
'@codemirror/view@6.39.4':
319
+
resolution: {integrity: sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==}
320
+
321
+
'@cyberalien/svg-utils@1.0.11':
322
+
resolution: {integrity: sha512-qEE9mnyI+avfGT3emKuRs3ucYkITeaV0Xi7VlYN41f+uGnZBecQP3jwz/AF437H9J4Q7qPClHKm4NiTYpNE6hA==}
323
324
'@esbuild/aix-ppc64@0.23.1':
325
resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
···
621
cpu: [x64]
622
os: [win32]
623
624
+
'@fsegurai/codemirror-theme-basic-dark@6.2.3':
625
+
resolution: {integrity: sha512-08d09Yn9Ic8mjCzrBQQhtws/HM+8B00bRV9FqW+GaIQwSOFmn17FsvzuLJQyervcKAkTzmKaLPjp2D3Y+2K8EQ==}
626
peerDependencies:
627
'@codemirror/language': ^6.0.0
628
'@codemirror/state': ^6.0.0
629
'@codemirror/view': ^6.0.0
630
'@lezer/highlight': ^1.0.0
631
632
+
'@fsegurai/codemirror-theme-basic-light@6.2.3':
633
+
resolution: {integrity: sha512-rkHCj1U3OwNAqLLi2xti47u3Fq6gDiSEKmQsAOwIADJKnnwU2LeAwCPqSEa7sUVlavFusjDvt5L/SmGjb10vWg==}
634
peerDependencies:
635
'@codemirror/language': ^6.0.0
636
'@codemirror/state': ^6.0.0
637
'@codemirror/view': ^6.0.0
638
'@lezer/highlight': ^1.0.0
639
640
+
'@iconify-json/lucide@1.2.81':
641
+
resolution: {integrity: sha512-6Kz/+SEuD5bkg0KImi0yFem9l6njKp4e1qF1LpQbgRfk7ngsJR/qjlB4y5rM8N1iKiDR/p19cqhmwZxyCWek+w==}
642
643
+
'@iconify/tailwind4@1.2.0':
644
+
resolution: {integrity: sha512-+t7XqfojOB0zzZdd8gV7IQZGq1AaIHTlsxMVzagxYR0hAlJCLUD63o3iSlNKRMH3ZR7gZ8y5c9dJ7J431avRbA==}
645
peerDependencies:
646
tailwindcss: '>= 4.0.0'
647
648
+
'@iconify/tools@5.0.0':
649
+
resolution: {integrity: sha512-GY/FsuNdWA/FbkLqgQ8b1PHFkNvjMeSFWaVJdLldYGHBp0lZ64HJlcS0qzLfglacHTd8zYdfQjF74RxGqyGMgw==}
650
651
'@iconify/types@2.0.0':
652
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
653
654
+
'@iconify/utils@3.1.0':
655
+
resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
656
657
'@jridgewell/gen-mapping@0.3.13':
658
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
···
673
'@jsr/mary__exif-rm@0.2.2':
674
resolution: {integrity: sha512-+ZpLaC+1CyqWhH608Sqd6/yTG0pOlokn2tCXha7s1SMQ+GLKo4Nn/PskTeeP9Pt+6gNYSu6ednoSlRvXb2ZGxg==, tarball: https://npm.jsr.io/~/11/@jsr/mary__exif-rm/0.2.2.tgz}
675
676
+
'@lezer/common@1.4.0':
677
+
resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==}
678
679
'@lezer/highlight@1.2.3':
680
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
···
682
'@lezer/json@1.0.3':
683
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
684
685
+
'@lezer/lr@1.4.5':
686
+
resolution: {integrity: sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==}
687
688
'@marijn/find-cluster-break@1.0.2':
689
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
···
817
'@standard-schema/spec@1.0.0':
818
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
819
820
+
'@tailwindcss/node@4.1.18':
821
+
resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
822
823
+
'@tailwindcss/oxide-android-arm64@4.1.18':
824
+
resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
825
engines: {node: '>= 10'}
826
cpu: [arm64]
827
os: [android]
828
829
+
'@tailwindcss/oxide-darwin-arm64@4.1.18':
830
+
resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
831
engines: {node: '>= 10'}
832
cpu: [arm64]
833
os: [darwin]
834
835
+
'@tailwindcss/oxide-darwin-x64@4.1.18':
836
+
resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
837
engines: {node: '>= 10'}
838
cpu: [x64]
839
os: [darwin]
840
841
+
'@tailwindcss/oxide-freebsd-x64@4.1.18':
842
+
resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
843
engines: {node: '>= 10'}
844
cpu: [x64]
845
os: [freebsd]
846
847
+
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
848
+
resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
849
engines: {node: '>= 10'}
850
cpu: [arm]
851
os: [linux]
852
853
+
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
854
+
resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
855
engines: {node: '>= 10'}
856
cpu: [arm64]
857
os: [linux]
858
859
+
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
860
+
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
861
engines: {node: '>= 10'}
862
cpu: [arm64]
863
os: [linux]
864
865
+
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
866
+
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
867
engines: {node: '>= 10'}
868
cpu: [x64]
869
os: [linux]
870
871
+
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
872
+
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
873
engines: {node: '>= 10'}
874
cpu: [x64]
875
os: [linux]
876
877
+
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
878
+
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
879
engines: {node: '>=14.0.0'}
880
cpu: [wasm32]
881
bundledDependencies:
···
886
- '@emnapi/wasi-threads'
887
- tslib
888
889
+
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
890
+
resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
891
engines: {node: '>= 10'}
892
cpu: [arm64]
893
os: [win32]
894
895
+
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
896
+
resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
897
engines: {node: '>= 10'}
898
cpu: [x64]
899
os: [win32]
900
901
+
'@tailwindcss/oxide@4.1.18':
902
+
resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
903
engines: {node: '>= 10'}
904
905
+
'@tailwindcss/vite@4.1.18':
906
+
resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==}
907
peerDependencies:
908
vite: ^5.2.0 || ^6 || ^7
909
910
'@types/babel__core@7.20.5':
911
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
912
···
925
'@types/node@24.10.1':
926
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
927
928
acorn@8.15.0:
929
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
930
engines: {node: '>=0.4.0'}
931
hasBin: true
932
933
babel-plugin-jsx-dom-expressions@0.40.3:
934
resolution: {integrity: sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==}
935
peerDependencies:
···
944
solid-js:
945
optional: true
946
947
+
baseline-browser-mapping@2.9.7:
948
+
resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==}
949
hasBin: true
950
951
boolbase@1.0.0:
952
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
953
954
+
browserslist@4.28.1:
955
+
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
956
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
957
hasBin: true
958
959
+
caniuse-lite@1.0.30001760:
960
+
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
961
962
codemirror@6.0.2:
963
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
964
965
+
commander@11.1.0:
966
+
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
967
+
engines: {node: '>=16'}
968
969
confbox@0.1.8:
970
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
971
972
convert-source-map@2.0.0:
973
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
974
···
982
resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
983
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
984
985
+
css-tree@3.1.0:
986
+
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
987
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
988
989
css-what@6.2.2:
···
1006
supports-color:
1007
optional: true
1008
1009
detect-libc@2.1.2:
1010
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
1011
engines: {node: '>=8'}
···
1023
domutils@3.2.2:
1024
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
1025
1026
+
electron-to-chromium@1.5.267:
1027
+
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
1028
1029
+
enhanced-resolve@5.18.4:
1030
+
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
1031
engines: {node: '>=10.13.0'}
1032
1033
entities@4.5.0:
···
1037
entities@6.0.1:
1038
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
1039
engines: {node: '>=0.12'}
1040
1041
esbuild@0.23.1:
1042
resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==}
···
1055
esm-env@1.2.2:
1056
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
1057
1058
fdir@6.5.0:
1059
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
1060
engines: {node: '>=12.0.0'}
···
1064
picomatch:
1065
optional: true
1066
1067
+
fflate@0.8.2:
1068
+
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
1069
1070
fsevents@2.3.3:
1071
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
1072
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
1073
os: [darwin]
1074
1075
gensync@1.0.0-beta.2:
1076
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
1077
engines: {node: '>=6.9.0'}
1078
1079
get-tsconfig@4.13.0:
1080
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
1081
1082
graceful-fs@4.2.11:
1083
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
1084
1085
html-entities@2.3.3:
1086
resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==}
1087
1088
is-what@4.1.16:
1089
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
1090
engines: {node: '>=12.13'}
···
1105
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
1106
engines: {node: '>=6'}
1107
hasBin: true
1108
1109
lightningcss-android-arm64@1.30.2:
1110
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
···
1176
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
1177
engines: {node: '>= 12.0.0'}
1178
1179
lru-cache@5.1.1:
1180
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
1181
1182
magic-string@0.30.21:
1183
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
1184
1185
mdn-data@2.0.28:
1186
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
1187
1188
+
mdn-data@2.12.2:
1189
+
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
1190
1191
merge-anything@5.1.7:
1192
resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==}
1193
engines: {node: '>=12.13'}
1194
1195
mlly@1.8.0:
1196
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
1197
+
1198
+
modern-tar@0.7.2:
1199
+
resolution: {integrity: sha512-TGG1ZRk1TAQ3neuZwahAHke3rKsSlro+ooMYtjh9sl2gGPVMLMuWiHgwC7im9T5bSM566RSo2Dko56ETgEvZcA==}
1200
+
engines: {node: '>=18.0.0'}
1201
1202
ms@2.1.3:
1203
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
···
1222
nth-check@2.1.1:
1223
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
1224
1225
+
package-manager-detector@1.6.0:
1226
+
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
1227
1228
parse5@7.3.0:
1229
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
1230
1231
pathe@2.0.3:
1232
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
1233
1234
picocolors@1.1.1:
1235
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
···
1241
pkg-types@1.3.1:
1242
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
1243
1244
postcss@8.5.6:
1245
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
1246
engines: {node: ^10 || ^12 || >=14}
···
1255
vue-tsc:
1256
optional: true
1257
1258
+
prettier-plugin-tailwindcss@0.7.2:
1259
+
resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==}
1260
engines: {node: '>=20.19'}
1261
peerDependencies:
1262
'@ianvs/prettier-plugin-sort-imports': '*'
···
1310
prettier-plugin-svelte:
1311
optional: true
1312
1313
+
prettier@3.7.4:
1314
+
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
1315
engines: {node: '>=14'}
1316
hasBin: true
1317
1318
resolve-pkg-maps@1.0.0:
1319
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
1320
···
1323
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1324
hasBin: true
1325
1326
+
sax@1.4.3:
1327
+
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
1328
1329
semver@6.3.1:
1330
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
···
1355
style-mod@4.1.3:
1356
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
1357
1358
+
svgo@4.0.0:
1359
+
resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==}
1360
+
engines: {node: '>=16'}
1361
hasBin: true
1362
1363
+
tailwindcss@4.1.18:
1364
+
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
1365
1366
tapable@2.3.0:
1367
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
1368
engines: {node: '>=6'}
1369
1370
tinyexec@1.0.2:
1371
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
1372
engines: {node: '>=18'}
···
1391
undici-types@7.16.0:
1392
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
1393
1394
+
update-browserslist-db@1.2.2:
1395
+
resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==}
1396
hasBin: true
1397
peerDependencies:
1398
browserslist: '>= 4.21.0'
···
1407
'@testing-library/jest-dom':
1408
optional: true
1409
1410
+
vite@7.2.7:
1411
+
resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==}
1412
engines: {node: ^20.19.0 || >=22.12.0}
1413
hasBin: true
1414
peerDependencies:
···
1458
w3c-keyname@2.2.8:
1459
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
1460
1461
yallist@3.1.1:
1462
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
1463
1464
yocto-queue@1.2.2:
1465
resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
1466
engines: {node: '>=12.20'}
···
1469
1470
'@antfu/install-pkg@1.1.0':
1471
dependencies:
1472
+
package-manager-detector: 1.6.0
1473
tinyexec: 1.0.2
1474
1475
'@atcute/atproto@3.1.9':
1476
dependencies:
1477
+
'@atcute/lexicons': 1.2.5
1478
1479
+
'@atcute/bluesky@3.2.14':
1480
dependencies:
1481
'@atcute/atproto': 3.1.9
1482
+
'@atcute/lexicons': 1.2.5
1483
1484
'@atcute/car@3.1.3':
1485
dependencies:
1486
'@atcute/cbor': 2.2.8
1487
'@atcute/cid': 2.2.6
1488
+
'@atcute/uint8array': 1.0.6
1489
'@atcute/varint': 1.0.3
1490
yocto-queue: 1.2.2
1491
···
1493
dependencies:
1494
'@atcute/cbor': 2.2.8
1495
'@atcute/cid': 2.2.6
1496
+
'@atcute/uint8array': 1.0.6
1497
'@atcute/varint': 1.0.3
1498
1499
'@atcute/cbor@2.2.8':
1500
dependencies:
1501
'@atcute/cid': 2.2.6
1502
'@atcute/multibase': 1.1.6
1503
+
'@atcute/uint8array': 1.0.6
1504
1505
'@atcute/cid@2.2.6':
1506
dependencies:
1507
'@atcute/multibase': 1.1.6
1508
+
'@atcute/uint8array': 1.0.6
1509
1510
+
'@atcute/client@4.1.1':
1511
dependencies:
1512
'@atcute/identity': 1.1.3
1513
+
'@atcute/lexicons': 1.2.5
1514
1515
+
'@atcute/crypto@2.3.0':
1516
dependencies:
1517
'@atcute/multibase': 1.1.6
1518
+
'@atcute/uint8array': 1.0.6
1519
'@noble/secp256k1': 3.0.0
1520
1521
'@atcute/did-plc@0.2.0':
1522
dependencies:
1523
'@atcute/cbor': 2.2.8
1524
'@atcute/cid': 2.2.6
1525
+
'@atcute/crypto': 2.3.0
1526
'@atcute/identity': 1.1.3
1527
+
'@atcute/lexicons': 1.2.5
1528
'@atcute/multibase': 1.1.6
1529
+
'@atcute/uint8array': 1.0.6
1530
'@badrap/valita': 0.4.6
1531
1532
+
'@atcute/identity-resolver@1.2.0(@atcute/identity@1.1.3)':
1533
dependencies:
1534
'@atcute/identity': 1.1.3
1535
+
'@atcute/lexicons': 1.2.5
1536
+
'@atcute/util-fetch': 1.0.4
1537
'@badrap/valita': 0.4.6
1538
1539
'@atcute/identity@1.1.3':
1540
dependencies:
1541
+
'@atcute/lexicons': 1.2.5
1542
'@badrap/valita': 0.4.6
1543
1544
+
'@atcute/leaflet@1.0.14':
1545
dependencies:
1546
'@atcute/atproto': 3.1.9
1547
+
'@atcute/lexicons': 1.2.5
1548
1549
+
'@atcute/lexicon-doc@2.0.5':
1550
dependencies:
1551
'@atcute/identity': 1.1.3
1552
+
'@atcute/lexicons': 1.2.5
1553
'@badrap/valita': 0.4.6
1554
1555
+
'@atcute/lexicon-resolver@0.1.5(@atcute/identity-resolver@1.2.0(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)':
1556
dependencies:
1557
+
'@atcute/crypto': 2.3.0
1558
'@atcute/identity': 1.1.3
1559
+
'@atcute/identity-resolver': 1.2.0(@atcute/identity@1.1.3)
1560
+
'@atcute/lexicon-doc': 2.0.5
1561
+
'@atcute/lexicons': 1.2.5
1562
'@atcute/repo': 0.1.0
1563
+
'@atcute/util-fetch': 1.0.4
1564
'@badrap/valita': 0.4.6
1565
1566
+
'@atcute/lexicons@1.2.5':
1567
dependencies:
1568
'@standard-schema/spec': 1.0.0
1569
esm-env: 1.2.2
···
1572
dependencies:
1573
'@atcute/cbor': 2.2.8
1574
'@atcute/cid': 2.2.6
1575
+
'@atcute/uint8array': 1.0.6
1576
1577
'@atcute/multibase@1.1.6':
1578
dependencies:
1579
+
'@atcute/uint8array': 1.0.6
1580
1581
+
'@atcute/oauth-browser-client@2.0.3(@atcute/identity@1.1.3)':
1582
dependencies:
1583
+
'@atcute/client': 4.1.1
1584
+
'@atcute/identity-resolver': 1.2.0(@atcute/identity@1.1.3)
1585
+
'@atcute/lexicons': 1.2.5
1586
'@atcute/multibase': 1.1.6
1587
+
'@atcute/uint8array': 1.0.6
1588
nanoid: 5.1.6
1589
+
transitivePeerDependencies:
1590
+
- '@atcute/identity'
1591
1592
'@atcute/repo@0.1.0':
1593
dependencies:
1594
'@atcute/car': 5.0.0
1595
'@atcute/cbor': 2.2.8
1596
'@atcute/cid': 2.2.6
1597
+
'@atcute/crypto': 2.3.0
1598
+
'@atcute/lexicons': 1.2.5
1599
'@atcute/mst': 0.1.0
1600
+
'@atcute/uint8array': 1.0.6
1601
1602
+
'@atcute/tangled@1.0.13':
1603
dependencies:
1604
'@atcute/atproto': 3.1.9
1605
+
'@atcute/lexicons': 1.2.5
1606
1607
'@atcute/tid@1.0.3': {}
1608
1609
+
'@atcute/uint8array@1.0.6': {}
1610
1611
+
'@atcute/util-fetch@1.0.4':
1612
dependencies:
1613
'@badrap/valita': 0.4.6
1614
···
1654
dependencies:
1655
'@babel/compat-data': 7.28.5
1656
'@babel/helper-validator-option': 7.27.1
1657
+
browserslist: 4.28.1
1658
lru-cache: 5.1.1
1659
semver: 6.3.1
1660
···
1731
dependencies:
1732
'@codemirror/language': 6.11.3
1733
'@codemirror/state': 6.5.2
1734
+
'@codemirror/view': 6.39.4
1735
+
'@lezer/common': 1.4.0
1736
1737
'@codemirror/commands@6.10.0':
1738
dependencies:
1739
'@codemirror/language': 6.11.3
1740
'@codemirror/state': 6.5.2
1741
+
'@codemirror/view': 6.39.4
1742
+
'@lezer/common': 1.4.0
1743
1744
'@codemirror/lang-json@6.0.2':
1745
dependencies:
···
1749
'@codemirror/language@6.11.3':
1750
dependencies:
1751
'@codemirror/state': 6.5.2
1752
+
'@codemirror/view': 6.39.4
1753
+
'@lezer/common': 1.4.0
1754
'@lezer/highlight': 1.2.3
1755
+
'@lezer/lr': 1.4.5
1756
style-mod: 4.1.3
1757
1758
'@codemirror/lint@6.9.2':
1759
dependencies:
1760
'@codemirror/state': 6.5.2
1761
+
'@codemirror/view': 6.39.4
1762
crelt: 1.0.6
1763
1764
'@codemirror/search@6.5.11':
1765
dependencies:
1766
'@codemirror/state': 6.5.2
1767
+
'@codemirror/view': 6.39.4
1768
crelt: 1.0.6
1769
1770
'@codemirror/state@6.5.2':
1771
dependencies:
1772
'@marijn/find-cluster-break': 1.0.2
1773
1774
+
'@codemirror/view@6.39.4':
1775
dependencies:
1776
'@codemirror/state': 6.5.2
1777
crelt: 1.0.6
1778
style-mod: 4.1.3
1779
w3c-keyname: 2.2.8
1780
+
1781
+
'@cyberalien/svg-utils@1.0.11':
1782
+
dependencies:
1783
+
'@iconify/types': 2.0.0
1784
1785
'@esbuild/aix-ppc64@0.23.1':
1786
optional: true
···
1932
'@esbuild/win32-x64@0.25.12':
1933
optional: true
1934
1935
+
'@fsegurai/codemirror-theme-basic-dark@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3)':
1936
dependencies:
1937
'@codemirror/language': 6.11.3
1938
'@codemirror/state': 6.5.2
1939
+
'@codemirror/view': 6.39.4
1940
'@lezer/highlight': 1.2.3
1941
1942
+
'@fsegurai/codemirror-theme-basic-light@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3)':
1943
dependencies:
1944
'@codemirror/language': 6.11.3
1945
'@codemirror/state': 6.5.2
1946
+
'@codemirror/view': 6.39.4
1947
'@lezer/highlight': 1.2.3
1948
1949
+
'@iconify-json/lucide@1.2.81':
1950
dependencies:
1951
'@iconify/types': 2.0.0
1952
1953
+
'@iconify/tailwind4@1.2.0(tailwindcss@4.1.18)':
1954
dependencies:
1955
+
'@iconify/tools': 5.0.0
1956
'@iconify/types': 2.0.0
1957
+
'@iconify/utils': 3.1.0
1958
+
tailwindcss: 4.1.18
1959
1960
+
'@iconify/tools@5.0.0':
1961
dependencies:
1962
+
'@cyberalien/svg-utils': 1.0.11
1963
'@iconify/types': 2.0.0
1964
+
'@iconify/utils': 3.1.0
1965
+
fflate: 0.8.2
1966
+
modern-tar: 0.7.2
1967
+
pathe: 2.0.3
1968
+
svgo: 4.0.0
1969
1970
'@iconify/types@2.0.0': {}
1971
1972
+
'@iconify/utils@3.1.0':
1973
dependencies:
1974
'@antfu/install-pkg': 1.1.0
1975
'@iconify/types': 2.0.0
1976
mlly: 1.8.0
1977
1978
'@jridgewell/gen-mapping@0.3.13':
1979
dependencies:
···
1996
1997
'@jsr/mary__exif-rm@0.2.2': {}
1998
1999
+
'@lezer/common@1.4.0': {}
2000
2001
'@lezer/highlight@1.2.3':
2002
dependencies:
2003
+
'@lezer/common': 1.4.0
2004
2005
'@lezer/json@1.0.3':
2006
dependencies:
2007
+
'@lezer/common': 1.4.0
2008
'@lezer/highlight': 1.2.3
2009
+
'@lezer/lr': 1.4.5
2010
2011
+
'@lezer/lr@1.4.5':
2012
dependencies:
2013
+
'@lezer/common': 1.4.0
2014
2015
'@marijn/find-cluster-break@1.0.2': {}
2016
···
2098
2099
'@standard-schema/spec@1.0.0': {}
2100
2101
+
'@tailwindcss/node@4.1.18':
2102
dependencies:
2103
'@jridgewell/remapping': 2.3.5
2104
+
enhanced-resolve: 5.18.4
2105
jiti: 2.6.1
2106
lightningcss: 1.30.2
2107
magic-string: 0.30.21
2108
source-map-js: 1.2.1
2109
+
tailwindcss: 4.1.18
2110
2111
+
'@tailwindcss/oxide-android-arm64@4.1.18':
2112
optional: true
2113
2114
+
'@tailwindcss/oxide-darwin-arm64@4.1.18':
2115
optional: true
2116
2117
+
'@tailwindcss/oxide-darwin-x64@4.1.18':
2118
optional: true
2119
2120
+
'@tailwindcss/oxide-freebsd-x64@4.1.18':
2121
optional: true
2122
2123
+
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
2124
optional: true
2125
2126
+
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
2127
optional: true
2128
2129
+
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
2130
optional: true
2131
2132
+
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
2133
optional: true
2134
2135
+
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
2136
optional: true
2137
2138
+
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
2139
optional: true
2140
2141
+
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
2142
optional: true
2143
2144
+
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
2145
optional: true
2146
2147
+
'@tailwindcss/oxide@4.1.18':
2148
optionalDependencies:
2149
+
'@tailwindcss/oxide-android-arm64': 4.1.18
2150
+
'@tailwindcss/oxide-darwin-arm64': 4.1.18
2151
+
'@tailwindcss/oxide-darwin-x64': 4.1.18
2152
+
'@tailwindcss/oxide-freebsd-x64': 4.1.18
2153
+
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
2154
+
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
2155
+
'@tailwindcss/oxide-linux-arm64-musl': 4.1.18
2156
+
'@tailwindcss/oxide-linux-x64-gnu': 4.1.18
2157
+
'@tailwindcss/oxide-linux-x64-musl': 4.1.18
2158
+
'@tailwindcss/oxide-wasm32-wasi': 4.1.18
2159
+
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
2160
+
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18
2161
2162
+
'@tailwindcss/vite@4.1.18(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))':
2163
dependencies:
2164
+
'@tailwindcss/node': 4.1.18
2165
+
'@tailwindcss/oxide': 4.1.18
2166
+
tailwindcss: 4.1.18
2167
+
vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
2168
2169
'@types/babel__core@7.20.5':
2170
dependencies:
···
2192
'@types/node@24.10.1':
2193
dependencies:
2194
undici-types: 7.16.0
2195
optional: true
2196
2197
acorn@8.15.0: {}
2198
2199
babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.5):
2200
dependencies:
···
2212
optionalDependencies:
2213
solid-js: 1.9.10
2214
2215
+
baseline-browser-mapping@2.9.7: {}
2216
2217
boolbase@1.0.0: {}
2218
2219
+
browserslist@4.28.1:
2220
dependencies:
2221
+
baseline-browser-mapping: 2.9.7
2222
+
caniuse-lite: 1.0.30001760
2223
+
electron-to-chromium: 1.5.267
2224
node-releases: 2.0.27
2225
+
update-browserslist-db: 1.2.2(browserslist@4.28.1)
2226
2227
+
caniuse-lite@1.0.30001760: {}
2228
2229
codemirror@6.0.2:
2230
dependencies:
···
2234
'@codemirror/lint': 6.9.2
2235
'@codemirror/search': 6.5.11
2236
'@codemirror/state': 6.5.2
2237
+
'@codemirror/view': 6.39.4
2238
2239
+
commander@11.1.0: {}
2240
2241
confbox@0.1.8: {}
2242
2243
convert-source-map@2.0.0: {}
2244
···
2257
mdn-data: 2.0.28
2258
source-map-js: 1.2.1
2259
2260
+
css-tree@3.1.0:
2261
dependencies:
2262
+
mdn-data: 2.12.2
2263
source-map-js: 1.2.1
2264
2265
css-what@6.2.2: {}
···
2273
debug@4.4.3:
2274
dependencies:
2275
ms: 2.1.3
2276
2277
detect-libc@2.1.2: {}
2278
···
2294
domelementtype: 2.3.0
2295
domhandler: 5.0.3
2296
2297
+
electron-to-chromium@1.5.267: {}
2298
2299
+
enhanced-resolve@5.18.4:
2300
dependencies:
2301
graceful-fs: 4.2.11
2302
tapable: 2.3.0
···
2304
entities@4.5.0: {}
2305
2306
entities@6.0.1: {}
2307
2308
esbuild@0.23.1:
2309
optionalDependencies:
···
2366
2367
esm-env@1.2.2: {}
2368
2369
fdir@6.5.0(picomatch@4.0.3):
2370
optionalDependencies:
2371
picomatch: 4.0.3
2372
2373
+
fflate@0.8.2: {}
2374
2375
fsevents@2.3.3:
2376
optional: true
2377
2378
gensync@1.0.0-beta.2: {}
2379
2380
get-tsconfig@4.13.0:
2381
dependencies:
2382
resolve-pkg-maps: 1.0.0
2383
optional: true
2384
2385
graceful-fs@4.2.11: {}
2386
2387
html-entities@2.3.3: {}
2388
2389
is-what@4.1.16: {}
2390
2391
jiti@2.6.1: {}
···
2395
jsesc@3.1.0: {}
2396
2397
json5@2.2.3: {}
2398
2399
lightningcss-android-arm64@1.30.2:
2400
optional: true
···
2445
lightningcss-win32-arm64-msvc: 1.30.2
2446
lightningcss-win32-x64-msvc: 1.30.2
2447
2448
lru-cache@5.1.1:
2449
dependencies:
2450
yallist: 3.1.1
···
2453
dependencies:
2454
'@jridgewell/sourcemap-codec': 1.5.5
2455
2456
mdn-data@2.0.28: {}
2457
2458
+
mdn-data@2.12.2: {}
2459
2460
merge-anything@5.1.7:
2461
dependencies:
2462
is-what: 4.1.16
2463
2464
mlly@1.8.0:
2465
dependencies:
···
2468
pkg-types: 1.3.1
2469
ufo: 1.6.1
2470
2471
+
modern-tar@0.7.2: {}
2472
+
2473
ms@2.1.3: {}
2474
2475
nanoevents@9.1.0: {}
···
2484
dependencies:
2485
boolbase: 1.0.0
2486
2487
+
package-manager-detector@1.6.0: {}
2488
2489
parse5@7.3.0:
2490
dependencies:
2491
entities: 6.0.1
2492
2493
pathe@2.0.3: {}
2494
2495
picocolors@1.1.1: {}
2496
···
2500
dependencies:
2501
confbox: 0.1.8
2502
mlly: 1.8.0
2503
pathe: 2.0.3
2504
2505
postcss@8.5.6:
···
2508
picocolors: 1.1.1
2509
source-map-js: 1.2.1
2510
2511
+
prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3):
2512
dependencies:
2513
+
prettier: 3.7.4
2514
typescript: 5.9.3
2515
2516
+
prettier-plugin-tailwindcss@0.7.2(prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3))(prettier@3.7.4):
2517
dependencies:
2518
+
prettier: 3.7.4
2519
optionalDependencies:
2520
+
prettier-plugin-organize-imports: 4.3.0(prettier@3.7.4)(typescript@5.9.3)
2521
2522
+
prettier@3.7.4: {}
2523
2524
resolve-pkg-maps@1.0.0:
2525
optional: true
···
2552
'@rollup/rollup-win32-x64-msvc': 4.53.3
2553
fsevents: 2.3.3
2554
2555
+
sax@1.4.3: {}
2556
2557
semver@6.3.1: {}
2558
···
2581
2582
style-mod@4.1.3: {}
2583
2584
+
svgo@4.0.0:
2585
dependencies:
2586
+
commander: 11.1.0
2587
css-select: 5.2.2
2588
+
css-tree: 3.1.0
2589
css-what: 6.2.2
2590
csso: 5.0.5
2591
picocolors: 1.1.1
2592
+
sax: 1.4.3
2593
2594
+
tailwindcss@4.1.18: {}
2595
2596
tapable@2.3.0: {}
2597
2598
tinyexec@1.0.2: {}
2599
2600
tinyglobby@0.2.15:
···
2614
2615
ufo@1.6.1: {}
2616
2617
+
undici-types@7.16.0:
2618
+
optional: true
2619
2620
+
update-browserslist-db@1.2.2(browserslist@4.28.1):
2621
dependencies:
2622
+
browserslist: 4.28.1
2623
escalade: 3.2.0
2624
picocolors: 1.1.1
2625
2626
+
vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)):
2627
dependencies:
2628
'@babel/core': 7.28.5
2629
'@types/babel__core': 7.20.5
···
2631
merge-anything: 5.1.7
2632
solid-js: 1.9.10
2633
solid-refresh: 0.6.3(solid-js@1.9.10)
2634
+
vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
2635
+
vitefu: 1.1.1(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))
2636
transitivePeerDependencies:
2637
- supports-color
2638
2639
+
vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2):
2640
dependencies:
2641
esbuild: 0.25.12
2642
fdir: 6.5.0(picomatch@4.0.3)
···
2651
lightningcss: 1.30.2
2652
tsx: 4.19.2
2653
2654
+
vitefu@1.1.1(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)):
2655
optionalDependencies:
2656
+
vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
2657
2658
w3c-keyname@2.2.8: {}
2659
2660
yallist@3.1.1: {}
2661
2662
yocto-queue@1.2.2: {}
public/favicon.ico
public/favicon.ico
This is a binary file and will not be displayed.
public/fonts/Figtree[wght].woff2
public/fonts/Figtree[wght].woff2
This is a binary file and will not be displayed.
+1
-1
public/oauth-client-metadata.json
+1
-1
public/oauth-client-metadata.json
···
4
"client_uri": "https://pdsls.dev",
5
"logo_uri": "https://pdsls.dev/favicon.ico",
6
"redirect_uris": ["https://pdsls.dev/"],
7
-
"scope": "atproto transition:generic",
8
"grant_types": ["authorization_code", "refresh_token"],
9
"response_types": ["code"],
10
"token_endpoint_auth_method": "none",
···
4
"client_uri": "https://pdsls.dev",
5
"logo_uri": "https://pdsls.dev/favicon.ico",
6
"redirect_uris": ["https://pdsls.dev/"],
7
+
"scope": "atproto repo:*?action=create repo:*?action=update repo:*?action=delete blob:*/*",
8
"grant_types": ["authorization_code", "refresh_token"],
9
"response_types": ["code"],
10
"token_endpoint_auth_method": "none",
+194
src/auth/account.tsx
+194
src/auth/account.tsx
···
···
1
+
import { Did } from "@atcute/lexicons";
2
+
import { deleteStoredSession, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client";
3
+
import { A } from "@solidjs/router";
4
+
import { createSignal, For, onMount, Show } from "solid-js";
5
+
import { createStore, produce } from "solid-js/store";
6
+
import { ActionMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown.jsx";
7
+
import { Modal } from "../components/modal.jsx";
8
+
import { Login } from "./login.jsx";
9
+
import { useOAuthScopeFlow } from "./scope-flow.js";
10
+
import { ScopeSelector } from "./scope-selector.jsx";
11
+
import { parseScopeString } from "./scope-utils.js";
12
+
import {
13
+
getAvatar,
14
+
loadHandleForSession,
15
+
loadSessionsFromStorage,
16
+
resumeSession,
17
+
retrieveSession,
18
+
saveSessionToStorage,
19
+
} from "./session-manager.js";
20
+
import { agent, sessions, setAgent, setSessions } from "./state.js";
21
+
22
+
const AccountDropdown = (props: { did: Did; onEditPermissions: (did: Did) => void }) => {
23
+
const removeSession = async (did: Did) => {
24
+
const currentSession = agent()?.sub;
25
+
try {
26
+
const session = await getSession(did, { allowStale: true });
27
+
const agent = new OAuthUserAgent(session);
28
+
await agent.signOut();
29
+
} catch {
30
+
deleteStoredSession(did);
31
+
}
32
+
setSessions(
33
+
produce((accs) => {
34
+
delete accs[did];
35
+
}),
36
+
);
37
+
saveSessionToStorage(sessions);
38
+
if (currentSession === did) setAgent(undefined);
39
+
};
40
+
41
+
return (
42
+
<MenuProvider>
43
+
<DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-md p-2">
44
+
<NavMenu
45
+
href={`/at://${props.did}`}
46
+
label={agent()?.sub === props.did ? "Go to repo (g)" : "Go to repo"}
47
+
icon="lucide--user-round"
48
+
/>
49
+
<ActionMenu
50
+
icon="lucide--settings"
51
+
label="Edit permissions"
52
+
onClick={() => props.onEditPermissions(props.did)}
53
+
/>
54
+
<ActionMenu
55
+
icon="lucide--x"
56
+
label="Remove account"
57
+
onClick={() => removeSession(props.did)}
58
+
/>
59
+
</DropdownMenu>
60
+
</MenuProvider>
61
+
);
62
+
};
63
+
64
+
export const AccountManager = () => {
65
+
const [openManager, setOpenManager] = createSignal(false);
66
+
const [avatars, setAvatars] = createStore<Record<Did, string>>();
67
+
const [showingAddAccount, setShowingAddAccount] = createSignal(false);
68
+
69
+
const getThumbnailUrl = (avatarUrl: string) => {
70
+
return avatarUrl.replace("img/avatar/", "img/avatar_thumbnail/");
71
+
};
72
+
73
+
const scopeFlow = useOAuthScopeFlow({
74
+
beforeRedirect: (account) => resumeSession(account as Did),
75
+
});
76
+
77
+
const handleAccountClick = async (did: Did) => {
78
+
try {
79
+
await resumeSession(did);
80
+
} catch {
81
+
scopeFlow.initiate(did);
82
+
}
83
+
};
84
+
85
+
onMount(async () => {
86
+
try {
87
+
await retrieveSession();
88
+
} catch {}
89
+
90
+
const storedSessions = loadSessionsFromStorage();
91
+
if (storedSessions) {
92
+
const sessionDids = Object.keys(storedSessions) as Did[];
93
+
sessionDids.forEach(async (did) => {
94
+
await loadHandleForSession(did, storedSessions);
95
+
});
96
+
sessionDids.forEach(async (did) => {
97
+
const avatar = await getAvatar(did);
98
+
if (avatar) setAvatars(did, avatar);
99
+
});
100
+
}
101
+
});
102
+
103
+
return (
104
+
<>
105
+
<Modal
106
+
open={openManager()}
107
+
onClose={() => {
108
+
setOpenManager(false);
109
+
setShowingAddAccount(false);
110
+
scopeFlow.cancel();
111
+
}}
112
+
>
113
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-18 left-[50%] w-88 -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
114
+
<Show when={!scopeFlow.showScopeSelector() && !showingAddAccount()}>
115
+
<div class="mb-2 px-1 font-semibold">
116
+
<span>Manage accounts</span>
117
+
</div>
118
+
<div class="mb-3 max-h-80 overflow-y-auto md:max-h-100">
119
+
<For each={Object.keys(sessions)}>
120
+
{(did) => (
121
+
<div class="flex w-full items-center justify-between">
122
+
<A
123
+
href={`/at://${did}`}
124
+
onClick={() => setOpenManager(false)}
125
+
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
126
+
>
127
+
<Show
128
+
when={avatars[did as Did]}
129
+
fallback={<span class="iconify lucide--user-round m-0.5 size-5"></span>}
130
+
>
131
+
<img
132
+
src={getThumbnailUrl(avatars[did as Did])}
133
+
class="size-6 rounded-full"
134
+
/>
135
+
</Show>
136
+
</A>
137
+
<button
138
+
class="flex grow items-center justify-between gap-1 truncate rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
139
+
onclick={() => handleAccountClick(did as Did)}
140
+
>
141
+
<span class="truncate">{sessions[did]?.handle || did}</span>
142
+
<Show when={did === agent()?.sub && sessions[did].signedIn}>
143
+
<span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span>
144
+
</Show>
145
+
<Show when={!sessions[did].signedIn}>
146
+
<span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span>
147
+
</Show>
148
+
</button>
149
+
<AccountDropdown
150
+
did={did as Did}
151
+
onEditPermissions={(accountDid) => scopeFlow.initiateWithRedirect(accountDid)}
152
+
/>
153
+
</div>
154
+
)}
155
+
</For>
156
+
</div>
157
+
<button
158
+
onclick={() => setShowingAddAccount(true)}
159
+
class="flex w-full items-center justify-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
160
+
>
161
+
<span class="iconify lucide--user-plus"></span>
162
+
<span>Add account</span>
163
+
</button>
164
+
</Show>
165
+
166
+
<Show when={showingAddAccount() && !scopeFlow.showScopeSelector()}>
167
+
<Login onCancel={() => setShowingAddAccount(false)} />
168
+
</Show>
169
+
170
+
<Show when={scopeFlow.showScopeSelector()}>
171
+
<ScopeSelector
172
+
initialScopes={parseScopeString(
173
+
sessions[scopeFlow.pendingAccount()]?.grantedScopes || "",
174
+
)}
175
+
onConfirm={scopeFlow.complete}
176
+
onCancel={() => {
177
+
scopeFlow.cancel();
178
+
setShowingAddAccount(false);
179
+
}}
180
+
/>
181
+
</Show>
182
+
</div>
183
+
</Modal>
184
+
<button
185
+
onclick={() => setOpenManager(true)}
186
+
class={`flex items-center rounded-lg ${agent() && avatars[agent()!.sub] ? "p-1.25" : "p-1.5"} hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`}
187
+
>
188
+
{agent() && avatars[agent()!.sub] ?
189
+
<img src={getThumbnailUrl(avatars[agent()!.sub])} class="size-5 rounded-full" />
190
+
: <span class="iconify lucide--circle-user-round text-lg"></span>}
191
+
</button>
192
+
</>
193
+
);
194
+
};
+88
src/auth/login.tsx
+88
src/auth/login.tsx
···
···
1
+
import { createSignal, Show } from "solid-js";
2
+
import "./oauth-config";
3
+
import { useOAuthScopeFlow } from "./scope-flow";
4
+
import { ScopeSelector } from "./scope-selector";
5
+
6
+
interface LoginProps {
7
+
onCancel?: () => void;
8
+
}
9
+
10
+
export const Login = (props: LoginProps) => {
11
+
const [notice, setNotice] = createSignal("");
12
+
const [loginInput, setLoginInput] = createSignal("");
13
+
14
+
const scopeFlow = useOAuthScopeFlow({
15
+
onError: (e) => setNotice(`${e}`),
16
+
onRedirecting: () => {
17
+
setNotice(`Contacting your data server...`);
18
+
setTimeout(() => setNotice(`Redirecting...`), 0);
19
+
},
20
+
});
21
+
22
+
const initiateLogin = (handle: string) => {
23
+
setNotice("");
24
+
scopeFlow.initiate(handle);
25
+
};
26
+
27
+
const handleCancel = () => {
28
+
scopeFlow.cancel();
29
+
setLoginInput("");
30
+
setNotice("");
31
+
props.onCancel?.();
32
+
};
33
+
34
+
return (
35
+
<div class="flex flex-col gap-y-2 px-1">
36
+
<Show when={!scopeFlow.showScopeSelector()}>
37
+
<Show when={props.onCancel}>
38
+
<div class="mb-1 flex items-center gap-2">
39
+
<button
40
+
onclick={handleCancel}
41
+
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
42
+
>
43
+
<span class="iconify lucide--arrow-left"></span>
44
+
</button>
45
+
<div class="font-semibold">Add account</div>
46
+
</div>
47
+
</Show>
48
+
<form class="flex flex-col gap-2" onsubmit={(e) => e.preventDefault()}>
49
+
<label for="username" class="hidden">
50
+
Add account
51
+
</label>
52
+
<div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex grow items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400">
53
+
<label
54
+
for="username"
55
+
class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400"
56
+
></label>
57
+
<input
58
+
type="text"
59
+
spellcheck={false}
60
+
placeholder="user.bsky.social"
61
+
id="username"
62
+
name="username"
63
+
autocomplete="username"
64
+
autofocus
65
+
aria-label="Your AT Protocol handle"
66
+
class="grow py-1 select-none placeholder:text-sm focus:outline-none"
67
+
onInput={(e) => setLoginInput(e.currentTarget.value)}
68
+
/>
69
+
</div>
70
+
<button
71
+
onclick={() => initiateLogin(loginInput())}
72
+
class="grow rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
73
+
>
74
+
Continue
75
+
</button>
76
+
</form>
77
+
</Show>
78
+
79
+
<Show when={scopeFlow.showScopeSelector()}>
80
+
<ScopeSelector onConfirm={scopeFlow.complete} onCancel={handleCancel} />
81
+
</Show>
82
+
83
+
<Show when={notice()}>
84
+
<div class="text-sm">{notice()}</div>
85
+
</Show>
86
+
</div>
87
+
);
88
+
};
+13
src/auth/oauth-config.ts
+13
src/auth/oauth-config.ts
···
···
1
+
import { configureOAuth, defaultIdentityResolver } from "@atcute/oauth-browser-client";
2
+
import { didDocumentResolver, handleResolver } from "../utils/api";
3
+
4
+
configureOAuth({
5
+
metadata: {
6
+
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
7
+
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
8
+
},
9
+
identityResolver: defaultIdentityResolver({
10
+
handleResolver: handleResolver,
11
+
didDocumentResolver: didDocumentResolver,
12
+
}),
13
+
});
+77
src/auth/scope-flow.ts
+77
src/auth/scope-flow.ts
···
···
1
+
import { isDid, isHandle } from "@atcute/lexicons/syntax";
2
+
import { createAuthorizationUrl } from "@atcute/oauth-browser-client";
3
+
import { createSignal } from "solid-js";
4
+
5
+
interface UseOAuthScopeFlowOptions {
6
+
onError?: (error: unknown) => void;
7
+
onRedirecting?: () => void;
8
+
beforeRedirect?: (account: string) => Promise<void>;
9
+
}
10
+
11
+
export const useOAuthScopeFlow = (options: UseOAuthScopeFlowOptions = {}) => {
12
+
const [showScopeSelector, setShowScopeSelector] = createSignal(false);
13
+
const [pendingAccount, setPendingAccount] = createSignal("");
14
+
const [shouldForceRedirect, setShouldForceRedirect] = createSignal(false);
15
+
16
+
const initiate = (account: string) => {
17
+
if (!account) return;
18
+
setPendingAccount(account);
19
+
setShouldForceRedirect(false);
20
+
setShowScopeSelector(true);
21
+
};
22
+
23
+
const initiateWithRedirect = (account: string) => {
24
+
if (!account) return;
25
+
setPendingAccount(account);
26
+
setShouldForceRedirect(true);
27
+
setShowScopeSelector(true);
28
+
};
29
+
30
+
const complete = async (scopeString: string, scopeIds: string) => {
31
+
try {
32
+
const account = pendingAccount();
33
+
34
+
if (options.beforeRedirect && !shouldForceRedirect()) {
35
+
try {
36
+
await options.beforeRedirect(account);
37
+
setShowScopeSelector(false);
38
+
return;
39
+
} catch {}
40
+
}
41
+
42
+
localStorage.setItem("pendingScopes", scopeIds);
43
+
44
+
options.onRedirecting?.();
45
+
46
+
const authUrl = await createAuthorizationUrl({
47
+
scope: scopeString,
48
+
target:
49
+
isHandle(account) || isDid(account) ?
50
+
{ type: "account", identifier: account }
51
+
: { type: "pds", serviceUrl: account },
52
+
});
53
+
54
+
await new Promise((resolve) => setTimeout(resolve, 250));
55
+
location.assign(authUrl);
56
+
} catch (e) {
57
+
console.error(e);
58
+
options.onError?.(e);
59
+
setShowScopeSelector(false);
60
+
}
61
+
};
62
+
63
+
const cancel = () => {
64
+
setShowScopeSelector(false);
65
+
setPendingAccount("");
66
+
setShouldForceRedirect(false);
67
+
};
68
+
69
+
return {
70
+
showScopeSelector,
71
+
pendingAccount,
72
+
initiate,
73
+
initiateWithRedirect,
74
+
complete,
75
+
cancel,
76
+
};
77
+
};
+86
src/auth/scope-selector.tsx
+86
src/auth/scope-selector.tsx
···
···
1
+
import { createSignal, For } from "solid-js";
2
+
import { buildScopeString, GRANULAR_SCOPES, scopeIdsToString } from "./scope-utils";
3
+
4
+
interface ScopeSelectorProps {
5
+
onConfirm: (scopeString: string, scopeIds: string) => void;
6
+
onCancel: () => void;
7
+
initialScopes?: Set<string>;
8
+
}
9
+
10
+
export const ScopeSelector = (props: ScopeSelectorProps) => {
11
+
const [selectedScopes, setSelectedScopes] = createSignal<Set<string>>(
12
+
props.initialScopes || new Set(["create", "update", "delete", "blob"]),
13
+
);
14
+
15
+
const isBlobDisabled = () => {
16
+
const scopes = selectedScopes();
17
+
return !scopes.has("create") && !scopes.has("update");
18
+
};
19
+
20
+
const toggleScope = (scopeId: string) => {
21
+
setSelectedScopes((prev) => {
22
+
const newSet = new Set(prev);
23
+
if (newSet.has(scopeId)) {
24
+
newSet.delete(scopeId);
25
+
if (
26
+
(scopeId === "create" || scopeId === "update") &&
27
+
!newSet.has("create") &&
28
+
!newSet.has("update")
29
+
) {
30
+
newSet.delete("blob");
31
+
}
32
+
} else {
33
+
newSet.add(scopeId);
34
+
}
35
+
return newSet;
36
+
});
37
+
};
38
+
39
+
const handleConfirm = () => {
40
+
const scopes = selectedScopes();
41
+
const scopeString = buildScopeString(scopes);
42
+
const scopeIds = scopeIdsToString(scopes);
43
+
props.onConfirm(scopeString, scopeIds);
44
+
};
45
+
46
+
return (
47
+
<div class="flex flex-col gap-y-2">
48
+
<div class="mb-1 flex items-center gap-2">
49
+
<button
50
+
onclick={props.onCancel}
51
+
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
52
+
>
53
+
<span class="iconify lucide--arrow-left"></span>
54
+
</button>
55
+
<div class="font-semibold">Select permissions</div>
56
+
</div>
57
+
<div class="flex flex-col gap-y-2 px-1">
58
+
<For each={GRANULAR_SCOPES}>
59
+
{(scope) => (
60
+
<div
61
+
class="flex items-center gap-2"
62
+
classList={{ "opacity-50": scope.id === "blob" && isBlobDisabled() }}
63
+
>
64
+
<input
65
+
id={`scope-${scope.id}`}
66
+
type="checkbox"
67
+
checked={selectedScopes().has(scope.id)}
68
+
disabled={scope.id === "blob" && isBlobDisabled()}
69
+
onChange={() => toggleScope(scope.id)}
70
+
/>
71
+
<label for={`scope-${scope.id}`} class="flex grow items-center gap-2 select-none">
72
+
<span>{scope.label}</span>
73
+
</label>
74
+
</div>
75
+
)}
76
+
</For>
77
+
</div>
78
+
<button
79
+
onclick={handleConfirm}
80
+
class="mt-2 grow rounded-lg border-[0.5px] border-neutral-300 bg-neutral-100 px-3 py-2 hover:bg-neutral-200 active:bg-neutral-300 dark:border-neutral-600 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
81
+
>
82
+
Continue
83
+
</button>
84
+
</div>
85
+
);
86
+
};
+53
src/auth/scope-utils.ts
+53
src/auth/scope-utils.ts
···
···
1
+
import { agent, sessions } from "./state";
2
+
3
+
export const GRANULAR_SCOPES = [
4
+
{
5
+
id: "create",
6
+
scope: "repo:*?action=create",
7
+
label: "Create records",
8
+
},
9
+
{
10
+
id: "update",
11
+
scope: "repo:*?action=update",
12
+
label: "Update records",
13
+
},
14
+
{
15
+
id: "delete",
16
+
scope: "repo:*?action=delete",
17
+
label: "Delete records",
18
+
},
19
+
{
20
+
id: "blob",
21
+
scope: "blob:*/*",
22
+
label: "Upload blobs",
23
+
},
24
+
];
25
+
26
+
export const BASE_SCOPES = ["atproto"];
27
+
28
+
export const buildScopeString = (selected: Set<string>): string => {
29
+
const granular = GRANULAR_SCOPES.filter((s) => selected.has(s.id)).map((s) => s.scope);
30
+
return [...BASE_SCOPES, ...granular].join(" ");
31
+
};
32
+
33
+
export const scopeIdsToString = (scopeIds: Set<string>): string => {
34
+
return ["atproto", ...Array.from(scopeIds)].join(",");
35
+
};
36
+
37
+
export const parseScopeString = (scopeIdsString: string): Set<string> => {
38
+
if (!scopeIdsString) return new Set();
39
+
const ids = scopeIdsString.split(",").filter(Boolean);
40
+
return new Set(ids.filter((id) => id !== "atproto"));
41
+
};
42
+
43
+
export const hasScope = (grantedScopes: string | undefined, scopeId: string): boolean => {
44
+
if (!grantedScopes) return false;
45
+
return grantedScopes.split(",").includes(scopeId);
46
+
};
47
+
48
+
export const hasUserScope = (scopeId: string): boolean => {
49
+
if (!agent()) return false;
50
+
const grantedScopes = sessions[agent()!.sub]?.grantedScopes;
51
+
if (!grantedScopes) return true;
52
+
return hasScope(grantedScopes, scopeId);
53
+
};
+95
src/auth/session-manager.ts
+95
src/auth/session-manager.ts
···
···
1
+
import { Client, simpleFetchHandler } from "@atcute/client";
2
+
import { Did } from "@atcute/lexicons";
3
+
import {
4
+
finalizeAuthorization,
5
+
getSession,
6
+
OAuthUserAgent,
7
+
type Session,
8
+
} from "@atcute/oauth-browser-client";
9
+
import { resolveDidDoc } from "../utils/api";
10
+
import { Sessions, setAgent, setSessions } from "./state";
11
+
12
+
export const saveSessionToStorage = (sessions: Sessions) => {
13
+
localStorage.setItem("sessions", JSON.stringify(sessions));
14
+
};
15
+
16
+
export const loadSessionsFromStorage = (): Sessions | null => {
17
+
const localSessions = localStorage.getItem("sessions");
18
+
return localSessions ? JSON.parse(localSessions) : null;
19
+
};
20
+
21
+
export const getAvatar = async (did: Did): Promise<string | undefined> => {
22
+
const rpc = new Client({
23
+
handler: simpleFetchHandler({ service: "https://public.api.bsky.app" }),
24
+
});
25
+
const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } });
26
+
if (res.ok) {
27
+
return res.data.avatar;
28
+
}
29
+
return undefined;
30
+
};
31
+
32
+
export const loadHandleForSession = async (did: Did, storedSessions: Sessions) => {
33
+
const doc = await resolveDidDoc(did);
34
+
const alias = doc.alsoKnownAs?.find((alias) => alias.startsWith("at://"));
35
+
if (alias) {
36
+
setSessions(did, {
37
+
signedIn: storedSessions[did].signedIn,
38
+
handle: alias.replace("at://", ""),
39
+
grantedScopes: storedSessions[did].grantedScopes,
40
+
});
41
+
}
42
+
};
43
+
44
+
export const retrieveSession = async (): Promise<void> => {
45
+
const init = async (): Promise<Session | undefined> => {
46
+
const params = new URLSearchParams(location.hash.slice(1));
47
+
48
+
if (params.has("state") && (params.has("code") || params.has("error"))) {
49
+
history.replaceState(null, "", location.pathname + location.search);
50
+
51
+
const auth = await finalizeAuthorization(params);
52
+
const did = auth.session.info.sub;
53
+
54
+
localStorage.setItem("lastSignedIn", did);
55
+
56
+
const grantedScopes = localStorage.getItem("pendingScopes") || "atproto";
57
+
localStorage.removeItem("pendingScopes");
58
+
59
+
const sessions = loadSessionsFromStorage();
60
+
const newSessions: Sessions = sessions || {};
61
+
newSessions[did] = { signedIn: true, grantedScopes };
62
+
saveSessionToStorage(newSessions);
63
+
return auth.session;
64
+
} else {
65
+
const lastSignedIn = localStorage.getItem("lastSignedIn");
66
+
67
+
if (lastSignedIn) {
68
+
const sessions = loadSessionsFromStorage();
69
+
const newSessions: Sessions = sessions || {};
70
+
try {
71
+
const session = await getSession(lastSignedIn as Did);
72
+
const rpc = new Client({ handler: new OAuthUserAgent(session) });
73
+
const res = await rpc.get("com.atproto.server.getSession");
74
+
newSessions[lastSignedIn].signedIn = true;
75
+
saveSessionToStorage(newSessions);
76
+
if (!res.ok) throw res.data.error;
77
+
return session;
78
+
} catch (err) {
79
+
newSessions[lastSignedIn].signedIn = false;
80
+
saveSessionToStorage(newSessions);
81
+
throw err;
82
+
}
83
+
}
84
+
}
85
+
};
86
+
87
+
const session = await init();
88
+
89
+
if (session) setAgent(new OAuthUserAgent(session));
90
+
};
91
+
92
+
export const resumeSession = async (did: Did): Promise<void> => {
93
+
localStorage.setItem("lastSignedIn", did);
94
+
await retrieveSession();
95
+
};
+14
src/auth/state.ts
+14
src/auth/state.ts
···
···
1
+
import { OAuthUserAgent } from "@atcute/oauth-browser-client";
2
+
import { createSignal } from "solid-js";
3
+
import { createStore } from "solid-js/store";
4
+
5
+
export type Account = {
6
+
signedIn: boolean;
7
+
handle?: string;
8
+
grantedScopes?: string;
9
+
};
10
+
11
+
export type Sessions = Record<string, Account>;
12
+
13
+
export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>();
14
+
export const [sessions, setSessions] = createStore<Sessions>();
-159
src/components/account.tsx
-159
src/components/account.tsx
···
1
-
import { Client, CredentialManager } from "@atcute/client";
2
-
import { Did } from "@atcute/lexicons";
3
-
import {
4
-
createAuthorizationUrl,
5
-
deleteStoredSession,
6
-
getSession,
7
-
OAuthUserAgent,
8
-
} from "@atcute/oauth-browser-client";
9
-
import { A } from "@solidjs/router";
10
-
import { createSignal, For, onMount, Show } from "solid-js";
11
-
import { createStore, produce } from "solid-js/store";
12
-
import { resolveDidDoc } from "../utils/api.js";
13
-
import { agent, Login, retrieveSession, Sessions, setAgent } from "./login.jsx";
14
-
import { Modal } from "./modal.jsx";
15
-
16
-
export const [sessions, setSessions] = createStore<Sessions>();
17
-
18
-
export const AccountManager = () => {
19
-
const [openManager, setOpenManager] = createSignal(false);
20
-
const [avatars, setAvatars] = createStore<Record<Did, string>>();
21
-
22
-
onMount(async () => {
23
-
try {
24
-
await retrieveSession();
25
-
} catch {}
26
-
27
-
const localSessions = localStorage.getItem("sessions");
28
-
if (localSessions) {
29
-
const storedSessions: Sessions = JSON.parse(localSessions);
30
-
const sessionDids = Object.keys(storedSessions) as Did[];
31
-
sessionDids.forEach(async (did) => {
32
-
const doc = await resolveDidDoc(did);
33
-
const alias = doc.alsoKnownAs?.find((alias) => alias.startsWith("at://"));
34
-
if (alias) {
35
-
setSessions(did, {
36
-
signedIn: storedSessions[did].signedIn,
37
-
handle: alias.replace("at://", ""),
38
-
});
39
-
}
40
-
});
41
-
sessionDids.forEach(async (did) => {
42
-
const avatar = await getAvatar(did);
43
-
if (avatar) setAvatars(did, avatar);
44
-
});
45
-
}
46
-
});
47
-
48
-
const resumeSession = async (did: Did) => {
49
-
try {
50
-
localStorage.setItem("lastSignedIn", did);
51
-
await retrieveSession();
52
-
} catch {
53
-
const authUrl = await createAuthorizationUrl({
54
-
scope: import.meta.env.VITE_OAUTH_SCOPE,
55
-
target: { type: "account", identifier: did },
56
-
});
57
-
58
-
await new Promise((resolve) => setTimeout(resolve, 250));
59
-
60
-
location.assign(authUrl);
61
-
}
62
-
};
63
-
64
-
const removeSession = async (did: Did) => {
65
-
const currentSession = agent()?.sub;
66
-
try {
67
-
const session = await getSession(did, { allowStale: true });
68
-
const agent = new OAuthUserAgent(session);
69
-
await agent.signOut();
70
-
} catch {
71
-
deleteStoredSession(did);
72
-
}
73
-
setSessions(
74
-
produce((accs) => {
75
-
delete accs[did];
76
-
}),
77
-
);
78
-
localStorage.setItem("sessions", JSON.stringify(sessions));
79
-
if (currentSession === did) setAgent(undefined);
80
-
};
81
-
82
-
const getAvatar = async (did: Did) => {
83
-
const rpc = new Client({
84
-
handler: new CredentialManager({ service: "https://public.api.bsky.app" }),
85
-
});
86
-
const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } });
87
-
if (res.ok) {
88
-
return res.data.avatar;
89
-
}
90
-
return undefined;
91
-
};
92
-
93
-
return (
94
-
<>
95
-
<Modal open={openManager()} onClose={() => setOpenManager(false)}>
96
-
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-18 left-[50%] w-88 -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
97
-
<div class="mb-2 px-1 font-semibold">
98
-
<span>Manage accounts</span>
99
-
</div>
100
-
<div class="mb-3 max-h-80 overflow-y-auto md:max-h-100">
101
-
<For each={Object.keys(sessions)}>
102
-
{(did) => (
103
-
<div class="flex items-center">
104
-
<button
105
-
class="flex w-full items-center justify-between gap-1 truncate rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
106
-
onclick={() => resumeSession(did as Did)}
107
-
>
108
-
<span class="flex items-center gap-2 truncate">
109
-
<Show when={avatars[did as Did]}>
110
-
<img
111
-
src={avatars[did as Did].replace("img/avatar/", "img/avatar_thumbnail/")}
112
-
class="size-6 rounded-full"
113
-
/>
114
-
</Show>
115
-
<span class="truncate">
116
-
{sessions[did]?.handle ? sessions[did].handle : did}
117
-
</span>
118
-
</span>
119
-
<Show when={did === agent()?.sub && sessions[did].signedIn}>
120
-
<span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span>
121
-
</Show>
122
-
<Show when={!sessions[did].signedIn}>
123
-
<span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span>
124
-
</Show>
125
-
</button>
126
-
<A
127
-
href={`/at://${did}`}
128
-
onClick={() => setOpenManager(false)}
129
-
class="flex items-center rounded-lg p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
130
-
>
131
-
<span class="iconify lucide--user-round"></span>
132
-
</A>
133
-
<button
134
-
onclick={() => removeSession(did as Did)}
135
-
class="flex items-center rounded-lg p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
136
-
>
137
-
<span class="iconify lucide--x"></span>
138
-
</button>
139
-
</div>
140
-
)}
141
-
</For>
142
-
</div>
143
-
<Login />
144
-
</div>
145
-
</Modal>
146
-
<button
147
-
onclick={() => setOpenManager(true)}
148
-
class={`flex items-center rounded-lg ${agent() && avatars[agent()!.sub] ? "p-1.25" : "p-1.5"} hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`}
149
-
>
150
-
{agent() && avatars[agent()!.sub] ?
151
-
<img
152
-
src={avatars[agent()!.sub].replace("img/avatar/", "img/avatar_thumbnail/")}
153
-
class="size-5 rounded-full"
154
-
/>
155
-
: <span class="iconify lucide--circle-user-round text-lg"></span>}
156
-
</button>
157
-
</>
158
-
);
159
-
};
···
+109
src/components/create/file-upload.tsx
+109
src/components/create/file-upload.tsx
···
···
1
+
import { Client } from "@atcute/client";
2
+
import { remove } from "@mary/exif-rm";
3
+
import { createSignal, onCleanup, Show } from "solid-js";
4
+
import { agent } from "../../auth/state";
5
+
import { Button } from "../button.jsx";
6
+
import { TextInput } from "../text-input.jsx";
7
+
import { editorInstance } from "./state";
8
+
9
+
export const FileUpload = (props: {
10
+
file: File;
11
+
blobInput: HTMLInputElement;
12
+
onClose: () => void;
13
+
}) => {
14
+
const [uploading, setUploading] = createSignal(false);
15
+
const [error, setError] = createSignal("");
16
+
17
+
onCleanup(() => (props.blobInput.value = ""));
18
+
19
+
const formatFileSize = (bytes: number) => {
20
+
if (bytes === 0) return "0 Bytes";
21
+
const k = 1024;
22
+
const sizes = ["Bytes", "KB", "MB", "GB"];
23
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
24
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
25
+
};
26
+
27
+
const uploadBlob = async () => {
28
+
let blob: Blob;
29
+
30
+
const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value;
31
+
(document.getElementById("mimetype") as HTMLInputElement).value = "";
32
+
if (mimetype) blob = new Blob([props.file], { type: mimetype });
33
+
else blob = props.file;
34
+
35
+
if ((document.getElementById("exif-rm") as HTMLInputElement).checked) {
36
+
const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer()));
37
+
if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type });
38
+
}
39
+
40
+
const rpc = new Client({ handler: agent()! });
41
+
setUploading(true);
42
+
const res = await rpc.post("com.atproto.repo.uploadBlob", {
43
+
input: blob,
44
+
});
45
+
setUploading(false);
46
+
if (!res.ok) {
47
+
setError(res.data.error);
48
+
return;
49
+
}
50
+
editorInstance.view.dispatch({
51
+
changes: {
52
+
from: editorInstance.view.state.selection.main.head,
53
+
insert: JSON.stringify(res.data.blob, null, 2),
54
+
},
55
+
});
56
+
props.onClose();
57
+
};
58
+
59
+
return (
60
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
61
+
<h2 class="mb-2 font-semibold">Upload blob</h2>
62
+
<div class="flex flex-col gap-2 text-sm">
63
+
<div class="flex flex-col gap-1">
64
+
<p class="flex gap-1">
65
+
<span class="truncate">{props.file.name}</span>
66
+
<span class="shrink-0 text-neutral-600 dark:text-neutral-400">
67
+
({formatFileSize(props.file.size)})
68
+
</span>
69
+
</p>
70
+
</div>
71
+
<div class="flex items-center gap-x-2">
72
+
<label for="mimetype" class="shrink-0 select-none">
73
+
MIME type
74
+
</label>
75
+
<TextInput id="mimetype" placeholder={props.file.type} />
76
+
</div>
77
+
<div class="flex items-center gap-1">
78
+
<input id="exif-rm" type="checkbox" checked />
79
+
<label for="exif-rm" class="select-none">
80
+
Remove EXIF data
81
+
</label>
82
+
</div>
83
+
<p class="text-xs text-neutral-600 dark:text-neutral-400">
84
+
Metadata will be pasted after the cursor
85
+
</p>
86
+
<Show when={error()}>
87
+
<span class="text-red-500 dark:text-red-400">Error: {error()}</span>
88
+
</Show>
89
+
<div class="flex justify-between gap-2">
90
+
<Button onClick={props.onClose}>Cancel</Button>
91
+
<Show when={uploading()}>
92
+
<div class="flex items-center gap-1">
93
+
<span class="iconify lucide--loader-circle animate-spin"></span>
94
+
<span>Uploading</span>
95
+
</div>
96
+
</Show>
97
+
<Show when={!uploading()}>
98
+
<Button
99
+
onClick={uploadBlob}
100
+
class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400"
101
+
>
102
+
Upload
103
+
</Button>
104
+
</Show>
105
+
</div>
106
+
</div>
107
+
</div>
108
+
);
109
+
};
+87
src/components/create/handle-input.tsx
+87
src/components/create/handle-input.tsx
···
···
1
+
import { Handle } from "@atcute/lexicons";
2
+
import { createSignal, Show } from "solid-js";
3
+
import { resolveHandle } from "../../utils/api";
4
+
import { Button } from "../button.jsx";
5
+
import { TextInput } from "../text-input.jsx";
6
+
import { editorInstance } from "./state";
7
+
8
+
export const HandleInput = (props: { onClose: () => void }) => {
9
+
const [resolving, setResolving] = createSignal(false);
10
+
const [error, setError] = createSignal("");
11
+
let handleFormRef!: HTMLFormElement;
12
+
13
+
const resolveDid = async (e: SubmitEvent) => {
14
+
e.preventDefault();
15
+
const formData = new FormData(handleFormRef);
16
+
const handleValue = formData.get("handle")?.toString().trim();
17
+
18
+
if (!handleValue) {
19
+
setError("Please enter a handle");
20
+
return;
21
+
}
22
+
23
+
setResolving(true);
24
+
setError("");
25
+
try {
26
+
const did = await resolveHandle(handleValue as Handle);
27
+
editorInstance.view.dispatch({
28
+
changes: {
29
+
from: editorInstance.view.state.selection.main.head,
30
+
insert: `"${did}"`,
31
+
},
32
+
});
33
+
props.onClose();
34
+
handleFormRef.reset();
35
+
} catch (err: any) {
36
+
setError(err.message || "Failed to resolve handle");
37
+
} finally {
38
+
setResolving(false);
39
+
}
40
+
};
41
+
42
+
return (
43
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
44
+
<h2 class="mb-2 font-semibold">Insert DID from handle</h2>
45
+
<form ref={handleFormRef} onSubmit={resolveDid} class="flex flex-col gap-2 text-sm">
46
+
<div class="flex flex-col gap-1">
47
+
<label for="handle-input" class="select-none">
48
+
Handle
49
+
</label>
50
+
<TextInput id="handle-input" name="handle" placeholder="user.bsky.social" />
51
+
</div>
52
+
<p class="text-xs text-neutral-600 dark:text-neutral-400">
53
+
DID will be pasted after the cursor
54
+
</p>
55
+
<Show when={error()}>
56
+
<span class="text-red-500 dark:text-red-400">Error: {error()}</span>
57
+
</Show>
58
+
<div class="flex justify-between gap-2">
59
+
<Button
60
+
type="button"
61
+
onClick={() => {
62
+
props.onClose();
63
+
handleFormRef.reset();
64
+
setError("");
65
+
}}
66
+
>
67
+
Cancel
68
+
</Button>
69
+
<Show when={resolving()}>
70
+
<div class="flex items-center gap-1">
71
+
<span class="iconify lucide--loader-circle animate-spin"></span>
72
+
<span>Resolving</span>
73
+
</div>
74
+
</Show>
75
+
<Show when={!resolving()}>
76
+
<Button
77
+
type="submit"
78
+
class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400"
79
+
>
80
+
Insert
81
+
</Button>
82
+
</Show>
83
+
</div>
84
+
</form>
85
+
</div>
86
+
);
87
+
};
+479
src/components/create/index.tsx
+479
src/components/create/index.tsx
···
···
1
+
import { Client } from "@atcute/client";
2
+
import { Did } from "@atcute/lexicons";
3
+
import { isNsid, isRecordKey } from "@atcute/lexicons/syntax";
4
+
import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client";
5
+
import { useNavigate, useParams } from "@solidjs/router";
6
+
import {
7
+
createEffect,
8
+
createSignal,
9
+
For,
10
+
lazy,
11
+
onCleanup,
12
+
onMount,
13
+
Show,
14
+
Suspense,
15
+
} from "solid-js";
16
+
import { hasUserScope } from "../../auth/scope-utils";
17
+
import { agent, sessions } from "../../auth/state";
18
+
import { Button } from "../button.jsx";
19
+
import { Modal } from "../modal.jsx";
20
+
import { addNotification, removeNotification } from "../notification.jsx";
21
+
import { TextInput } from "../text-input.jsx";
22
+
import Tooltip from "../tooltip.jsx";
23
+
import { FileUpload } from "./file-upload";
24
+
import { HandleInput } from "./handle-input";
25
+
import { MenuItem } from "./menu-item";
26
+
import { editorInstance, placeholder, setPlaceholder } from "./state";
27
+
28
+
const Editor = lazy(() => import("../editor.jsx").then((m) => ({ default: m.Editor })));
29
+
30
+
export { editorInstance, placeholder, setPlaceholder };
31
+
32
+
export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => {
33
+
const navigate = useNavigate();
34
+
const params = useParams();
35
+
const [openDialog, setOpenDialog] = createSignal(false);
36
+
const [notice, setNotice] = createSignal("");
37
+
const [openUpload, setOpenUpload] = createSignal(false);
38
+
const [openInsertMenu, setOpenInsertMenu] = createSignal(false);
39
+
const [openHandleDialog, setOpenHandleDialog] = createSignal(false);
40
+
const [validate, setValidate] = createSignal<boolean | undefined>(undefined);
41
+
const [isMaximized, setIsMaximized] = createSignal(false);
42
+
const [isMinimized, setIsMinimized] = createSignal(false);
43
+
const [collectionError, setCollectionError] = createSignal("");
44
+
const [rkeyError, setRkeyError] = createSignal("");
45
+
let blobInput!: HTMLInputElement;
46
+
let formRef!: HTMLFormElement;
47
+
let insertMenuRef!: HTMLDivElement;
48
+
49
+
createEffect(() => {
50
+
if (openInsertMenu()) {
51
+
const handleClickOutside = (e: MouseEvent) => {
52
+
if (insertMenuRef && !insertMenuRef.contains(e.target as Node)) {
53
+
setOpenInsertMenu(false);
54
+
}
55
+
};
56
+
document.addEventListener("mousedown", handleClickOutside);
57
+
onCleanup(() => document.removeEventListener("mousedown", handleClickOutside));
58
+
}
59
+
});
60
+
61
+
onMount(() => {
62
+
const keyEvent = (ev: KeyboardEvent) => {
63
+
if (ev.target instanceof HTMLInputElement || ev.target instanceof HTMLTextAreaElement) return;
64
+
if ((ev.target as HTMLElement).closest("[data-modal]")) return;
65
+
66
+
const key = props.create ? "n" : "e";
67
+
if (ev.key === key) {
68
+
ev.preventDefault();
69
+
70
+
if (openDialog() && isMinimized()) {
71
+
setIsMinimized(false);
72
+
} else if (!openDialog() && !document.querySelector("[data-modal]")) {
73
+
setOpenDialog(true);
74
+
}
75
+
}
76
+
};
77
+
78
+
window.addEventListener("keydown", keyEvent);
79
+
onCleanup(() => window.removeEventListener("keydown", keyEvent));
80
+
});
81
+
82
+
const defaultPlaceholder = () => {
83
+
return {
84
+
$type: "app.bsky.feed.post",
85
+
text: "This post was sent from PDSls",
86
+
embed: {
87
+
$type: "app.bsky.embed.external",
88
+
external: {
89
+
uri: "https://pdsls.dev",
90
+
title: "PDSls",
91
+
description: "Browse the public data on atproto",
92
+
},
93
+
},
94
+
langs: ["en"],
95
+
createdAt: new Date().toISOString(),
96
+
};
97
+
};
98
+
99
+
const getValidateIcon = () => {
100
+
return (
101
+
validate() === true ? "lucide--circle-check"
102
+
: validate() === false ? "lucide--circle-x"
103
+
: "lucide--circle"
104
+
);
105
+
};
106
+
107
+
const getValidateLabel = () => {
108
+
return (
109
+
validate() === true ? "True"
110
+
: validate() === false ? "False"
111
+
: "Unset"
112
+
);
113
+
};
114
+
115
+
createEffect(() => {
116
+
if (openDialog()) {
117
+
setValidate(undefined);
118
+
setCollectionError("");
119
+
setRkeyError("");
120
+
}
121
+
});
122
+
123
+
const createRecord = async (formData: FormData) => {
124
+
const repo = formData.get("repo")?.toString();
125
+
if (!repo) return;
126
+
const rpc = new Client({ handler: new OAuthUserAgent(await getSession(repo as Did)) });
127
+
const collection = formData.get("collection");
128
+
const rkey = formData.get("rkey");
129
+
let record: any;
130
+
try {
131
+
record = JSON.parse(editorInstance.view.state.doc.toString());
132
+
} catch (e: any) {
133
+
setNotice(e.message);
134
+
return;
135
+
}
136
+
const res = await rpc.post("com.atproto.repo.createRecord", {
137
+
input: {
138
+
repo: repo as Did,
139
+
collection: collection ? collection.toString() : record.$type,
140
+
rkey: rkey?.toString().length ? rkey?.toString() : undefined,
141
+
record: record,
142
+
validate: validate(),
143
+
},
144
+
});
145
+
if (!res.ok) {
146
+
setNotice(`${res.data.error}: ${res.data.message}`);
147
+
return;
148
+
}
149
+
setOpenDialog(false);
150
+
const id = addNotification({
151
+
message: "Record created",
152
+
type: "success",
153
+
});
154
+
setTimeout(() => removeNotification(id), 3000);
155
+
navigate(`/${res.data.uri}`);
156
+
};
157
+
158
+
const editRecord = async (recreate?: boolean) => {
159
+
const record = editorInstance.view.state.doc.toString();
160
+
if (!record) return;
161
+
const rpc = new Client({ handler: agent()! });
162
+
try {
163
+
const editedRecord = JSON.parse(record);
164
+
if (recreate) {
165
+
const res = await rpc.post("com.atproto.repo.applyWrites", {
166
+
input: {
167
+
repo: agent()!.sub,
168
+
validate: validate(),
169
+
writes: [
170
+
{
171
+
collection: params.collection as `${string}.${string}.${string}`,
172
+
rkey: params.rkey!,
173
+
$type: "com.atproto.repo.applyWrites#delete",
174
+
},
175
+
{
176
+
collection: params.collection as `${string}.${string}.${string}`,
177
+
rkey: params.rkey,
178
+
$type: "com.atproto.repo.applyWrites#create",
179
+
value: editedRecord,
180
+
},
181
+
],
182
+
},
183
+
});
184
+
if (!res.ok) {
185
+
setNotice(`${res.data.error}: ${res.data.message}`);
186
+
return;
187
+
}
188
+
} else {
189
+
const res = await rpc.post("com.atproto.repo.applyWrites", {
190
+
input: {
191
+
repo: agent()!.sub,
192
+
validate: validate(),
193
+
writes: [
194
+
{
195
+
collection: params.collection as `${string}.${string}.${string}`,
196
+
rkey: params.rkey!,
197
+
$type: "com.atproto.repo.applyWrites#update",
198
+
value: editedRecord,
199
+
},
200
+
],
201
+
},
202
+
});
203
+
if (!res.ok) {
204
+
setNotice(`${res.data.error}: ${res.data.message}`);
205
+
return;
206
+
}
207
+
}
208
+
setOpenDialog(false);
209
+
const id = addNotification({
210
+
message: "Record edited",
211
+
type: "success",
212
+
});
213
+
setTimeout(() => removeNotification(id), 3000);
214
+
props.refetch();
215
+
} catch (err: any) {
216
+
setNotice(err.message);
217
+
}
218
+
};
219
+
220
+
const insertTimestamp = () => {
221
+
const timestamp = new Date().toISOString();
222
+
editorInstance.view.dispatch({
223
+
changes: {
224
+
from: editorInstance.view.state.selection.main.head,
225
+
insert: `"${timestamp}"`,
226
+
},
227
+
});
228
+
setOpenInsertMenu(false);
229
+
};
230
+
231
+
const insertDidFromHandle = () => {
232
+
setOpenInsertMenu(false);
233
+
setOpenHandleDialog(true);
234
+
};
235
+
236
+
return (
237
+
<>
238
+
<Modal
239
+
open={openDialog()}
240
+
onClose={() => setOpenDialog(false)}
241
+
closeOnClick={false}
242
+
nonBlocking={isMinimized()}
243
+
>
244
+
<div
245
+
style="transform: translateX(-50%) translateZ(0);"
246
+
classList={{
247
+
"dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto absolute top-18 left-1/2 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-all duration-200 dark:border-neutral-700 starting:opacity-0": true,
248
+
"w-[calc(100%-1rem)] max-w-3xl h-[65vh]": !isMaximized(),
249
+
"w-[calc(100%-1rem)] max-w-7xl h-[85vh]": isMaximized(),
250
+
hidden: isMinimized(),
251
+
}}
252
+
>
253
+
<div class="mb-2 flex w-full justify-between text-base">
254
+
<div class="flex items-center gap-2">
255
+
<span class="font-semibold select-none">
256
+
{props.create ? "Creating" : "Editing"} record
257
+
</span>
258
+
</div>
259
+
<div class="flex items-center gap-1">
260
+
<button
261
+
type="button"
262
+
onclick={() => setIsMinimized(true)}
263
+
class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
264
+
>
265
+
<span class="iconify lucide--minus"></span>
266
+
</button>
267
+
<button
268
+
type="button"
269
+
onclick={() => setIsMaximized(!isMaximized())}
270
+
class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
271
+
>
272
+
<span
273
+
class={`iconify ${isMaximized() ? "lucide--minimize-2" : "lucide--maximize-2"}`}
274
+
></span>
275
+
</button>
276
+
<button
277
+
id="close"
278
+
onclick={() => setOpenDialog(false)}
279
+
class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
280
+
>
281
+
<span class="iconify lucide--x"></span>
282
+
</button>
283
+
</div>
284
+
</div>
285
+
<form ref={formRef} class="flex min-h-0 flex-1 flex-col gap-y-2">
286
+
<Show when={props.create}>
287
+
<div class="flex flex-wrap items-center gap-1 text-sm">
288
+
<span>at://</span>
289
+
<select
290
+
class="dark:bg-dark-100 max-w-40 truncate rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 select-none focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
291
+
name="repo"
292
+
id="repo"
293
+
>
294
+
<For each={Object.keys(sessions)}>
295
+
{(session) => (
296
+
<option value={session} selected={session === agent()?.sub}>
297
+
{sessions[session].handle ?? session}
298
+
</option>
299
+
)}
300
+
</For>
301
+
</select>
302
+
<span>/</span>
303
+
<TextInput
304
+
id="collection"
305
+
name="collection"
306
+
placeholder="Collection (default: $type)"
307
+
class={`w-40 placeholder:text-xs lg:w-52 ${collectionError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`}
308
+
onInput={(e) => {
309
+
const value = e.currentTarget.value;
310
+
if (!value || isNsid(value)) setCollectionError("");
311
+
else
312
+
setCollectionError(
313
+
"Invalid collection: use reverse domain format (e.g. app.bsky.feed.post)",
314
+
);
315
+
}}
316
+
/>
317
+
<span>/</span>
318
+
<TextInput
319
+
id="rkey"
320
+
name="rkey"
321
+
placeholder="Record key (default: TID)"
322
+
class={`w-40 placeholder:text-xs lg:w-52 ${rkeyError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`}
323
+
onInput={(e) => {
324
+
const value = e.currentTarget.value;
325
+
if (!value || isRecordKey(value)) setRkeyError("");
326
+
else setRkeyError("Invalid record key: 1-512 chars, use a-z A-Z 0-9 . _ ~ : -");
327
+
}}
328
+
/>
329
+
</div>
330
+
<Show when={collectionError() || rkeyError()}>
331
+
<div class="text-xs text-red-500 dark:text-red-400">
332
+
<div>{collectionError()}</div>
333
+
<div>{rkeyError()}</div>
334
+
</div>
335
+
</Show>
336
+
</Show>
337
+
<div class="min-h-0 flex-1">
338
+
<Suspense
339
+
fallback={
340
+
<div class="flex h-full items-center justify-center">
341
+
<span class="iconify lucide--loader-circle animate-spin text-xl"></span>
342
+
</div>
343
+
}
344
+
>
345
+
<Editor
346
+
content={JSON.stringify(
347
+
!props.create ? props.record
348
+
: params.rkey ? placeholder()
349
+
: defaultPlaceholder(),
350
+
null,
351
+
2,
352
+
)}
353
+
/>
354
+
</Suspense>
355
+
</div>
356
+
<div class="flex flex-col gap-2">
357
+
<Show when={notice()}>
358
+
<div class="text-sm text-red-500 dark:text-red-400">{notice()}</div>
359
+
</Show>
360
+
<div class="flex justify-between gap-2">
361
+
<div class="relative" ref={insertMenuRef}>
362
+
<button
363
+
type="button"
364
+
class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 flex w-fit rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 text-base shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
365
+
onClick={() => setOpenInsertMenu(!openInsertMenu())}
366
+
>
367
+
<span class="iconify lucide--plus select-none"></span>
368
+
</button>
369
+
<Show when={openInsertMenu()}>
370
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute bottom-full left-0 z-10 mb-1 flex w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 shadow-md dark:border-neutral-700">
371
+
<MenuItem
372
+
icon="lucide--id-card"
373
+
label="Insert DID"
374
+
onClick={insertDidFromHandle}
375
+
/>
376
+
<MenuItem
377
+
icon="lucide--clock"
378
+
label="Insert timestamp"
379
+
onClick={insertTimestamp}
380
+
/>
381
+
<Show when={hasUserScope("blob")}>
382
+
<MenuItem
383
+
icon="lucide--upload"
384
+
label="Upload blob"
385
+
onClick={() => {
386
+
setOpenInsertMenu(false);
387
+
blobInput.click();
388
+
}}
389
+
/>
390
+
</Show>
391
+
</div>
392
+
</Show>
393
+
<input
394
+
type="file"
395
+
id="blob"
396
+
class="sr-only"
397
+
ref={blobInput}
398
+
onChange={(e) => {
399
+
if (e.target.files !== null) setOpenUpload(true);
400
+
}}
401
+
/>
402
+
</div>
403
+
<Modal
404
+
open={openUpload()}
405
+
onClose={() => setOpenUpload(false)}
406
+
closeOnClick={false}
407
+
>
408
+
<FileUpload
409
+
file={blobInput.files![0]}
410
+
blobInput={blobInput}
411
+
onClose={() => setOpenUpload(false)}
412
+
/>
413
+
</Modal>
414
+
<Modal
415
+
open={openHandleDialog()}
416
+
onClose={() => setOpenHandleDialog(false)}
417
+
closeOnClick={false}
418
+
>
419
+
<HandleInput onClose={() => setOpenHandleDialog(false)} />
420
+
</Modal>
421
+
<div class="flex items-center justify-end gap-2">
422
+
<button
423
+
type="button"
424
+
class="flex items-center gap-1 rounded-sm p-1.5 text-xs hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
425
+
onClick={() =>
426
+
setValidate(
427
+
validate() === true ? false
428
+
: validate() === false ? undefined
429
+
: true,
430
+
)
431
+
}
432
+
>
433
+
<Tooltip text={getValidateLabel()}>
434
+
<span class={`iconify ${getValidateIcon()}`}></span>
435
+
</Tooltip>
436
+
<span>Validate</span>
437
+
</button>
438
+
<Show when={!props.create && hasUserScope("create") && hasUserScope("delete")}>
439
+
<Button onClick={() => editRecord(true)}>Recreate</Button>
440
+
</Show>
441
+
<Button
442
+
onClick={() =>
443
+
props.create ? createRecord(new FormData(formRef)) : editRecord()
444
+
}
445
+
>
446
+
{props.create ? "Create" : "Edit"}
447
+
</Button>
448
+
</div>
449
+
</div>
450
+
</div>
451
+
</form>
452
+
</div>
453
+
</Modal>
454
+
<Show when={isMinimized() && openDialog()}>
455
+
<button
456
+
class="dark:bg-dark-300 dark:hover:bg-dark-200 dark:active:bg-dark-100 fixed right-4 bottom-4 z-30 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-3 py-2 shadow-md hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700"
457
+
onclick={() => setIsMinimized(false)}
458
+
>
459
+
<span class="iconify lucide--square-pen text-lg"></span>
460
+
<span class="text-sm font-medium">{props.create ? "Creating" : "Editing"} record</span>
461
+
</button>
462
+
</Show>
463
+
<Tooltip text={props.create ? "Create record (n)" : "Edit record (e)"}>
464
+
<button
465
+
class={`flex items-center p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 ${props.create ? "rounded-lg" : "rounded-sm"}`}
466
+
onclick={() => {
467
+
setNotice("");
468
+
setOpenDialog(true);
469
+
setIsMinimized(false);
470
+
}}
471
+
>
472
+
<div
473
+
class={props.create ? "iconify lucide--square-pen text-lg" : "iconify lucide--pencil"}
474
+
/>
475
+
</button>
476
+
</Tooltip>
477
+
</>
478
+
);
479
+
};
+4
src/components/create/state.ts
+4
src/components/create/state.ts
-519
src/components/create.tsx
-519
src/components/create.tsx
···
1
-
import { Client } from "@atcute/client";
2
-
import { Did } from "@atcute/lexicons";
3
-
import { isNsid, isRecordKey } from "@atcute/lexicons/syntax";
4
-
import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client";
5
-
import { remove } from "@mary/exif-rm";
6
-
import { useNavigate, useParams } from "@solidjs/router";
7
-
import { createEffect, createSignal, For, onCleanup, Show } from "solid-js";
8
-
import { Editor, editorView } from "../components/editor.jsx";
9
-
import { agent } from "../components/login.jsx";
10
-
import { sessions } from "./account.jsx";
11
-
import { Button } from "./button.jsx";
12
-
import { Modal } from "./modal.jsx";
13
-
import { addNotification, removeNotification } from "./notification.jsx";
14
-
import { TextInput } from "./text-input.jsx";
15
-
import Tooltip from "./tooltip.jsx";
16
-
17
-
export const [placeholder, setPlaceholder] = createSignal<any>();
18
-
19
-
export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => {
20
-
const navigate = useNavigate();
21
-
const params = useParams();
22
-
const [openDialog, setOpenDialog] = createSignal(false);
23
-
const [notice, setNotice] = createSignal("");
24
-
const [openUpload, setOpenUpload] = createSignal(false);
25
-
const [openInsertMenu, setOpenInsertMenu] = createSignal(false);
26
-
const [validate, setValidate] = createSignal<boolean | undefined>(undefined);
27
-
const [isMaximized, setIsMaximized] = createSignal(false);
28
-
const [isMinimized, setIsMinimized] = createSignal(false);
29
-
const [collectionError, setCollectionError] = createSignal("");
30
-
const [rkeyError, setRkeyError] = createSignal("");
31
-
let blobInput!: HTMLInputElement;
32
-
let formRef!: HTMLFormElement;
33
-
let insertMenuRef!: HTMLDivElement;
34
-
35
-
createEffect(() => {
36
-
if (openInsertMenu()) {
37
-
const handleClickOutside = (e: MouseEvent) => {
38
-
if (insertMenuRef && !insertMenuRef.contains(e.target as Node)) {
39
-
setOpenInsertMenu(false);
40
-
}
41
-
};
42
-
document.addEventListener("mousedown", handleClickOutside);
43
-
onCleanup(() => document.removeEventListener("mousedown", handleClickOutside));
44
-
}
45
-
});
46
-
47
-
const defaultPlaceholder = () => {
48
-
return {
49
-
$type: "app.bsky.feed.post",
50
-
text: "This post was sent from PDSls",
51
-
embed: {
52
-
$type: "app.bsky.embed.external",
53
-
external: {
54
-
uri: "https://pdsls.dev",
55
-
title: "PDSls",
56
-
description: "Browse the public data on atproto",
57
-
},
58
-
},
59
-
langs: ["en"],
60
-
createdAt: new Date().toISOString(),
61
-
};
62
-
};
63
-
64
-
const getValidateIcon = () => {
65
-
return (
66
-
validate() === true ? "lucide--circle-check"
67
-
: validate() === false ? "lucide--circle-x"
68
-
: "lucide--circle"
69
-
);
70
-
};
71
-
72
-
const getValidateLabel = () => {
73
-
return (
74
-
validate() === true ? "True"
75
-
: validate() === false ? "False"
76
-
: "Unset"
77
-
);
78
-
};
79
-
80
-
createEffect(() => {
81
-
if (openDialog()) {
82
-
setValidate(undefined);
83
-
setCollectionError("");
84
-
setRkeyError("");
85
-
}
86
-
});
87
-
88
-
const createRecord = async (formData: FormData) => {
89
-
const repo = formData.get("repo")?.toString();
90
-
if (!repo) return;
91
-
const rpc = new Client({ handler: new OAuthUserAgent(await getSession(repo as Did)) });
92
-
const collection = formData.get("collection");
93
-
const rkey = formData.get("rkey");
94
-
let record: any;
95
-
try {
96
-
record = JSON.parse(editorView.state.doc.toString());
97
-
} catch (e: any) {
98
-
setNotice(e.message);
99
-
return;
100
-
}
101
-
const res = await rpc.post("com.atproto.repo.createRecord", {
102
-
input: {
103
-
repo: repo as Did,
104
-
collection: collection ? collection.toString() : record.$type,
105
-
rkey: rkey?.toString().length ? rkey?.toString() : undefined,
106
-
record: record,
107
-
validate: validate(),
108
-
},
109
-
});
110
-
if (!res.ok) {
111
-
setNotice(`${res.data.error}: ${res.data.message}`);
112
-
return;
113
-
}
114
-
setOpenDialog(false);
115
-
const id = addNotification({
116
-
message: "Record created",
117
-
type: "success",
118
-
});
119
-
setTimeout(() => removeNotification(id), 3000);
120
-
navigate(`/${res.data.uri}`);
121
-
};
122
-
123
-
const editRecord = async (recreate?: boolean) => {
124
-
const record = editorView.state.doc.toString();
125
-
if (!record) return;
126
-
const rpc = new Client({ handler: agent()! });
127
-
try {
128
-
const editedRecord = JSON.parse(record);
129
-
if (recreate) {
130
-
const res = await rpc.post("com.atproto.repo.applyWrites", {
131
-
input: {
132
-
repo: agent()!.sub,
133
-
validate: validate(),
134
-
writes: [
135
-
{
136
-
collection: params.collection as `${string}.${string}.${string}`,
137
-
rkey: params.rkey!,
138
-
$type: "com.atproto.repo.applyWrites#delete",
139
-
},
140
-
{
141
-
collection: params.collection as `${string}.${string}.${string}`,
142
-
rkey: params.rkey,
143
-
$type: "com.atproto.repo.applyWrites#create",
144
-
value: editedRecord,
145
-
},
146
-
],
147
-
},
148
-
});
149
-
if (!res.ok) {
150
-
setNotice(`${res.data.error}: ${res.data.message}`);
151
-
return;
152
-
}
153
-
} else {
154
-
const res = await rpc.post("com.atproto.repo.putRecord", {
155
-
input: {
156
-
repo: agent()!.sub,
157
-
collection: params.collection as `${string}.${string}.${string}`,
158
-
rkey: params.rkey!,
159
-
record: editedRecord,
160
-
validate: validate(),
161
-
},
162
-
});
163
-
if (!res.ok) {
164
-
setNotice(`${res.data.error}: ${res.data.message}`);
165
-
return;
166
-
}
167
-
}
168
-
setOpenDialog(false);
169
-
const id = addNotification({
170
-
message: "Record edited",
171
-
type: "success",
172
-
});
173
-
setTimeout(() => removeNotification(id), 3000);
174
-
props.refetch();
175
-
} catch (err: any) {
176
-
setNotice(err.message);
177
-
}
178
-
};
179
-
180
-
const insertTimestamp = () => {
181
-
const timestamp = new Date().toISOString();
182
-
editorView.dispatch({
183
-
changes: {
184
-
from: editorView.state.selection.main.head,
185
-
insert: `"${timestamp}"`,
186
-
},
187
-
});
188
-
setOpenInsertMenu(false);
189
-
};
190
-
191
-
const MenuItem = (props: { icon: string; label: string; onClick: () => void }) => {
192
-
return (
193
-
<button
194
-
type="button"
195
-
class="flex items-center gap-2 rounded-md p-2 text-left text-xs hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
196
-
onClick={props.onClick}
197
-
>
198
-
<span class={`iconify ${props.icon}`}></span>
199
-
<span>{props.label}</span>
200
-
</button>
201
-
);
202
-
};
203
-
204
-
const FileUpload = (props: { file: File }) => {
205
-
const [uploading, setUploading] = createSignal(false);
206
-
const [error, setError] = createSignal("");
207
-
208
-
onCleanup(() => (blobInput.value = ""));
209
-
210
-
const formatFileSize = (bytes: number) => {
211
-
if (bytes === 0) return "0 Bytes";
212
-
const k = 1024;
213
-
const sizes = ["Bytes", "KB", "MB", "GB"];
214
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
215
-
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
216
-
};
217
-
218
-
const uploadBlob = async () => {
219
-
let blob: Blob;
220
-
221
-
const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value;
222
-
(document.getElementById("mimetype") as HTMLInputElement).value = "";
223
-
if (mimetype) blob = new Blob([props.file], { type: mimetype });
224
-
else blob = props.file;
225
-
226
-
if ((document.getElementById("exif-rm") as HTMLInputElement).checked) {
227
-
const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer()));
228
-
if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type });
229
-
}
230
-
231
-
const rpc = new Client({ handler: agent()! });
232
-
setUploading(true);
233
-
const res = await rpc.post("com.atproto.repo.uploadBlob", {
234
-
input: blob,
235
-
});
236
-
setUploading(false);
237
-
if (!res.ok) {
238
-
setError(res.data.error);
239
-
return;
240
-
}
241
-
editorView.dispatch({
242
-
changes: {
243
-
from: editorView.state.selection.main.head,
244
-
insert: JSON.stringify(res.data.blob, null, 2),
245
-
},
246
-
});
247
-
setOpenUpload(false);
248
-
};
249
-
250
-
return (
251
-
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
252
-
<h2 class="mb-2 font-semibold">Upload blob</h2>
253
-
<div class="flex flex-col gap-2 text-sm">
254
-
<div class="flex flex-col gap-1">
255
-
<p class="flex gap-1">
256
-
<span class="truncate">{props.file.name}</span>
257
-
<span class="shrink-0 text-neutral-600 dark:text-neutral-400">
258
-
({formatFileSize(props.file.size)})
259
-
</span>
260
-
</p>
261
-
</div>
262
-
<div class="flex items-center gap-x-2">
263
-
<label for="mimetype" class="shrink-0 select-none">
264
-
MIME type
265
-
</label>
266
-
<TextInput id="mimetype" placeholder={props.file.type} />
267
-
</div>
268
-
<div class="flex items-center gap-1">
269
-
<input id="exif-rm" type="checkbox" checked />
270
-
<label for="exif-rm" class="select-none">
271
-
Remove EXIF data
272
-
</label>
273
-
</div>
274
-
<p class="text-xs text-neutral-600 dark:text-neutral-400">
275
-
Metadata will be pasted after the cursor
276
-
</p>
277
-
<Show when={error()}>
278
-
<span class="text-red-500 dark:text-red-400">Error: {error()}</span>
279
-
</Show>
280
-
<div class="flex justify-between gap-2">
281
-
<Button onClick={() => setOpenUpload(false)}>Cancel</Button>
282
-
<Show when={uploading()}>
283
-
<div class="flex items-center gap-1">
284
-
<span class="iconify lucide--loader-circle animate-spin"></span>
285
-
<span>Uploading</span>
286
-
</div>
287
-
</Show>
288
-
<Show when={!uploading()}>
289
-
<Button
290
-
onClick={uploadBlob}
291
-
class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400"
292
-
>
293
-
Upload
294
-
</Button>
295
-
</Show>
296
-
</div>
297
-
</div>
298
-
</div>
299
-
);
300
-
};
301
-
302
-
return (
303
-
<>
304
-
<Modal
305
-
open={openDialog()}
306
-
onClose={() => setOpenDialog(false)}
307
-
closeOnClick={false}
308
-
nonBlocking={isMinimized()}
309
-
>
310
-
<div
311
-
style="transform: translateX(-50%) translateZ(0);"
312
-
classList={{
313
-
"dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto absolute top-18 left-1/2 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-all duration-200 dark:border-neutral-700 starting:opacity-0": true,
314
-
"w-[calc(100%-1rem)] max-w-3xl h-[65vh]": !isMaximized(),
315
-
"w-[calc(100%-1rem)] max-w-7xl h-[85vh]": isMaximized(),
316
-
hidden: isMinimized(),
317
-
}}
318
-
>
319
-
<div class="mb-2 flex w-full justify-between text-base">
320
-
<div class="flex items-center gap-2">
321
-
<span class="font-semibold select-none">
322
-
{props.create ? "Creating" : "Editing"} record
323
-
</span>
324
-
</div>
325
-
<div class="flex items-center gap-1">
326
-
<button
327
-
type="button"
328
-
onclick={() => setIsMinimized(true)}
329
-
class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
330
-
>
331
-
<span class="iconify lucide--minus"></span>
332
-
</button>
333
-
<button
334
-
type="button"
335
-
onclick={() => setIsMaximized(!isMaximized())}
336
-
class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
337
-
>
338
-
<span
339
-
class={`iconify ${isMaximized() ? "lucide--minimize-2" : "lucide--maximize-2"}`}
340
-
></span>
341
-
</button>
342
-
<button
343
-
id="close"
344
-
onclick={() => setOpenDialog(false)}
345
-
class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
346
-
>
347
-
<span class="iconify lucide--x"></span>
348
-
</button>
349
-
</div>
350
-
</div>
351
-
<form ref={formRef} class="flex min-h-0 flex-1 flex-col gap-y-2">
352
-
<Show when={props.create}>
353
-
<div class="flex flex-wrap items-center gap-1 text-sm">
354
-
<span>at://</span>
355
-
<select
356
-
class="dark:bg-dark-100 max-w-40 truncate rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 select-none focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400"
357
-
name="repo"
358
-
id="repo"
359
-
>
360
-
<For each={Object.keys(sessions)}>
361
-
{(session) => (
362
-
<option value={session} selected={session === agent()?.sub}>
363
-
{sessions[session].handle ?? session}
364
-
</option>
365
-
)}
366
-
</For>
367
-
</select>
368
-
<span>/</span>
369
-
<TextInput
370
-
id="collection"
371
-
name="collection"
372
-
placeholder="Collection (default: $type)"
373
-
class={`w-40 placeholder:text-xs lg:w-52 ${collectionError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`}
374
-
onInput={(e) => {
375
-
const value = e.currentTarget.value;
376
-
if (!value || isNsid(value)) setCollectionError("");
377
-
else
378
-
setCollectionError(
379
-
"Invalid collection: use reverse domain format (e.g. app.bsky.feed.post)",
380
-
);
381
-
}}
382
-
/>
383
-
<span>/</span>
384
-
<TextInput
385
-
id="rkey"
386
-
name="rkey"
387
-
placeholder="Record key (default: TID)"
388
-
class={`w-40 placeholder:text-xs lg:w-52 ${rkeyError() ? "border-red-500 focus:outline-red-500 dark:border-red-400 dark:focus:outline-red-400" : ""}`}
389
-
onInput={(e) => {
390
-
const value = e.currentTarget.value;
391
-
if (!value || isRecordKey(value)) setRkeyError("");
392
-
else setRkeyError("Invalid record key: 1-512 chars, use a-z A-Z 0-9 . _ ~ : -");
393
-
}}
394
-
/>
395
-
</div>
396
-
<Show when={collectionError() || rkeyError()}>
397
-
<div class="text-xs text-red-500 dark:text-red-400">
398
-
<div>{collectionError()}</div>
399
-
<div>{rkeyError()}</div>
400
-
</div>
401
-
</Show>
402
-
</Show>
403
-
<div class="min-h-0 flex-1">
404
-
<Editor
405
-
content={JSON.stringify(
406
-
!props.create ? props.record
407
-
: params.rkey ? placeholder()
408
-
: defaultPlaceholder(),
409
-
null,
410
-
2,
411
-
)}
412
-
/>
413
-
</div>
414
-
<div class="flex flex-col gap-2">
415
-
<Show when={notice()}>
416
-
<div class="text-sm text-red-500 dark:text-red-400">{notice()}</div>
417
-
</Show>
418
-
<div class="flex justify-between gap-2">
419
-
<div class="relative" ref={insertMenuRef}>
420
-
<button
421
-
type="button"
422
-
class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 flex w-fit rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 text-base shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
423
-
onClick={() => setOpenInsertMenu(!openInsertMenu())}
424
-
>
425
-
<span class="iconify lucide--plus select-none"></span>
426
-
</button>
427
-
<Show when={openInsertMenu()}>
428
-
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute bottom-full left-0 z-10 mb-1 flex w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-1.5 shadow-md dark:border-neutral-700">
429
-
<MenuItem
430
-
icon="lucide--upload"
431
-
label="Upload blob"
432
-
onClick={() => {
433
-
setOpenInsertMenu(false);
434
-
blobInput.click();
435
-
}}
436
-
/>
437
-
<MenuItem
438
-
icon="lucide--clock"
439
-
label="Insert timestamp"
440
-
onClick={insertTimestamp}
441
-
/>
442
-
</div>
443
-
</Show>
444
-
<input
445
-
type="file"
446
-
id="blob"
447
-
class="sr-only"
448
-
ref={blobInput}
449
-
onChange={(e) => {
450
-
if (e.target.files !== null) setOpenUpload(true);
451
-
}}
452
-
/>
453
-
</div>
454
-
<Modal
455
-
open={openUpload()}
456
-
onClose={() => setOpenUpload(false)}
457
-
closeOnClick={false}
458
-
>
459
-
<FileUpload file={blobInput.files![0]} />
460
-
</Modal>
461
-
<div class="flex items-center justify-end gap-2">
462
-
<button
463
-
type="button"
464
-
class="flex items-center gap-1 rounded-sm p-1.5 text-xs hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
465
-
onClick={() =>
466
-
setValidate(
467
-
validate() === true ? false
468
-
: validate() === false ? undefined
469
-
: true,
470
-
)
471
-
}
472
-
>
473
-
<Tooltip text={getValidateLabel()}>
474
-
<span class={`iconify ${getValidateIcon()}`}></span>
475
-
</Tooltip>
476
-
<span>Validate</span>
477
-
</button>
478
-
<Show when={!props.create}>
479
-
<Button onClick={() => editRecord(true)}>Recreate</Button>
480
-
</Show>
481
-
<Button
482
-
onClick={() =>
483
-
props.create ? createRecord(new FormData(formRef)) : editRecord()
484
-
}
485
-
>
486
-
{props.create ? "Create" : "Edit"}
487
-
</Button>
488
-
</div>
489
-
</div>
490
-
</div>
491
-
</form>
492
-
</div>
493
-
</Modal>
494
-
<Show when={isMinimized() && openDialog()}>
495
-
<button
496
-
class="dark:bg-dark-300 dark:hover:bg-dark-200 dark:active:bg-dark-100 fixed right-4 bottom-4 z-30 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-3 py-2 shadow-md hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700"
497
-
onclick={() => setIsMinimized(false)}
498
-
>
499
-
<span class="iconify lucide--square-pen text-lg"></span>
500
-
<span class="text-sm font-medium">{props.create ? "Creating" : "Editing"} record</span>
501
-
</button>
502
-
</Show>
503
-
<Tooltip text={`${props.create ? "Create" : "Edit"} record`}>
504
-
<button
505
-
class={`flex items-center p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 ${props.create ? "rounded-lg" : "rounded-sm"}`}
506
-
onclick={() => {
507
-
setNotice("");
508
-
setOpenDialog(true);
509
-
setIsMinimized(false);
510
-
}}
511
-
>
512
-
<div
513
-
class={props.create ? "iconify lucide--square-pen text-lg" : "iconify lucide--pencil"}
514
-
/>
515
-
</button>
516
-
</Tooltip>
517
-
</>
518
-
);
519
-
};
···
+41
-15
src/components/dropdown.tsx
+41
-15
src/components/dropdown.tsx
···
10
Show,
11
useContext,
12
} from "solid-js";
13
import { addToClipboard } from "../utils/copy";
14
15
const MenuContext = createContext<{
···
33
addToClipboard(props.content);
34
ctx?.setShowMenu(false);
35
}}
36
-
class="flex items-center gap-1.5 rounded-md p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
37
>
38
<Show when={props.icon}>
39
<span class={"iconify shrink-0 " + props.icon}></span>
···
56
<A
57
href={props.href}
58
onClick={() => ctx?.setShowMenu(false)}
59
-
class="flex items-center gap-1.5 rounded-md p-1 hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
60
classList={{ "justify-between": props.external }}
61
target={props.newTab ? "_blank" : undefined}
62
>
···
79
return (
80
<button
81
onClick={props.onClick}
82
-
class="flex items-center gap-1.5 rounded-md p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
83
>
84
<Show when={props.icon}>
85
<span class={"iconify shrink-0 " + props.icon}></span>
···
102
const ctx = useContext(MenuContext);
103
const [menu, setMenu] = createSignal<HTMLDivElement>();
104
const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>();
105
106
const clickEvent = (event: MouseEvent) => {
107
const target = event.target as Node;
108
if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false);
109
};
110
111
-
onMount(() => window.addEventListener("click", clickEvent));
112
-
onCleanup(() => window.removeEventListener("click", clickEvent));
113
114
return (
115
<div class="relative">
···
119
props.buttonClass
120
}
121
ref={setMenuButton}
122
-
onClick={() => ctx?.setShowMenu(!ctx?.showMenu())}
123
>
124
<span class={"iconify " + props.icon}></span>
125
</button>
126
<Show when={ctx?.showMenu()}>
127
-
<div
128
-
ref={setMenu}
129
-
class={
130
-
"dark:bg-dark-300 dark:shadow-dark-700 absolute right-0 z-40 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 shadow-md dark:border-neutral-700 " +
131
-
props.menuClass
132
-
}
133
-
>
134
-
{props.children}
135
-
</div>
136
</Show>
137
</div>
138
);
···
10
Show,
11
useContext,
12
} from "solid-js";
13
+
import { Portal } from "solid-js/web";
14
import { addToClipboard } from "../utils/copy";
15
16
const MenuContext = createContext<{
···
34
addToClipboard(props.content);
35
ctx?.setShowMenu(false);
36
}}
37
+
class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
38
>
39
<Show when={props.icon}>
40
<span class={"iconify shrink-0 " + props.icon}></span>
···
57
<A
58
href={props.href}
59
onClick={() => ctx?.setShowMenu(false)}
60
+
class="flex items-center gap-2 rounded-md p-1.5 hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
61
classList={{ "justify-between": props.external }}
62
target={props.newTab ? "_blank" : undefined}
63
>
···
80
return (
81
<button
82
onClick={props.onClick}
83
+
class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
84
>
85
<Show when={props.icon}>
86
<span class={"iconify shrink-0 " + props.icon}></span>
···
103
const ctx = useContext(MenuContext);
104
const [menu, setMenu] = createSignal<HTMLDivElement>();
105
const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>();
106
+
const [buttonRect, setButtonRect] = createSignal<DOMRect>();
107
108
const clickEvent = (event: MouseEvent) => {
109
const target = event.target as Node;
110
if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false);
111
};
112
113
+
const updatePosition = () => {
114
+
const rect = menuButton()?.getBoundingClientRect();
115
+
if (rect) setButtonRect(rect);
116
+
};
117
+
118
+
onMount(() => {
119
+
window.addEventListener("click", clickEvent);
120
+
window.addEventListener("scroll", updatePosition, true);
121
+
window.addEventListener("resize", updatePosition);
122
+
});
123
+
124
+
onCleanup(() => {
125
+
window.removeEventListener("click", clickEvent);
126
+
window.removeEventListener("scroll", updatePosition, true);
127
+
window.removeEventListener("resize", updatePosition);
128
+
});
129
130
return (
131
<div class="relative">
···
135
props.buttonClass
136
}
137
ref={setMenuButton}
138
+
onClick={() => {
139
+
updatePosition();
140
+
ctx?.setShowMenu(!ctx?.showMenu());
141
+
}}
142
>
143
<span class={"iconify " + props.icon}></span>
144
</button>
145
<Show when={ctx?.showMenu()}>
146
+
<Portal>
147
+
<div
148
+
ref={setMenu}
149
+
style={{
150
+
position: "fixed",
151
+
top: `${(buttonRect()?.bottom ?? 0) + 4}px`,
152
+
left: `${(buttonRect()?.right ?? 0) - 160}px`,
153
+
}}
154
+
class={
155
+
"dark:bg-dark-300 dark:shadow-dark-700 z-50 flex min-w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-md dark:border-neutral-700 " +
156
+
props.menuClass
157
+
}
158
+
>
159
+
{props.children}
160
+
</div>
161
+
</Portal>
162
</Show>
163
</div>
164
);
+5
-4
src/components/editor.tsx
+5
-4
src/components/editor.tsx
···
7
import { basicLight } from "@fsegurai/codemirror-theme-basic-light";
8
import { basicSetup, EditorView } from "codemirror";
9
import { onCleanup, onMount } from "solid-js";
10
-
11
-
export let editorView: EditorView;
12
13
const Editor = (props: { content: string }) => {
14
let editorDiv!: HTMLDivElement;
15
let themeColor = new Compartment();
16
17
const themeEvent = () => {
18
-
editorView.dispatch({
19
effects: themeColor.reconfigure(
20
window.matchMedia("(prefers-color-scheme: dark)").matches ? basicDark : basicLight,
21
),
···
38
39
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent);
40
41
-
editorView = new EditorView({
42
doc: props.content,
43
parent: editorDiv,
44
extensions: [
···
50
themeColor.of(document.documentElement.classList.contains("dark") ? basicDark : basicLight),
51
],
52
});
53
});
54
55
onCleanup(() =>
···
7
import { basicLight } from "@fsegurai/codemirror-theme-basic-light";
8
import { basicSetup, EditorView } from "codemirror";
9
import { onCleanup, onMount } from "solid-js";
10
+
import { editorInstance } from "./create/state";
11
12
const Editor = (props: { content: string }) => {
13
let editorDiv!: HTMLDivElement;
14
let themeColor = new Compartment();
15
+
let view: EditorView;
16
17
const themeEvent = () => {
18
+
view.dispatch({
19
effects: themeColor.reconfigure(
20
window.matchMedia("(prefers-color-scheme: dark)").matches ? basicDark : basicLight,
21
),
···
38
39
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent);
40
41
+
view = new EditorView({
42
doc: props.content,
43
parent: editorDiv,
44
extensions: [
···
50
themeColor.of(document.documentElement.classList.contains("dark") ? basicDark : basicLight),
51
],
52
});
53
+
editorInstance.view = view;
54
});
55
56
onCleanup(() =>
-143
src/components/login.tsx
-143
src/components/login.tsx
···
1
-
import { Client } from "@atcute/client";
2
-
import { Did } from "@atcute/lexicons";
3
-
import { isDid, isHandle } from "@atcute/lexicons/syntax";
4
-
import {
5
-
configureOAuth,
6
-
createAuthorizationUrl,
7
-
defaultIdentityResolver,
8
-
finalizeAuthorization,
9
-
getSession,
10
-
OAuthUserAgent,
11
-
type Session,
12
-
} from "@atcute/oauth-browser-client";
13
-
import { createSignal, Show } from "solid-js";
14
-
import { didDocumentResolver, handleResolver } from "../utils/api";
15
-
16
-
configureOAuth({
17
-
metadata: {
18
-
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
19
-
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
20
-
},
21
-
identityResolver: defaultIdentityResolver({
22
-
handleResolver: handleResolver,
23
-
didDocumentResolver: didDocumentResolver,
24
-
}),
25
-
});
26
-
27
-
export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>();
28
-
29
-
type Account = {
30
-
signedIn: boolean;
31
-
handle?: string;
32
-
};
33
-
34
-
export type Sessions = Record<string, Account>;
35
-
36
-
const Login = () => {
37
-
const [notice, setNotice] = createSignal("");
38
-
const [loginInput, setLoginInput] = createSignal("");
39
-
40
-
const login = async (handle: string) => {
41
-
try {
42
-
setNotice("");
43
-
if (!handle) return;
44
-
setNotice(`Contacting your data server...`);
45
-
const authUrl = await createAuthorizationUrl({
46
-
scope: import.meta.env.VITE_OAUTH_SCOPE,
47
-
target:
48
-
isHandle(handle) || isDid(handle) ?
49
-
{ type: "account", identifier: handle }
50
-
: { type: "pds", serviceUrl: handle },
51
-
});
52
-
53
-
setNotice(`Redirecting...`);
54
-
await new Promise((resolve) => setTimeout(resolve, 250));
55
-
56
-
location.assign(authUrl);
57
-
} catch (e) {
58
-
console.error(e);
59
-
setNotice(`${e}`);
60
-
}
61
-
};
62
-
63
-
return (
64
-
<form class="flex flex-col gap-y-2 px-1" onsubmit={(e) => e.preventDefault()}>
65
-
<label for="username" class="hidden">
66
-
Add account
67
-
</label>
68
-
<div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex grow items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400">
69
-
<label
70
-
for="username"
71
-
class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400"
72
-
></label>
73
-
<input
74
-
type="text"
75
-
spellcheck={false}
76
-
placeholder="user.bsky.social"
77
-
id="username"
78
-
name="username"
79
-
autocomplete="username"
80
-
aria-label="Your AT Protocol handle"
81
-
class="grow py-1 select-none placeholder:text-sm focus:outline-none"
82
-
onInput={(e) => setLoginInput(e.currentTarget.value)}
83
-
/>
84
-
<button
85
-
onclick={() => login(loginInput())}
86
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
87
-
>
88
-
<span class="iconify lucide--log-in"></span>
89
-
</button>
90
-
</div>
91
-
<Show when={notice()}>
92
-
<div class="text-sm">{notice()}</div>
93
-
</Show>
94
-
</form>
95
-
);
96
-
};
97
-
98
-
const retrieveSession = async () => {
99
-
const init = async (): Promise<Session | undefined> => {
100
-
const params = new URLSearchParams(location.hash.slice(1));
101
-
102
-
if (params.has("state") && (params.has("code") || params.has("error"))) {
103
-
history.replaceState(null, "", location.pathname + location.search);
104
-
105
-
const auth = await finalizeAuthorization(params);
106
-
const did = auth.session.info.sub;
107
-
108
-
localStorage.setItem("lastSignedIn", did);
109
-
110
-
const sessions = localStorage.getItem("sessions");
111
-
const newSessions: Sessions = sessions ? JSON.parse(sessions) : { [did]: {} };
112
-
newSessions[did] = { signedIn: true };
113
-
localStorage.setItem("sessions", JSON.stringify(newSessions));
114
-
return auth.session;
115
-
} else {
116
-
const lastSignedIn = localStorage.getItem("lastSignedIn");
117
-
118
-
if (lastSignedIn) {
119
-
const sessions = localStorage.getItem("sessions");
120
-
const newSessions: Sessions = sessions ? JSON.parse(sessions) : {};
121
-
try {
122
-
const session = await getSession(lastSignedIn as Did);
123
-
const rpc = new Client({ handler: new OAuthUserAgent(session) });
124
-
const res = await rpc.get("com.atproto.server.getSession");
125
-
newSessions[lastSignedIn].signedIn = true;
126
-
localStorage.setItem("sessions", JSON.stringify(newSessions));
127
-
if (!res.ok) throw res.data.error;
128
-
return session;
129
-
} catch (err) {
130
-
newSessions[lastSignedIn].signedIn = false;
131
-
localStorage.setItem("sessions", JSON.stringify(newSessions));
132
-
throw err;
133
-
}
134
-
}
135
-
}
136
-
};
137
-
138
-
const session = await init();
139
-
140
-
if (session) setAgent(new OAuthUserAgent(session));
141
-
};
142
-
143
-
export { Login, retrieveSession };
···
+7
-6
src/components/notification.tsx
+7
-6
src/components/notification.tsx
···
1
import { createSignal, For, Show } from "solid-js";
2
3
export type Notification = {
4
id: string;
···
8
type?: "info" | "success" | "error";
9
};
10
11
-
const [notifications, setNotifications] = createSignal<Notification[]>([]);
12
const [removingIds, setRemovingIds] = createSignal<Set<string>>(new Set());
13
14
export const addNotification = (notification: Omit<Notification, "id">) => {
15
const id = `notification-${Date.now()}-${Math.random()}`;
16
-
setNotifications([...notifications(), { ...notification, id }]);
17
return id;
18
};
19
20
export const updateNotification = (id: string, updates: Partial<Notification>) => {
21
-
setNotifications(notifications().map((n) => (n.id === id ? { ...n, ...updates } : n)));
22
};
23
24
export const removeNotification = (id: string) => {
25
setRemovingIds(new Set([...removingIds(), id]));
26
setTimeout(() => {
27
-
setNotifications(notifications().filter((n) => n.id !== id));
28
setRemovingIds((ids) => {
29
const newIds = new Set(ids);
30
newIds.delete(id);
···
35
36
export const NotificationContainer = () => {
37
return (
38
-
<div class="pointer-events-none fixed bottom-4 left-4 z-50 flex flex-col gap-2">
39
-
<For each={notifications()}>
40
{(notification) => (
41
<div
42
class="dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto flex min-w-64 flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 shadow-md select-none dark:border-neutral-700"
···
1
import { createSignal, For, Show } from "solid-js";
2
+
import { createStore } from "solid-js/store";
3
4
export type Notification = {
5
id: string;
···
9
type?: "info" | "success" | "error";
10
};
11
12
+
const [notifications, setNotifications] = createStore<Notification[]>([]);
13
const [removingIds, setRemovingIds] = createSignal<Set<string>>(new Set());
14
15
export const addNotification = (notification: Omit<Notification, "id">) => {
16
const id = `notification-${Date.now()}-${Math.random()}`;
17
+
setNotifications(notifications.length, { ...notification, id });
18
return id;
19
};
20
21
export const updateNotification = (id: string, updates: Partial<Notification>) => {
22
+
setNotifications((n) => n.id === id, updates);
23
};
24
25
export const removeNotification = (id: string) => {
26
setRemovingIds(new Set([...removingIds(), id]));
27
setTimeout(() => {
28
+
setNotifications((n) => n.filter((notification) => notification.id !== id));
29
setRemovingIds((ids) => {
30
const newIds = new Set(ids);
31
newIds.delete(id);
···
36
37
export const NotificationContainer = () => {
38
return (
39
+
<div class="pointer-events-none fixed bottom-4 left-4 z-60 flex flex-col gap-2">
40
+
<For each={notifications}>
41
{(notification) => (
42
<div
43
class="dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto flex min-w-64 flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 shadow-md select-none dark:border-neutral-700"
+42
-15
src/components/search.tsx
+42
-15
src/components/search.tsx
···
1
-
import { Client, CredentialManager } from "@atcute/client";
2
import { Nsid } from "@atcute/lexicons";
3
-
import { A, useLocation, useNavigate } from "@solidjs/router";
4
-
import { createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js";
5
import { isTouchDevice } from "../layout";
6
import { resolveLexiconAuthority } from "../utils/api";
7
import { appHandleLink, appList, appName, AppUrl } from "../utils/app-urls";
···
38
39
if ((ev.ctrlKey || ev.metaKey) && ev.key == "k") {
40
ev.preventDefault();
41
-
setShowSearch(!showSearch());
42
} else if (ev.key == "Escape") {
43
ev.preventDefault();
44
setShowSearch(false);
···
67
const navigate = useNavigate();
68
let searchInput!: HTMLInputElement;
69
const rpc = new Client({
70
-
handler: new CredentialManager({ service: "https://public.api.bsky.app" }),
71
});
72
73
onMount(() => {
74
-
if (useLocation().pathname !== "/") searchInput.focus();
75
-
76
const handlePaste = (e: ClipboardEvent) => {
77
if (e.target === searchInput) return;
78
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
···
83
84
window.addEventListener("paste", handlePaste);
85
onCleanup(() => window.removeEventListener("paste", handlePaste));
86
});
87
88
const fetchTypeahead = async (input: string) => {
···
111
const currentInput = input();
112
if (!currentInput) return SEARCH_PREFIXES;
113
114
-
const { prefix } = parsePrefix(currentInput);
115
-
if (prefix) return [];
116
117
return SEARCH_PREFIXES.filter((p) => p.prefix.startsWith(currentInput.toLowerCase()));
118
};
···
238
<Show when={input()} fallback={ListUrlsTooltip()}>
239
<button
240
type="button"
241
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
242
onClick={() => setInput(undefined)}
243
>
244
<span class="iconify lucide--x"></span>
···
255
{(prefixItem, index) => (
256
<button
257
type="button"
258
-
class={`flex items-center rounded-lg p-2 ${
259
index() === selectedIndex() ?
260
"bg-neutral-200 dark:bg-neutral-700"
261
: "hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
···
280
const adjustedIndex = getPrefixSuggestions().length + index();
281
return (
282
<A
283
-
class={`flex items-center gap-2 rounded-lg p-2 ${
284
adjustedIndex === selectedIndex() ?
285
"bg-neutral-200 dark:bg-neutral-700"
286
: "hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
···
290
>
291
<img
292
src={actor.avatar?.replace("img/avatar/", "img/avatar_thumbnail/")}
293
-
class="size-8 rounded-full"
294
/>
295
-
<span>{actor.handle}</span>
296
</A>
297
);
298
}}
···
352
</Modal>
353
<button
354
type="button"
355
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
356
onClick={() => setOpenList(true)}
357
>
358
<span class="iconify lucide--help-circle"></span>
···
1
+
import { Client, simpleFetchHandler } from "@atcute/client";
2
import { Nsid } from "@atcute/lexicons";
3
+
import { A, useNavigate } from "@solidjs/router";
4
+
import {
5
+
createEffect,
6
+
createResource,
7
+
createSignal,
8
+
For,
9
+
onCleanup,
10
+
onMount,
11
+
Show,
12
+
} from "solid-js";
13
import { isTouchDevice } from "../layout";
14
import { resolveLexiconAuthority } from "../utils/api";
15
import { appHandleLink, appList, appName, AppUrl } from "../utils/app-urls";
···
46
47
if ((ev.ctrlKey || ev.metaKey) && ev.key == "k") {
48
ev.preventDefault();
49
+
50
+
if (showSearch()) {
51
+
const searchInput = document.querySelector("#input") as HTMLInputElement;
52
+
if (searchInput && document.activeElement !== searchInput) {
53
+
searchInput.focus();
54
+
} else {
55
+
setShowSearch(false);
56
+
}
57
+
} else {
58
+
setShowSearch(true);
59
+
}
60
} else if (ev.key == "Escape") {
61
ev.preventDefault();
62
setShowSearch(false);
···
85
const navigate = useNavigate();
86
let searchInput!: HTMLInputElement;
87
const rpc = new Client({
88
+
handler: simpleFetchHandler({ service: "https://public.api.bsky.app" }),
89
});
90
91
onMount(() => {
92
const handlePaste = (e: ClipboardEvent) => {
93
if (e.target === searchInput) return;
94
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
···
99
100
window.addEventListener("paste", handlePaste);
101
onCleanup(() => window.removeEventListener("paste", handlePaste));
102
+
});
103
+
104
+
createEffect(() => {
105
+
if (showSearch()) searchInput.focus();
106
});
107
108
const fetchTypeahead = async (input: string) => {
···
131
const currentInput = input();
132
if (!currentInput) return SEARCH_PREFIXES;
133
134
+
const { prefix, query } = parsePrefix(currentInput);
135
+
if (prefix && query.length > 0) return [];
136
137
return SEARCH_PREFIXES.filter((p) => p.prefix.startsWith(currentInput.toLowerCase()));
138
};
···
258
<Show when={input()} fallback={ListUrlsTooltip()}>
259
<button
260
type="button"
261
+
class="flex items-center rounded-md p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
262
onClick={() => setInput(undefined)}
263
>
264
<span class="iconify lucide--x"></span>
···
275
{(prefixItem, index) => (
276
<button
277
type="button"
278
+
class={`flex items-center rounded-md p-2 ${
279
index() === selectedIndex() ?
280
"bg-neutral-200 dark:bg-neutral-700"
281
: "hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
···
300
const adjustedIndex = getPrefixSuggestions().length + index();
301
return (
302
<A
303
+
class={`flex items-center gap-2 rounded-md p-2 ${
304
adjustedIndex === selectedIndex() ?
305
"bg-neutral-200 dark:bg-neutral-700"
306
: "hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
···
310
>
311
<img
312
src={actor.avatar?.replace("img/avatar/", "img/avatar_thumbnail/")}
313
+
class="size-9 rounded-full"
314
/>
315
+
<div class="flex flex-col">
316
+
<Show when={actor.displayName}>
317
+
<span class="text-sm font-medium">{actor.displayName}</span>
318
+
</Show>
319
+
<span class="text-xs text-neutral-600 dark:text-neutral-400">
320
+
@{actor.handle}
321
+
</span>
322
+
</div>
323
</A>
324
);
325
}}
···
379
</Modal>
380
<button
381
type="button"
382
+
class="flex items-center rounded-md p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
383
onClick={() => setOpenList(true)}
384
>
385
<span class="iconify lucide--help-circle"></span>
+13
-8
src/components/theme.tsx
+13
-8
src/components/theme.tsx
···
24
else localStorage.theme = newTheme;
25
};
26
27
-
const ThemeButton = (props: { theme: string; icon: string }) => {
28
return (
29
<button
30
classList={{
31
-
"p-1.5 flex items-center rounded-full border-[0.5px]": true,
32
-
"bg-neutral-200/60 border-neutral-300/60 dark:border-neutral-500/60 dark:bg-neutral-600":
33
theme() === props.theme,
34
-
"border-transparent": theme() !== props.theme,
35
}}
36
onclick={() => updateTheme(props.theme)}
37
>
38
<span class={"iconify " + props.icon}></span>
39
</button>
40
);
41
};
42
43
return (
44
-
<div class="dark:bg-dark-100 dark:inset-shadow-dark-200 mt-2 flex items-center justify-between gap-1 rounded-full border-[0.5px] border-neutral-200/60 bg-white p-1 text-base text-neutral-800 inset-shadow-sm dark:border-neutral-600 dark:text-neutral-300">
45
-
<ThemeButton theme="system" icon="lucide--monitor" />
46
-
<ThemeButton theme="light" icon="lucide--sun" />
47
-
<ThemeButton theme="dark" icon="lucide--moon" />
48
</div>
49
);
50
};
···
24
else localStorage.theme = newTheme;
25
};
26
27
+
const ThemeOption = (props: { theme: string; icon: string; label: string }) => {
28
return (
29
<button
30
classList={{
31
+
"flex items-center gap-2 rounded-xl border px-3 py-2": true,
32
+
"bg-neutral-200/60 border-neutral-300 dark:border-neutral-500 dark:bg-neutral-700":
33
theme() === props.theme,
34
+
"border-neutral-200 dark:border-neutral-600 hover:bg-neutral-200/30 dark:hover:bg-neutral-800":
35
+
theme() !== props.theme,
36
}}
37
onclick={() => updateTheme(props.theme)}
38
>
39
<span class={"iconify " + props.icon}></span>
40
+
<span>{props.label}</span>
41
</button>
42
);
43
};
44
45
return (
46
+
<div class="flex flex-col gap-0.5">
47
+
<label class="select-none">Theme</label>
48
+
<div class="flex gap-2">
49
+
<ThemeOption theme="system" icon="lucide--monitor" label="System" />
50
+
<ThemeOption theme="light" icon="lucide--sun" label="Light" />
51
+
<ThemeOption theme="dark" icon="lucide--moon" label="Dark" />
52
+
</div>
53
</div>
54
);
55
};
+7
-1
src/components/video-player.tsx
+7
-1
src/components/video-player.tsx
+30
-26
src/layout.tsx
+30
-26
src/layout.tsx
···
1
import { Handle } from "@atcute/lexicons";
2
import { Meta, MetaProvider } from "@solidjs/meta";
3
import { A, RouteSectionProps, useLocation, useNavigate } from "@solidjs/router";
4
-
import { createEffect, ErrorBoundary, onMount, Show, Suspense } from "solid-js";
5
-
import { AccountManager } from "./components/account.jsx";
6
-
import { RecordEditor } from "./components/create.jsx";
7
import { DropdownMenu, MenuProvider, MenuSeparator, NavMenu } from "./components/dropdown.jsx";
8
-
import { agent } from "./components/login.jsx";
9
import { NavBar } from "./components/navbar.jsx";
10
import { NotificationContainer } from "./components/notification.jsx";
11
import { Search, SearchButton, showSearch } from "./components/search.jsx";
12
-
import { themeEvent, ThemeSelection } from "./components/theme.jsx";
13
import { resolveHandle } from "./utils/api.js";
14
15
export const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1;
···
38
createEffect(async () => {
39
if (props.params.repo && !props.params.repo.startsWith("did:")) {
40
const did = await resolveHandle(props.params.repo as Handle);
41
-
navigate(location.pathname.replace(props.params.repo, did));
42
}
43
});
44
45
onMount(() => {
46
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent);
47
48
if (localStorage.getItem("sailor") === "true") {
49
const style = document.createElement("style");
···
104
});
105
106
return (
107
-
<div
108
-
id="main"
109
-
class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4 text-neutral-900 dark:text-neutral-200"
110
-
>
111
<MetaProvider>
112
<Show when={location.pathname !== "/"}>
113
<Meta name="robots" content="noindex, nofollow" />
···
131
<span>PDSls</span>
132
</A>
133
<div class="dark:bg-dark-300/60 relative flex items-center gap-0.5 rounded-lg bg-neutral-50/60 px-1 py-0.5">
134
-
<Show when={location.pathname !== "/"}>
135
-
<SearchButton />
136
-
</Show>
137
-
<Show when={agent()}>
138
<RecordEditor create={true} />
139
</Show>
140
<AccountManager />
141
<MenuProvider>
142
-
<DropdownMenu
143
-
icon="lucide--menu text-lg"
144
-
buttonClass="rounded-lg p-1.5"
145
-
menuClass="top-11 p-3 text-sm"
146
-
>
147
-
<NavMenu href="/jetstream" label="Jetstream" />
148
-
<NavMenu href="/firehose" label="Firehose" />
149
-
<NavMenu href="/labels" label="Labels" />
150
-
<NavMenu href="/settings" label="Settings" />
151
<MenuSeparator />
152
<NavMenu
153
href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz"
154
label="Bluesky"
155
newTab
156
-
external
157
/>
158
<NavMenu
159
href="https://tangled.org/@pdsls.dev/pdsls/"
160
label="Source"
161
newTab
162
-
external
163
/>
164
-
<ThemeSelection />
165
</DropdownMenu>
166
</MenuProvider>
167
</div>
···
1
import { Handle } from "@atcute/lexicons";
2
import { Meta, MetaProvider } from "@solidjs/meta";
3
import { A, RouteSectionProps, useLocation, useNavigate } from "@solidjs/router";
4
+
import { createEffect, ErrorBoundary, onCleanup, onMount, Show, Suspense } from "solid-js";
5
+
import { AccountManager } from "./auth/account.jsx";
6
+
import { hasUserScope } from "./auth/scope-utils";
7
+
import { agent } from "./auth/state.js";
8
+
import { RecordEditor } from "./components/create";
9
import { DropdownMenu, MenuProvider, MenuSeparator, NavMenu } from "./components/dropdown.jsx";
10
import { NavBar } from "./components/navbar.jsx";
11
import { NotificationContainer } from "./components/notification.jsx";
12
import { Search, SearchButton, showSearch } from "./components/search.jsx";
13
+
import { themeEvent } from "./components/theme.jsx";
14
import { resolveHandle } from "./utils/api.js";
15
16
export const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 1;
···
39
createEffect(async () => {
40
if (props.params.repo && !props.params.repo.startsWith("did:")) {
41
const did = await resolveHandle(props.params.repo as Handle);
42
+
navigate(location.pathname.replace(props.params.repo, did), { replace: true });
43
}
44
});
45
46
onMount(() => {
47
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent);
48
+
49
+
const handleGoToRepo = (ev: KeyboardEvent) => {
50
+
if (document.querySelector("[data-modal]")) return;
51
+
if (ev.target instanceof HTMLInputElement || ev.target instanceof HTMLTextAreaElement) return;
52
+
53
+
if (ev.key === "g" && agent()?.sub) {
54
+
ev.preventDefault();
55
+
navigate(`/at://${agent()!.sub}`);
56
+
}
57
+
};
58
+
59
+
window.addEventListener("keydown", handleGoToRepo);
60
+
onCleanup(() => window.removeEventListener("keydown", handleGoToRepo));
61
62
if (localStorage.getItem("sailor") === "true") {
63
const style = document.createElement("style");
···
118
});
119
120
return (
121
+
<div id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4">
122
<MetaProvider>
123
<Show when={location.pathname !== "/"}>
124
<Meta name="robots" content="noindex, nofollow" />
···
142
<span>PDSls</span>
143
</A>
144
<div class="dark:bg-dark-300/60 relative flex items-center gap-0.5 rounded-lg bg-neutral-50/60 px-1 py-0.5">
145
+
<SearchButton />
146
+
<Show when={hasUserScope("create")}>
147
<RecordEditor create={true} />
148
</Show>
149
<AccountManager />
150
<MenuProvider>
151
+
<DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5">
152
+
<NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" />
153
+
<NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" />
154
+
<NavMenu href="/labels" label="Labels" icon="lucide--tags" />
155
+
<NavMenu href="/settings" label="Settings" icon="lucide--settings" />
156
<MenuSeparator />
157
<NavMenu
158
href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz"
159
label="Bluesky"
160
+
icon="simple-icons--bluesky text-[#0085ff]"
161
newTab
162
/>
163
<NavMenu
164
href="https://tangled.org/@pdsls.dev/pdsls/"
165
label="Source"
166
+
icon="lucide--code"
167
newTab
168
/>
169
</DropdownMenu>
170
</MenuProvider>
171
</div>
+13
-3
src/styles/index.css
+13
-3
src/styles/index.css
···
6
7
@custom-variant dark (&:where(.dark, .dark *));
8
9
@theme {
10
-
--font-sans: "Inter", sans-serif;
11
--font-mono: "Roboto Mono", monospace;
12
--font-pecita: "Pecita", serif;
13
···
40
--svg: url("data:image/svg+xml,%3Csvg%20width%3D%22800%22%20height%3D%22800%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22m3%206%203.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%206M3%2010.5l3.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%2010.5M3%2015l3.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%2015M3%2019.5l3.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%2019.5%22%20stroke%3D%22%23ffe5ea%22%20stroke-width%3D%223%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
41
}
42
43
-
.ri--bluesky {
44
-
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 11.388c-.906-1.761-3.372-5.044-5.665-6.662c-2.197-1.55-3.034-1.283-3.583-1.033C2.116 3.978 2 4.955 2 5.528c0 .575.315 4.709.52 5.4c.68 2.28 3.094 3.05 5.32 2.803c-3.26.483-6.157 1.67-2.36 5.898c4.178 4.325 5.726-.927 6.52-3.59c.794 2.663 1.708 7.726 6.444 3.59c3.556-3.59.977-5.415-2.283-5.898c2.225.247 4.64-.523 5.319-2.803c.205-.69.52-4.825.52-5.399c0-.575-.116-1.55-.752-1.838c-.549-.248-1.386-.517-3.583 1.033c-2.293 1.621-4.76 4.904-5.665 6.664'/%3E%3C/svg%3E");
45
}
46
47
@keyframes slideIn {
···
6
7
@custom-variant dark (&:where(.dark, .dark *));
8
9
+
@font-face {
10
+
font-family: "Figtree";
11
+
src: url("/fonts/Figtree[wght].woff2") format("woff2");
12
+
font-display: swap;
13
+
}
14
+
15
@theme {
16
+
--font-sans: "Figtree", sans-serif;
17
--font-mono: "Roboto Mono", monospace;
18
--font-pecita: "Pecita", serif;
19
···
46
--svg: url("data:image/svg+xml,%3Csvg%20width%3D%22800%22%20height%3D%22800%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22m3%206%203.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%206M3%2010.5l3.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%2010.5M3%2015l3.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%2015M3%2019.5l3.106-1.553a2%202%200%200%201%201.788%200l1.423.711a6%206%200%200%200%205.366%200l1.423-.71a2%202%200%200%201%201.788%200L21%2019.5%22%20stroke%3D%22%23ffe5ea%22%20stroke-width%3D%223%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
47
}
48
49
+
.simple-icons--bluesky {
50
+
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037c-.856 3.061-3.978 3.842-6.755 3.37c4.854.826 6.089 3.562 3.422 6.299c-5.065 5.196-7.28-1.304-7.847-2.97c-.104-.305-.152-.448-.153-.327c0-.121-.05.022-.153.327c-.568 1.666-2.782 8.166-7.847 2.97c-2.667-2.737-1.432-5.473 3.422-6.3c-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026'/%3E%3C/svg%3E");
51
+
}
52
+
53
+
.i-leaflet {
54
+
--svg: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22none%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M1.468%2010.977c-.55%201.061-.961%201.751-1.359%202.741a1.508%201.508%200%201%200%202.8%201.124l.227-.574v-.002c.28-.71.52-1.316.81-1.862.328-.018.702-.02%201.125-.023h.053c.77-.005%201.697-.01%202.497-.172s1.791-.545%202.229-1.57c.119-.278.239-.688.134-1.105h.151c.422%200%201.017.001%201.548-.143.62-.17%201.272-.569%201.558-1.41a1.52%201.52%200%200%200%20.034-.925l.084-.015.042-.007c.363-.063.849-.148%201.264-.304.404-.15%201.068-.488%201.267-1.262.113-.44.1-.908-.154-1.33a1.7%201.7%200%200%200-.36-.414c.112-.14.253-.333.35-.547.17-.371.257-.916-.089-1.45-.393-.604-1.066-.71-1.4-.737a6%206%200%200%200-.985.026%201.2%201.2%200%200%200-.156-.275c-.371-.496-.947-.538-1.272-.53-.655.018-1.167.31-1.538.61-.194.159-.657.806-.808.974%200-.603-.581-.91-.99-.973-.794-.123-1.285.388-1.742.973-.57.73-1.01%201.668-1.531%202.373-.18-.117-.393-.39-.733-.375-.56.026-.932.406-1.173.666-.419.452-.685%201.273-.867%201.885-.197.885-.332%201.258-.491%202.228a9.4%209.4%200%200%200-.144%201.677c-.109.213-.234.443-.381.728%22%20fill%3D%22%23639431%22%2F%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M4.714%204.78c.16.14.349.306.755.165.266-.093.61-.695.993-1.367.56-.982%201.205-2.114%201.816-2.02.738.114.693.523.658.837-.025.22-.044.394.216.387.264-.006.521-.317.82-.678.413-.498.904-1.092%201.602-1.11.492-.014.484.198.476.413-.005.138-.01.276.116.358.123.08.434.053.79.02.573-.052%201.265-.114%201.497.243.204.314-.056.626-.305.925-.21.254-.414.498-.321.726.076.186.231.291.383.394.25.168.491.33.361.834-.136.533-.96.677-1.732.812-.646.113-1.257.22-1.397.544-.088.203.058.297.222.403.195.127.415.27.292.633-.29.85-1.254.85-2.16.85-.777%200-1.51%200-1.735.537-.13.31.067.365.282.425.264.074.557.155.315.723-.464%201.087-2.195%201.096-3.78%201.105-.58.004-1.141.007-1.613.063a.18.18%200%200%200-.13.083c-.434.713-.742%201.496-1.07%202.332l-.221.559a.486.486%200%201%201-.903-.363c.373-.928.803-1.781%201.273-2.564.767-1.413%202.28-3.147%203.88-4.45%201.423-1.184%202.782-2.071%204.364-2.744.198-.084.139-.316-.068-.256-1.403.405-2.643%201.21-3.928%202.02-1.399.881-2.57%202.073-3.291%202.94-.127.153-.405.027-.365-.168.313-1.523.636-2.92%201.11-3.432.45-.485.603-.35.798-.18%22%20fill%3D%22%23d9ea72%22%2F%3E%3C%2Fsvg%3E");
55
}
56
57
@keyframes slideIn {
-10
src/utils/app-urls.ts
-10
src/utils/app-urls.ts
···
3
export enum App {
4
Bluesky,
5
Tangled,
6
-
Whitewind,
7
Frontpage,
8
Pinksea,
9
Linkat,
···
12
export const appName = {
13
[App.Bluesky]: "Bluesky",
14
[App.Tangled]: "Tangled",
15
-
[App.Whitewind]: "Whitewind",
16
[App.Frontpage]: "Frontpage",
17
[App.Pinksea]: "Pinksea",
18
[App.Linkat]: "Linkat",
···
29
"main.bsky.dev": App.Bluesky,
30
"social.daniela.lol": App.Bluesky,
31
"tangled.org": App.Tangled,
32
-
"whtwnd.com": App.Whitewind,
33
"frontpage.fyi": App.Frontpage,
34
"pinksea.art": App.Pinksea,
35
"linkat.blue": App.Linkat,
···
91
}
92
93
return `at://${user}`;
94
-
},
95
-
[App.Whitewind]: (path) => {
96
-
if (path.length === 2) {
97
-
return `at://${path[0]}/com.whtwnd.blog.entry/${path[1]}`;
98
-
}
99
-
100
-
return `at://${path[0]}/com.whtwnd.blog.entry`;
101
},
102
[App.Frontpage]: (path) => {
103
if (path.length === 3) {
···
3
export enum App {
4
Bluesky,
5
Tangled,
6
Frontpage,
7
Pinksea,
8
Linkat,
···
11
export const appName = {
12
[App.Bluesky]: "Bluesky",
13
[App.Tangled]: "Tangled",
14
[App.Frontpage]: "Frontpage",
15
[App.Pinksea]: "Pinksea",
16
[App.Linkat]: "Linkat",
···
27
"main.bsky.dev": App.Bluesky,
28
"social.daniela.lol": App.Bluesky,
29
"tangled.org": App.Tangled,
30
"frontpage.fyi": App.Frontpage,
31
"pinksea.art": App.Pinksea,
32
"linkat.blue": App.Linkat,
···
88
}
89
90
return `at://${user}`;
91
},
92
[App.Frontpage]: (path) => {
93
if (path.length === 3) {
+15
-15
src/utils/hooks/debounced.ts
+15
-15
src/utils/hooks/debounced.ts
···
1
-
import { type Accessor, createEffect, createSignal, onCleanup } from 'solid-js';
2
3
export const createDebouncedValue = <T>(
4
-
accessor: Accessor<T>,
5
-
delay: number,
6
-
equals?: false | ((prev: T, next: T) => boolean),
7
): Accessor<T> => {
8
-
const initial = accessor();
9
-
const [state, setState] = createSignal(initial, { equals });
10
11
-
createEffect((prev: T) => {
12
-
const next = accessor();
13
14
-
if (prev !== next) {
15
-
const timeout = setTimeout(() => setState(() => next), delay);
16
-
onCleanup(() => clearTimeout(timeout));
17
-
}
18
19
-
return next;
20
-
}, initial);
21
22
-
return state;
23
};
···
1
+
import { type Accessor, createEffect, createSignal, onCleanup } from "solid-js";
2
3
export const createDebouncedValue = <T>(
4
+
accessor: Accessor<T>,
5
+
delay: number,
6
+
equals?: false | ((prev: T, next: T) => boolean),
7
): Accessor<T> => {
8
+
const initial = accessor();
9
+
const [state, setState] = createSignal(initial, { equals });
10
11
+
createEffect((prev: T) => {
12
+
const next = accessor();
13
14
+
if (prev !== next) {
15
+
const timeout = setTimeout(() => setState(() => next), delay);
16
+
onCleanup(() => clearTimeout(timeout));
17
+
}
18
19
+
return next;
20
+
}, initial);
21
22
+
return state;
23
};
+30
src/utils/key.ts
+30
src/utils/key.ts
···
···
1
+
import { parseDidKey, parsePublicMultikey } from "@atcute/crypto";
2
+
import { fromBase58Btc } from "@atcute/multibase";
3
+
4
+
export const detectKeyType = (key: string): string => {
5
+
try {
6
+
return parsePublicMultikey(key).type;
7
+
} catch (e) {
8
+
try {
9
+
const bytes = fromBase58Btc(key.startsWith("z") ? key.slice(1) : key);
10
+
if (bytes.length >= 2) {
11
+
const type = (bytes[0] << 8) | bytes[1];
12
+
if (type === 0xed01) {
13
+
return "ed25519";
14
+
}
15
+
}
16
+
} catch {}
17
+
return "unknown";
18
+
}
19
+
};
20
+
21
+
export const detectDidKeyType = (key: string): string => {
22
+
try {
23
+
return parseDidKey(key).type;
24
+
} catch (e) {
25
+
if (key.startsWith("did:key:")) {
26
+
return detectKeyType(key.slice(8));
27
+
}
28
+
return "unknown";
29
+
}
30
+
};
+14
-8
src/utils/templates.ts
+14
-8
src/utils/templates.ts
···
6
"app.bsky.actor.profile": (uri) => ({
7
label: "Bluesky",
8
link: `https://bsky.app/profile/${uri.repo}`,
9
-
icon: "ri--bluesky",
10
}),
11
"app.bsky.feed.post": (uri) => ({
12
label: "Bluesky",
13
link: `https://bsky.app/profile/${uri.repo}/post/${uri.rkey}`,
14
-
icon: "ri--bluesky",
15
}),
16
"app.bsky.graph.list": (uri) => ({
17
label: "Bluesky",
18
link: `https://bsky.app/profile/${uri.repo}/lists/${uri.rkey}`,
19
-
icon: "ri--bluesky",
20
}),
21
"app.bsky.feed.generator": (uri) => ({
22
label: "Bluesky",
23
link: `https://bsky.app/profile/${uri.repo}/feed/${uri.rkey}`,
24
-
icon: "ri--bluesky",
25
}),
26
"fyi.unravel.frontpage.post": (uri) => ({
27
label: "Frontpage",
28
link: `https://frontpage.fyi/post/${uri.repo}/${uri.rkey}`,
29
}),
30
-
"com.whtwnd.blog.entry": (uri) => ({
31
-
label: "WhiteWind",
32
-
link: `https://whtwnd.com/${uri.repo}/${uri.rkey}`,
33
-
}),
34
"com.shinolabs.pinksea.oekaki": (uri) => ({
35
label: "PinkSea",
36
link: `https://pinksea.art/${uri.repo}/oekaki/${uri.rkey}`,
···
54
label: "Tangled",
55
link: `https://tangled.org/${uri.repo}/${record.name}`,
56
icon: "i-tangled",
57
}),
58
};
···
6
"app.bsky.actor.profile": (uri) => ({
7
label: "Bluesky",
8
link: `https://bsky.app/profile/${uri.repo}`,
9
+
icon: "simple-icons--bluesky text-[#0085ff]",
10
}),
11
"app.bsky.feed.post": (uri) => ({
12
label: "Bluesky",
13
link: `https://bsky.app/profile/${uri.repo}/post/${uri.rkey}`,
14
+
icon: "simple-icons--bluesky text-[#0085ff]",
15
}),
16
"app.bsky.graph.list": (uri) => ({
17
label: "Bluesky",
18
link: `https://bsky.app/profile/${uri.repo}/lists/${uri.rkey}`,
19
+
icon: "simple-icons--bluesky text-[#0085ff]",
20
}),
21
"app.bsky.feed.generator": (uri) => ({
22
label: "Bluesky",
23
link: `https://bsky.app/profile/${uri.repo}/feed/${uri.rkey}`,
24
+
icon: "simple-icons--bluesky text-[#0085ff]",
25
}),
26
"fyi.unravel.frontpage.post": (uri) => ({
27
label: "Frontpage",
28
link: `https://frontpage.fyi/post/${uri.repo}/${uri.rkey}`,
29
}),
30
"com.shinolabs.pinksea.oekaki": (uri) => ({
31
label: "PinkSea",
32
link: `https://pinksea.art/${uri.repo}/oekaki/${uri.rkey}`,
···
50
label: "Tangled",
51
link: `https://tangled.org/${uri.repo}/${record.name}`,
52
icon: "i-tangled",
53
+
}),
54
+
"pub.leaflet.document": (uri) => ({
55
+
label: "Leaflet",
56
+
link: `https://leaflet.pub/p/${uri.repo}/${uri.rkey}`,
57
+
icon: "iconify-color i-leaflet",
58
+
}),
59
+
"pub.leaflet.publication": (uri) => ({
60
+
label: "Leaflet",
61
+
link: `https://leaflet.pub/lish/${uri.repo}/${uri.rkey}`,
62
+
icon: "iconify-color i-leaflet",
63
}),
64
};
+4
-4
src/views/blob.tsx
+4
-4
src/views/blob.tsx
···
1
-
import { Client, CredentialManager } from "@atcute/client";
2
import { createResource, createSignal, For, Show } from "solid-js";
3
import { Button } from "../components/button";
4
···
9
let rpc: Client;
10
11
const fetchBlobs = async () => {
12
-
if (!rpc) rpc = new Client({ handler: new CredentialManager({ service: props.pds }) });
13
const res = await rpc.get("com.atproto.sync.listBlobs", {
14
params: {
15
did: props.repo as `did:${string}:${string}`,
···
30
return (
31
<div class="flex flex-col items-center gap-2">
32
<Show when={blobs() || response()}>
33
-
<div class="flex flex-col gap-0.5 font-mono text-sm wrap-anywhere lg:break-normal">
34
<For each={blobs()}>
35
{(cid) => (
36
<a
37
href={`${props.pds}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${cid}`}
38
target="_blank"
39
-
class="rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
40
>
41
<span class="text-blue-400">{cid}</span>
42
</a>
···
1
+
import { Client, simpleFetchHandler } from "@atcute/client";
2
import { createResource, createSignal, For, Show } from "solid-js";
3
import { Button } from "../components/button";
4
···
9
let rpc: Client;
10
11
const fetchBlobs = async () => {
12
+
if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: props.pds }) });
13
const res = await rpc.get("com.atproto.sync.listBlobs", {
14
params: {
15
did: props.repo as `did:${string}:${string}`,
···
30
return (
31
<div class="flex flex-col items-center gap-2">
32
<Show when={blobs() || response()}>
33
+
<div class="flex w-full flex-col gap-0.5 font-mono text-xs wrap-anywhere">
34
<For each={blobs()}>
35
{(cid) => (
36
<a
37
href={`${props.pds}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${cid}`}
38
target="_blank"
39
+
class="w-fit rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
40
>
41
<span class="text-blue-400">{cid}</span>
42
</a>
+35
-34
src/views/collection.tsx
+35
-34
src/views/collection.tsx
···
1
import { ComAtprotoRepoApplyWrites, ComAtprotoRepoGetRecord } from "@atcute/atproto";
2
-
import { Client, CredentialManager } from "@atcute/client";
3
import { $type, ActorIdentifier, InferXRPCBodyOutput } from "@atcute/lexicons";
4
import * as TID from "@atcute/tid";
5
import { A, useParams } from "@solidjs/router";
6
-
import { createEffect, createResource, createSignal, For, Show, untrack } from "solid-js";
7
import { createStore } from "solid-js/store";
8
import { Button } from "../components/button.jsx";
9
import { JSONType, JSONValue } from "../components/json.jsx";
10
-
import { agent } from "../components/login.jsx";
11
import { Modal } from "../components/modal.jsx";
12
import { addNotification, removeNotification } from "../components/notification.jsx";
13
import { StickyOverlay } from "../components/sticky.jsx";
···
88
89
const fetchRecords = async () => {
90
if (!pds) pds = await resolvePDS(did!);
91
-
if (!rpc) rpc = new Client({ handler: new CredentialManager({ service: pds }) });
92
const res = await rpc.get("com.atproto.repo.listRecords", {
93
params: {
94
repo: did as ActorIdentifier,
···
117
118
const [response, { refetch }] = createResource(fetchRecords);
119
120
const deleteRecords = async () => {
121
const recsToDel = records.filter((record) => record.toDelete);
122
let writes: Array<
···
192
<StickyOverlay>
193
<div class="flex w-full flex-col gap-2">
194
<div class="flex items-center gap-1">
195
-
<Show when={agent() && agent()?.sub === did}>
196
<div class="flex items-center">
197
<Tooltip
198
text={batchDelete() ? "Cancel" : "Delete"}
199
children={
200
<button
201
onclick={() => {
202
-
setRecords(
203
-
{ from: 0, to: untrack(() => records.length) - 1 },
204
-
"toDelete",
205
-
false,
206
-
);
207
setLastSelected(undefined);
208
setBatchDelete(!batchDelete());
209
}}
210
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
211
>
212
<span
213
class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `}
···
221
children={
222
<button
223
onclick={() => selectAll()}
224
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
225
>
226
<span class="iconify lucide--copy-check text-lg"></span>
227
</button>
228
}
229
/>
230
-
<Tooltip
231
-
text="Recreate"
232
-
children={
233
-
<button
234
-
onclick={() => {
235
-
setRecreate(true);
236
-
setOpenDelete(true);
237
-
}}
238
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
239
-
>
240
-
<span class="iconify lucide--recycle text-lg text-green-500 dark:text-green-400"></span>
241
-
</button>
242
-
}
243
-
/>
244
<Tooltip
245
text="Delete"
246
children={
···
249
setRecreate(false);
250
setOpenDelete(true);
251
}}
252
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
253
>
254
<span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span>
255
</button>
···
278
<Tooltip text="Jetstream">
279
<A
280
href={`/jetstream?collections=${params.collection}&dids=${params.repo}`}
281
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
282
>
283
<span class="iconify lucide--radio-tower text-lg"></span>
284
</A>
···
310
<span>{records.filter((rec) => rec.toDelete).length}</span>
311
<span>/</span>
312
</Show>
313
-
<span>{records.length} records</span>
314
</div>
315
<div class="flex w-20 items-center justify-end">
316
<Show when={cursor()}>
···
327
</div>
328
</StickyOverlay>
329
<div class="flex max-w-full flex-col px-2 font-mono">
330
-
<For
331
-
each={records.filter((rec) =>
332
-
filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true,
333
-
)}
334
-
>
335
{(record, index) => (
336
<>
337
<Show when={batchDelete()}>
···
1
import { ComAtprotoRepoApplyWrites, ComAtprotoRepoGetRecord } from "@atcute/atproto";
2
+
import { Client, simpleFetchHandler } from "@atcute/client";
3
import { $type, ActorIdentifier, InferXRPCBodyOutput } from "@atcute/lexicons";
4
import * as TID from "@atcute/tid";
5
import { A, useParams } from "@solidjs/router";
6
+
import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js";
7
import { createStore } from "solid-js/store";
8
+
import { hasUserScope } from "../auth/scope-utils";
9
+
import { agent } from "../auth/state";
10
import { Button } from "../components/button.jsx";
11
import { JSONType, JSONValue } from "../components/json.jsx";
12
import { Modal } from "../components/modal.jsx";
13
import { addNotification, removeNotification } from "../components/notification.jsx";
14
import { StickyOverlay } from "../components/sticky.jsx";
···
89
90
const fetchRecords = async () => {
91
if (!pds) pds = await resolvePDS(did!);
92
+
if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
93
const res = await rpc.get("com.atproto.repo.listRecords", {
94
params: {
95
repo: did as ActorIdentifier,
···
118
119
const [response, { refetch }] = createResource(fetchRecords);
120
121
+
const filteredRecords = createMemo(() =>
122
+
records.filter((rec) =>
123
+
filter() ? JSON.stringify(rec.record.value).includes(filter()!) : true,
124
+
),
125
+
);
126
+
127
const deleteRecords = async () => {
128
const recsToDel = records.filter((record) => record.toDelete);
129
let writes: Array<
···
199
<StickyOverlay>
200
<div class="flex w-full flex-col gap-2">
201
<div class="flex items-center gap-1">
202
+
<Show when={agent() && agent()?.sub === did && hasUserScope("delete")}>
203
<div class="flex items-center">
204
<Tooltip
205
text={batchDelete() ? "Cancel" : "Delete"}
206
children={
207
<button
208
onclick={() => {
209
+
setRecords({ from: 0, to: records.length - 1 }, "toDelete", false);
210
setLastSelected(undefined);
211
setBatchDelete(!batchDelete());
212
}}
213
+
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
214
>
215
<span
216
class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `}
···
224
children={
225
<button
226
onclick={() => selectAll()}
227
+
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
228
>
229
<span class="iconify lucide--copy-check text-lg"></span>
230
</button>
231
}
232
/>
233
+
<Show when={hasUserScope("create")}>
234
+
<Tooltip
235
+
text="Recreate"
236
+
children={
237
+
<button
238
+
onclick={() => {
239
+
setRecreate(true);
240
+
setOpenDelete(true);
241
+
}}
242
+
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
243
+
>
244
+
<span class="iconify lucide--recycle text-lg text-green-500 dark:text-green-400"></span>
245
+
</button>
246
+
}
247
+
/>
248
+
</Show>
249
<Tooltip
250
text="Delete"
251
children={
···
254
setRecreate(false);
255
setOpenDelete(true);
256
}}
257
+
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
258
>
259
<span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span>
260
</button>
···
283
<Tooltip text="Jetstream">
284
<A
285
href={`/jetstream?collections=${params.collection}&dids=${params.repo}`}
286
+
class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
287
>
288
<span class="iconify lucide--radio-tower text-lg"></span>
289
</A>
···
315
<span>{records.filter((rec) => rec.toDelete).length}</span>
316
<span>/</span>
317
</Show>
318
+
<span>{filter() ? filteredRecords().length : records.length} records</span>
319
</div>
320
<div class="flex w-20 items-center justify-end">
321
<Show when={cursor()}>
···
332
</div>
333
</StickyOverlay>
334
<div class="flex max-w-full flex-col px-2 font-mono">
335
+
<For each={filteredRecords()}>
336
{(record, index) => (
337
<>
338
<Show when={batchDelete()}>
+19
-21
src/views/labels.tsx
+19
-21
src/views/labels.tsx
···
1
import { ComAtprotoLabelDefs } from "@atcute/atproto";
2
-
import { Client, CredentialManager } from "@atcute/client";
3
import { isAtprotoDid } from "@atcute/identity";
4
import { Handle } from "@atcute/lexicons";
5
import { A, useSearchParams } from "@solidjs/router";
···
17
18
return (
19
<div class="flex flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 dark:border-neutral-700 dark:bg-neutral-800">
20
-
<div class="flex flex-wrap items-center gap-x-2 gap-y-2">
21
-
<div class="inline-flex items-center gap-x-1 text-sm font-medium">
22
-
<span class="iconify lucide--tag shrink-0" />
23
-
{label.val}
24
-
</div>
25
-
<Show when={label.neg}>
26
-
<div class="inline-flex items-center gap-x-1 text-xs font-medium text-red-500 dark:text-red-400">
27
-
<span>negated</span>
28
-
</div>
29
-
</Show>
30
-
<div class="flex flex-wrap gap-3 text-xs text-neutral-600 dark:text-neutral-400">
31
-
<span>{localDateFromTimestamp(new Date(label.cts).getTime())}</span>
32
-
<Show when={label.exp}>
33
-
{(exp) => (
34
-
<div class="flex items-center gap-x-1">
35
-
<span class="iconify lucide--clock-fading shrink-0" />
36
-
<span>{localDateFromTimestamp(new Date(exp()).getTime())}</span>
37
-
</div>
38
-
)}
39
</Show>
40
</div>
41
</div>
42
···
160
await resolvePDS(did);
161
if (!labelerCache[did]) throw new Error("Repository is not a labeler");
162
rpc = new Client({
163
-
handler: new CredentialManager({ service: labelerCache[did] }),
164
});
165
166
setSearchParams({ did, uriPatterns });
···
1
import { ComAtprotoLabelDefs } from "@atcute/atproto";
2
+
import { Client, simpleFetchHandler } from "@atcute/client";
3
import { isAtprotoDid } from "@atcute/identity";
4
import { Handle } from "@atcute/lexicons";
5
import { A, useSearchParams } from "@solidjs/router";
···
17
18
return (
19
<div class="flex flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 dark:border-neutral-700 dark:bg-neutral-800">
20
+
<div class="flex gap-1 text-sm">
21
+
<span class="iconify lucide--tag shrink-0 self-center" />
22
+
<div class="flex flex-wrap items-baseline gap-2">
23
+
<span class="font-medium">{label.val}</span>
24
+
<Show when={label.neg}>
25
+
<span class="text-xs font-medium text-red-500 dark:text-red-400">negated</span>
26
</Show>
27
+
<div class="flex flex-wrap gap-2 text-xs text-neutral-600 dark:text-neutral-400">
28
+
<span>{localDateFromTimestamp(new Date(label.cts).getTime())}</span>
29
+
<Show when={label.exp}>
30
+
{(exp) => (
31
+
<div class="flex items-center gap-x-1">
32
+
<span class="iconify lucide--clock-fading shrink-0" />
33
+
<span>{localDateFromTimestamp(new Date(exp()).getTime())}</span>
34
+
</div>
35
+
)}
36
+
</Show>
37
+
</div>
38
</div>
39
</div>
40
···
158
await resolvePDS(did);
159
if (!labelerCache[did]) throw new Error("Repository is not a labeler");
160
rpc = new Client({
161
+
handler: simpleFetchHandler({ service: labelerCache[did] }),
162
});
163
164
setSearchParams({ did, uriPatterns });
+46
-40
src/views/pds.tsx
+46
-40
src/views/pds.tsx
···
1
import { ComAtprotoServerDescribeServer, ComAtprotoSyncListRepos } from "@atcute/atproto";
2
-
import { Client, CredentialManager } from "@atcute/client";
3
import { InferXRPCBodyOutput } from "@atcute/lexicons";
4
import * as TID from "@atcute/tid";
5
import { A, useLocation, useParams } from "@solidjs/router";
···
23
setPDS(params.pds);
24
const pds =
25
params.pds!.startsWith("localhost") ? `http://${params.pds}` : `https://${params.pds}`;
26
-
const rpc = new Client({ handler: new CredentialManager({ service: pds }) });
27
28
const getVersion = async () => {
29
// @ts-expect-error: undocumented endpoint
···
56
const [openInfo, setOpenInfo] = createSignal(false);
57
58
return (
59
-
<div class="flex items-center">
60
<A
61
href={`/at://${repo.did}`}
62
-
class="grow truncate rounded py-0.5 font-mono hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
63
>
64
{repo.did}
65
</A>
···
70
</Show>
71
<button
72
onclick={() => setOpenInfo(true)}
73
-
class="flex items-center rounded-lg p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
74
>
75
<span class="iconify lucide--info"></span>
76
</button>
77
<Modal open={openInfo()} onClose={() => setOpenInfo(false)}>
78
-
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-max max-w-full -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 wrap-break-word shadow-md transition-opacity duration-200 sm:max-w-lg dark:border-neutral-700 starting:opacity-0">
79
-
<div class="mb-1 flex justify-between gap-2">
80
-
<div class="flex items-center gap-1">
81
-
<span class="iconify lucide--info"></span>
82
-
<span class="font-semibold">{repo.did}</span>
83
-
</div>
84
<button
85
onclick={() => setOpenInfo(false)}
86
-
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
87
>
88
<span class="iconify lucide--x"></span>
89
</button>
90
</div>
91
-
<div class="flex flex-col text-sm">
92
-
<span>
93
-
Head: <span class="text-xs">{repo.head}</span>
94
-
</span>
95
<Show when={TID.validate(repo.rev)}>
96
-
<span>
97
-
Rev: {repo.rev} ({localDateFromTimestamp(TID.parse(repo.rev).timestamp / 1000)})
98
-
</span>
99
</Show>
100
<Show when={repo.active !== undefined}>
101
-
<span>Active: {repo.active ? "true" : "false"}</span>
102
</Show>
103
<Show when={repo.status}>
104
-
<span>Status: {repo.status}</span>
105
</Show>
106
</div>
107
</div>
···
111
};
112
113
const Tab = (props: { tab: "repos" | "info"; label: string }) => (
114
-
<div class="flex items-center gap-0.5">
115
-
<A
116
-
classList={{
117
-
"flex items-center gap-1 border-b-2": true,
118
-
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
119
-
(!!location.hash && location.hash !== `#${props.tab}`) ||
120
-
(!location.hash && props.tab !== "repos"),
121
-
}}
122
-
href={`/${params.pds}#${props.tab}`}
123
-
>
124
-
{props.label}
125
-
</A>
126
-
</div>
127
);
128
129
return (
130
<Show when={repos() || response()}>
131
<div class="flex w-full flex-col">
132
<div class="dark:shadow-dark-700 dark:bg-dark-300 mb-2 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700">
133
-
<div class="ml-1 flex gap-3">
134
<Tab tab="repos" label="Repositories" />
135
<Tab tab="info" label="Info" />
136
</div>
137
<MenuProvider>
138
-
<DropdownMenu
139
-
icon="lucide--ellipsis-vertical"
140
-
buttonClass="rounded-sm p-1.5"
141
-
menuClass="top-9 p-2 text-sm"
142
-
>
143
<CopyMenu content={params.pds!} label="Copy PDS" icon="lucide--copy" />
144
<NavMenu
145
href={`/firehose?instance=wss://${params.pds}`}
···
1
import { ComAtprotoServerDescribeServer, ComAtprotoSyncListRepos } from "@atcute/atproto";
2
+
import { Client, simpleFetchHandler } from "@atcute/client";
3
import { InferXRPCBodyOutput } from "@atcute/lexicons";
4
import * as TID from "@atcute/tid";
5
import { A, useLocation, useParams } from "@solidjs/router";
···
23
setPDS(params.pds);
24
const pds =
25
params.pds!.startsWith("localhost") ? `http://${params.pds}` : `https://${params.pds}`;
26
+
const rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
27
28
const getVersion = async () => {
29
// @ts-expect-error: undocumented endpoint
···
56
const [openInfo, setOpenInfo] = createSignal(false);
57
58
return (
59
+
<div class="flex items-center gap-0.5">
60
<A
61
href={`/at://${repo.did}`}
62
+
class="grow truncate rounded-md p-0.5 font-mono hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
63
>
64
{repo.did}
65
</A>
···
70
</Show>
71
<button
72
onclick={() => setOpenInfo(true)}
73
+
class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
74
>
75
<span class="iconify lucide--info"></span>
76
</button>
77
<Modal open={openInfo()} onClose={() => setOpenInfo(false)}>
78
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-max max-w-[90vw] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-white p-3 shadow-md transition-opacity duration-200 sm:max-w-xl dark:border-neutral-700 starting:opacity-0">
79
+
<div class="mb-2 flex items-center justify-between gap-4">
80
+
<p class="truncate font-semibold">{repo.did}</p>
81
<button
82
onclick={() => setOpenInfo(false)}
83
+
class="flex shrink-0 items-center rounded-md p-1.5 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 active:bg-neutral-200 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-200 dark:active:bg-neutral-600"
84
>
85
<span class="iconify lucide--x"></span>
86
</button>
87
</div>
88
+
<div class="grid grid-cols-[auto_1fr] items-baseline gap-x-1 gap-y-0.5 text-sm">
89
+
<span class="font-medium">Head:</span>
90
+
<span class="wrap-anywhere text-neutral-700 dark:text-neutral-300">{repo.head}</span>
91
+
92
<Show when={TID.validate(repo.rev)}>
93
+
<span class="font-medium">Rev:</span>
94
+
<div class="flex gap-1">
95
+
<span class="text-neutral-700 dark:text-neutral-300">{repo.rev}</span>
96
+
<span class="text-neutral-600 dark:text-neutral-400">ยท</span>
97
+
<span class="text-neutral-600 dark:text-neutral-400">
98
+
{localDateFromTimestamp(TID.parse(repo.rev).timestamp / 1000)}
99
+
</span>
100
+
</div>
101
</Show>
102
+
103
<Show when={repo.active !== undefined}>
104
+
<span class="font-medium">Active:</span>
105
+
<span
106
+
class={`iconify self-center ${
107
+
repo.active ?
108
+
"lucide--check text-green-500 dark:text-green-400"
109
+
: "lucide--x text-red-500 dark:text-red-400"
110
+
}`}
111
+
></span>
112
</Show>
113
+
114
<Show when={repo.status}>
115
+
<span class="font-medium">Status:</span>
116
+
<span class="text-neutral-700 dark:text-neutral-300">{repo.status}</span>
117
</Show>
118
</div>
119
</div>
···
123
};
124
125
const Tab = (props: { tab: "repos" | "info"; label: string }) => (
126
+
<A
127
+
classList={{
128
+
"border-b-2": true,
129
+
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
130
+
(!!location.hash && location.hash !== `#${props.tab}`) ||
131
+
(!location.hash && props.tab !== "repos"),
132
+
}}
133
+
href={`/${params.pds}#${props.tab}`}
134
+
>
135
+
{props.label}
136
+
</A>
137
);
138
139
return (
140
<Show when={repos() || response()}>
141
<div class="flex w-full flex-col">
142
<div class="dark:shadow-dark-700 dark:bg-dark-300 mb-2 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700">
143
+
<div class="ml-1 flex items-center gap-3">
144
<Tab tab="repos" label="Repositories" />
145
<Tab tab="info" label="Info" />
146
</div>
147
<MenuProvider>
148
+
<DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5">
149
<CopyMenu content={params.pds!} label="Copy PDS" icon="lucide--copy" />
150
<NavMenu
151
href={`/firehose?instance=wss://${params.pds}`}
+35
-34
src/views/record.tsx
+35
-34
src/views/record.tsx
···
1
-
import { Client, CredentialManager } from "@atcute/client";
2
import { DidDocument, getPdsEndpoint } from "@atcute/identity";
3
import { lexiconDoc } from "@atcute/lexicon-doc";
4
import { RecordValidator } from "@atcute/lexicon-doc/validations";
···
8
import { verifyRecord } from "@atcute/repo";
9
import { A, useLocation, useNavigate, useParams } from "@solidjs/router";
10
import { createResource, createSignal, ErrorBoundary, Show, Suspense } from "solid-js";
11
import { Backlinks } from "../components/backlinks.jsx";
12
import { Button } from "../components/button.jsx";
13
-
import { RecordEditor, setPlaceholder } from "../components/create.jsx";
14
import {
15
CopyMenu,
16
DropdownMenu,
···
20
} from "../components/dropdown.jsx";
21
import { JSONValue } from "../components/json.jsx";
22
import { LexiconSchemaView } from "../components/lexicon-schema.jsx";
23
-
import { agent } from "../components/login.jsx";
24
import { Modal } from "../components/modal.jsx";
25
import { pds } from "../components/navbar.jsx";
26
import { addNotification, removeNotification } from "../components/notification.jsx";
···
67
});
68
}
69
70
-
const rpc = new Client({ handler: new CredentialManager({ service: pdsEndpoint }) });
71
const response = await rpc.get("com.atproto.repo.getRecord", {
72
params: {
73
repo: authority,
···
207
setValidSchema(undefined);
208
setLexiconUri(undefined);
209
const pds = await resolvePDS(did!);
210
-
rpc = new Client({ handler: new CredentialManager({ service: pds }) });
211
const res = await rpc.get("com.atproto.repo.getRecord", {
212
params: {
213
repo: did as ActorIdentifier,
···
362
<div class="flex items-center gap-0.5">
363
<A
364
classList={{
365
-
"flex items-center gap-1 border-b-2": true,
366
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
367
!isActive(),
368
}}
···
381
<Show when={record()} keyed>
382
<div class="flex w-full flex-col items-center">
383
<div class="dark:shadow-dark-700 dark:bg-dark-300 mb-3 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700">
384
-
<div class="ml-1 flex gap-3">
385
<RecordTab tab="record" label="Record" />
386
<RecordTab tab="schema" label="Schema" />
387
<RecordTab tab="backlinks" label="Backlinks" />
···
389
</div>
390
<div class="flex gap-0.5">
391
<Show when={agent() && agent()?.sub === record()?.uri.split("/")[2]}>
392
-
<RecordEditor create={false} record={record()?.value} refetch={refetch} />
393
-
<Tooltip text="Delete">
394
-
<button
395
-
class="flex items-center rounded-sm p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
396
-
onclick={() => setOpenDelete(true)}
397
-
>
398
-
<span class="iconify lucide--trash-2"></span>
399
-
</button>
400
-
</Tooltip>
401
-
<Modal open={openDelete()} onClose={() => setOpenDelete(false)}>
402
-
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
403
-
<h2 class="mb-2 font-semibold">Delete this record?</h2>
404
-
<div class="flex justify-end gap-2">
405
-
<Button onClick={() => setOpenDelete(false)}>Cancel</Button>
406
-
<Button
407
-
onClick={deleteRecord}
408
-
class="dark:shadow-dark-700 rounded-lg bg-red-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-red-400 active:bg-red-400"
409
-
>
410
-
Delete
411
-
</Button>
412
</div>
413
-
</div>
414
-
</Modal>
415
</Show>
416
<MenuProvider>
417
-
<DropdownMenu
418
-
icon="lucide--ellipsis-vertical"
419
-
buttonClass="rounded-sm p-1.5"
420
-
menuClass="top-9 p-2 text-sm"
421
-
>
422
<CopyMenu
423
content={JSON.stringify(record()?.value, null, 2)}
424
label="Copy record"
···
1
+
import { Client, simpleFetchHandler } from "@atcute/client";
2
import { DidDocument, getPdsEndpoint } from "@atcute/identity";
3
import { lexiconDoc } from "@atcute/lexicon-doc";
4
import { RecordValidator } from "@atcute/lexicon-doc/validations";
···
8
import { verifyRecord } from "@atcute/repo";
9
import { A, useLocation, useNavigate, useParams } from "@solidjs/router";
10
import { createResource, createSignal, ErrorBoundary, Show, Suspense } from "solid-js";
11
+
import { hasUserScope } from "../auth/scope-utils";
12
+
import { agent } from "../auth/state";
13
import { Backlinks } from "../components/backlinks.jsx";
14
import { Button } from "../components/button.jsx";
15
+
import { RecordEditor, setPlaceholder } from "../components/create";
16
import {
17
CopyMenu,
18
DropdownMenu,
···
22
} from "../components/dropdown.jsx";
23
import { JSONValue } from "../components/json.jsx";
24
import { LexiconSchemaView } from "../components/lexicon-schema.jsx";
25
import { Modal } from "../components/modal.jsx";
26
import { pds } from "../components/navbar.jsx";
27
import { addNotification, removeNotification } from "../components/notification.jsx";
···
68
});
69
}
70
71
+
const rpc = new Client({ handler: simpleFetchHandler({ service: pdsEndpoint }) });
72
const response = await rpc.get("com.atproto.repo.getRecord", {
73
params: {
74
repo: authority,
···
208
setValidSchema(undefined);
209
setLexiconUri(undefined);
210
const pds = await resolvePDS(did!);
211
+
rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
212
const res = await rpc.get("com.atproto.repo.getRecord", {
213
params: {
214
repo: did as ActorIdentifier,
···
363
<div class="flex items-center gap-0.5">
364
<A
365
classList={{
366
+
"border-b-2": true,
367
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
368
!isActive(),
369
}}
···
382
<Show when={record()} keyed>
383
<div class="flex w-full flex-col items-center">
384
<div class="dark:shadow-dark-700 dark:bg-dark-300 mb-3 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700">
385
+
<div class="ml-1 flex items-center gap-3">
386
<RecordTab tab="record" label="Record" />
387
<RecordTab tab="schema" label="Schema" />
388
<RecordTab tab="backlinks" label="Backlinks" />
···
390
</div>
391
<div class="flex gap-0.5">
392
<Show when={agent() && agent()?.sub === record()?.uri.split("/")[2]}>
393
+
<Show when={hasUserScope("update")}>
394
+
<RecordEditor create={false} record={record()?.value} refetch={refetch} />
395
+
</Show>
396
+
<Show when={hasUserScope("delete")}>
397
+
<Tooltip text="Delete">
398
+
<button
399
+
class="flex items-center rounded-sm p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
400
+
onclick={() => setOpenDelete(true)}
401
+
>
402
+
<span class="iconify lucide--trash-2"></span>
403
+
</button>
404
+
</Tooltip>
405
+
<Modal open={openDelete()} onClose={() => setOpenDelete(false)}>
406
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
407
+
<h2 class="mb-2 font-semibold">Delete this record?</h2>
408
+
<div class="flex justify-end gap-2">
409
+
<Button onClick={() => setOpenDelete(false)}>Cancel</Button>
410
+
<Button
411
+
onClick={deleteRecord}
412
+
class="dark:shadow-dark-700 rounded-lg bg-red-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-red-400 active:bg-red-400"
413
+
>
414
+
Delete
415
+
</Button>
416
+
</div>
417
</div>
418
+
</Modal>
419
+
</Show>
420
</Show>
421
<MenuProvider>
422
+
<DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5">
423
<CopyMenu
424
content={JSON.stringify(record()?.value, null, 2)}
425
label="Copy record"
+51
-55
src/views/repo.tsx
+51
-55
src/views/repo.tsx
···
1
-
import { Client, CredentialManager } from "@atcute/client";
2
-
import { parseDidKey, parsePublicMultikey } from "@atcute/crypto";
3
import { DidDocument } from "@atcute/identity";
4
import { ActorIdentifier, Did, Handle, Nsid } from "@atcute/lexicons";
5
import { A, useLocation, useNavigate, useParams } from "@solidjs/router";
···
39
resolvePDS,
40
validateHandle,
41
} from "../utils/api.js";
42
import { BlobView } from "./blob.jsx";
43
import { PlcLogView } from "./logs.jsx";
44
···
86
};
87
88
return (
89
-
<A class="flex items-center" href={`/at://${params.repo}#${props.tab}`}>
90
-
<span
91
-
classList={{
92
-
"flex items-center border-b-2": true,
93
-
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
94
-
!isActive(),
95
-
}}
96
-
>
97
-
{props.label}
98
-
</span>
99
</A>
100
);
101
};
···
115
if (!did.startsWith("did:")) {
116
try {
117
const did = await resolveHandle(params.repo as Handle);
118
-
navigate(location.pathname.replace(params.repo!, did));
119
return;
120
} catch {
121
try {
122
const nsid = params.repo as Nsid;
123
const res = await resolveLexiconAuthority(nsid);
124
-
navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`);
125
return;
126
} catch {
127
-
navigate(`/${did}`);
128
return;
129
}
130
}
···
141
return {};
142
}
143
144
-
rpc = new Client({ handler: new CredentialManager({ service: pds }) });
145
-
const res = await rpc.get("com.atproto.repo.describeRepo", {
146
-
params: { repo: did as ActorIdentifier },
147
-
});
148
-
if (res.ok) {
149
-
const collections: Record<string, { hidden: boolean; nsids: string[] }> = {};
150
-
res.data.collections.forEach((c) => {
151
-
const nsid = c.split(".");
152
-
if (nsid.length > 2) {
153
-
const authority = `${nsid[0]}.${nsid[1]}`;
154
-
collections[authority] = {
155
-
nsids: (collections[authority]?.nsids ?? []).concat(nsid.slice(2).join(".")),
156
-
hidden: false,
157
-
};
158
-
}
159
});
160
-
setNsids(collections);
161
-
} else {
162
-
console.error(res.data.error);
163
-
switch (res.data.error) {
164
-
case "RepoDeactivated":
165
-
setError("Deactivated");
166
-
break;
167
-
case "RepoTakendown":
168
-
setError("Takendown");
169
-
break;
170
-
default:
171
-
setError("Unreachable");
172
}
173
-
}
174
175
-
return res.data;
176
};
177
178
const [repo] = createResource(fetchRepo);
···
274
<Show when={repo()}>
275
<div class="flex w-full flex-col gap-3 wrap-break-word">
276
<div class="dark:shadow-dark-700 dark:bg-dark-300 flex justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700">
277
-
<div class="ml-1 flex gap-2 text-xs sm:gap-4 sm:text-sm">
278
<Show when={!error()}>
279
<RepoTab tab="collections" label="Collections" />
280
</Show>
···
305
</Tooltip>
306
</Show>
307
<MenuProvider>
308
-
<DropdownMenu
309
-
icon="lucide--ellipsis-vertical"
310
-
buttonClass="rounded-sm p-1.5"
311
-
menuClass="top-9 p-2 text-sm"
312
-
>
313
<CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" />
314
<NavMenu
315
href={`/jetstream?dids=${params.repo}`}
···
480
<Show when={location.hash === "#identity" || (error() && !location.hash)}>
481
<Show when={didDoc()}>
482
{(didDocument) => (
483
-
<div class="flex flex-col gap-2 wrap-anywhere">
484
{/* ID Section */}
485
<div>
486
<div class="flex items-center gap-1">
···
572
#{verif.id.split("#")[1]}
573
</span>
574
<span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300">
575
-
<ErrorBoundary fallback={<>unknown</>}>
576
-
{parsePublicMultikey(key()).type}
577
-
</ErrorBoundary>
578
</span>
579
</div>
580
<div class="font-mono break-all">{key()}</div>
···
598
{(key) => (
599
<div class="text-sm">
600
<span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300">
601
-
{parseDidKey(key).type}
602
</span>
603
<div class="font-mono break-all">{key.replace("did:key:", "")}</div>
604
</div>
···
1
+
import { Client, simpleFetchHandler } from "@atcute/client";
2
import { DidDocument } from "@atcute/identity";
3
import { ActorIdentifier, Did, Handle, Nsid } from "@atcute/lexicons";
4
import { A, useLocation, useNavigate, useParams } from "@solidjs/router";
···
38
resolvePDS,
39
validateHandle,
40
} from "../utils/api.js";
41
+
import { detectDidKeyType, detectKeyType } from "../utils/key.js";
42
import { BlobView } from "./blob.jsx";
43
import { PlcLogView } from "./logs.jsx";
44
···
86
};
87
88
return (
89
+
<A
90
+
classList={{
91
+
"border-b-2": true,
92
+
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": !isActive(),
93
+
}}
94
+
href={`/at://${params.repo}#${props.tab}`}
95
+
>
96
+
{props.label}
97
</A>
98
);
99
};
···
113
if (!did.startsWith("did:")) {
114
try {
115
const did = await resolveHandle(params.repo as Handle);
116
+
navigate(location.pathname.replace(params.repo!, did), { replace: true });
117
return;
118
} catch {
119
try {
120
const nsid = params.repo as Nsid;
121
const res = await resolveLexiconAuthority(nsid);
122
+
navigate(`/at://${res}/com.atproto.lexicon.schema/${nsid}`, { replace: true });
123
return;
124
} catch {
125
+
navigate(`/${did}`, { replace: true });
126
return;
127
}
128
}
···
139
return {};
140
}
141
142
+
rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
143
+
try {
144
+
const res = await rpc.get("com.atproto.repo.describeRepo", {
145
+
params: { repo: did as ActorIdentifier },
146
});
147
+
if (res.ok) {
148
+
const collections: Record<string, { hidden: boolean; nsids: string[] }> = {};
149
+
res.data.collections.forEach((c) => {
150
+
const nsid = c.split(".");
151
+
if (nsid.length > 2) {
152
+
const authority = `${nsid[0]}.${nsid[1]}`;
153
+
collections[authority] = {
154
+
nsids: (collections[authority]?.nsids ?? []).concat(nsid.slice(2).join(".")),
155
+
hidden: false,
156
+
};
157
+
}
158
+
});
159
+
setNsids(collections);
160
+
} else {
161
+
console.error(res.data.error);
162
+
switch (res.data.error) {
163
+
case "RepoDeactivated":
164
+
setError("Deactivated");
165
+
break;
166
+
case "RepoTakendown":
167
+
setError("Takendown");
168
+
break;
169
+
default:
170
+
setError("Unreachable");
171
+
}
172
}
173
174
+
return res.data;
175
+
} catch {
176
+
return {};
177
+
}
178
};
179
180
const [repo] = createResource(fetchRepo);
···
276
<Show when={repo()}>
277
<div class="flex w-full flex-col gap-3 wrap-break-word">
278
<div class="dark:shadow-dark-700 dark:bg-dark-300 flex justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700">
279
+
<div class="ml-1 flex items-center gap-2 text-xs sm:gap-4 sm:text-sm">
280
<Show when={!error()}>
281
<RepoTab tab="collections" label="Collections" />
282
</Show>
···
307
</Tooltip>
308
</Show>
309
<MenuProvider>
310
+
<DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5">
311
<CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" />
312
<NavMenu
313
href={`/jetstream?dids=${params.repo}`}
···
478
<Show when={location.hash === "#identity" || (error() && !location.hash)}>
479
<Show when={didDoc()}>
480
{(didDocument) => (
481
+
<div class="flex flex-col gap-3 wrap-anywhere">
482
{/* ID Section */}
483
<div>
484
<div class="flex items-center gap-1">
···
570
#{verif.id.split("#")[1]}
571
</span>
572
<span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300">
573
+
{detectKeyType(key())}
574
</span>
575
</div>
576
<div class="font-mono break-all">{key()}</div>
···
594
{(key) => (
595
<div class="text-sm">
596
<span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300">
597
+
{detectDidKeyType(key)}
598
</span>
599
<div class="font-mono break-all">{key.replace("did:key:", "")}</div>
600
</div>
+3
-1
src/views/settings.tsx
+3
-1
src/views/settings.tsx
···
1
import { createSignal } from "solid-js";
2
import { TextInput } from "../components/text-input.jsx";
3
4
export const [hideMedia, setHideMedia] = createSignal(localStorage.hideMedia === "true");
5
···
9
<div class="flex items-center gap-1 font-semibold">
10
<span>Settings</span>
11
</div>
12
-
<div class="flex flex-col gap-2">
13
<div class="flex flex-col gap-0.5">
14
<label for="plcDirectory" class="select-none">
15
PLC Directory
···
24
}}
25
/>
26
</div>
27
<div class="flex justify-between">
28
<div class="flex items-center gap-1">
29
<input
···
1
import { createSignal } from "solid-js";
2
import { TextInput } from "../components/text-input.jsx";
3
+
import { ThemeSelection } from "../components/theme.jsx";
4
5
export const [hideMedia, setHideMedia] = createSignal(localStorage.hideMedia === "true");
6
···
10
<div class="flex items-center gap-1 font-semibold">
11
<span>Settings</span>
12
</div>
13
+
<div class="flex flex-col gap-3">
14
<div class="flex flex-col gap-0.5">
15
<label for="plcDirectory" class="select-none">
16
PLC Directory
···
25
}}
26
/>
27
</div>
28
+
<ThemeSelection />
29
<div class="flex justify-between">
30
<div class="flex items-center gap-1">
31
<input